Compare commits

...

85 Commits

Author SHA1 Message Date
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
jmiller cd1d3241bd Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup into dev
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 16s
Generic: Project CI / Lint & Validate (pull_request) Successful in 20s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 11s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 17s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 29m21s
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
2026-06-28 14:03:25 -05:00
jmiller e2d88313cf merge: resolve version conflicts (keep dev's 01.43.37)
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-28 14:02:44 -05:00
gitea-actions[bot] 256d79a270 chore(version): pre-release bump to 01.44.01-dev [skip ci] 2026-06-28 18:57:02 +00:00
gitea-actions[bot] d8d5a1e48e chore(version): auto-bump patch 01.43.38-dev [skip ci] 2026-06-28 18:56:50 +00:00
jmiller 330cfa9dc5 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-28 18:56:20 +00:00
jmiller 8485f24342 docs: update changelog for v01.43.35, add retention to README
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-28 13:56:17 -05:00
gitea-actions[bot] 51c4db5115 chore: promote changelog [Unreleased] → [01.44.00] 2026-06-28 18:52:14 +00:00
gitea-actions[bot] fd33c86157 chore(release): build 01.44.00 [skip ci] 2026-06-28 18:52:07 +00:00
gitea-actions[bot] ac7673805e chore(version): pre-release bump to 01.43.37-dev [skip ci] 2026-06-28 18:51:38 +00:00
gitea-actions[bot] 428e217b56 chore(version): auto-bump patch 01.43.36-dev [skip ci] 2026-06-28 18:51:13 +00:00
jmiller c0ba29aca8 Merge pull request 'release: promote dev to main (v01.43.x)' (#157) from dev into main 2026-06-28 18:50:58 +00:00
jmiller 81a72d466b merge: resolve workflow file conflicts (take main's v05.02.00)
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m0s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
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
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 11s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m0s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 29s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 21m34s
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-28 13:50:24 -05:00
gitea-actions[bot] 942b33f9ce chore(version): pre-release bump to 01.43.35-dev [skip ci] 2026-06-28 18:46:50 +00:00
jmiller 60e04cfd0a Merge pull request 'fix: remove ordering column from profiles table' (#158) from fix/remove-profiles-ordering into dev
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 44s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
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 / 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 Issues (pull_request) Has been cancelled
2026-06-28 18:46:09 +00:00
gitea-actions[bot] 672b953ef5 chore(version): pre-release bump to 01.43.34-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
2026-06-28 18:45:40 +00:00
jmiller 1b481f2e2c fix: remove ordering column from profiles table
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
The ordering column was unused in the profiles UI — profiles sort by ID.
Drops the column via migration, removes all references from model,
config query, importer, and install SQL.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-28 13:45:01 -05:00
gitea-actions[bot] 4a027d6245 chore(version): pre-release bump to 01.43.32-dev [skip ci] 2026-06-28 07:49:06 +00:00
jmiller 8af19f875c chore: add SECURITY.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
2026-06-28 07:25:44 +00:00
jmiller b56e4060bf chore: add SECURITY.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
2026-06-28 07:15:34 +00:00
jmiller 9b5d0475f5 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-27 20:45:20 +00:00
jmiller 53e11fe9ad chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-27 05:32:40 +00:00
gitea-actions[bot] 9757658c34 chore(version): pre-release bump to 01.43.31-dev [skip ci] 2026-06-27 02:33:25 +00:00
gitea-actions[bot] c82378128a chore(version): auto-bump patch 01.43.30-dev [skip ci] 2026-06-27 02:33:14 +00:00
jmiller f95505704a fix: add submodule checkout to pre-release workflow
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
The CI checkout step was missing submodules: recursive, causing
MokoSuiteClient to be an empty gitlink during builds. This resulted
in broken MokoSuiteClient.zip and "Install path does not exist" errors.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-26 21:32:49 -05:00
gitea-actions[bot] 6cdc9b04d0 chore(version): pre-release bump to 01.43.29-dev [skip ci] 2026-06-27 02:20:50 +00:00
jmiller bad73529ae Merge pull request 'fix: SSH key indicator, schema alignment, MokoSuiteClient bundle' (#155) from fix/ssh-key-indicator into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
2026-06-27 02:20:30 +00:00
jmiller 3af3708020 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-27 00:49:34 +00:00
jmiller 3098b7ad40 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-25 19:47:25 +00:00
jmiller d10bda3321 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-25 19:47:24 +00:00
jmiller 12214bade1 chore: sync ci-issue-reporter.yml from Template-Generic [skip ci] 2026-06-25 19:47:22 +00:00
jmiller ee1de178b1 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-25 17:12:17 +00:00
jmiller 014d659908 chore: sync version-set.yml from Template-Generic [skip ci] 2026-06-25 17:12:17 +00:00
jmiller 113febad3f chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-25 17:12:16 +00:00
jmiller 18a3a524f2 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-25 17:12:16 +00:00
jmiller 659fc6e274 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-25 17:12:15 +00:00
jmiller 89af9fa14c chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-25 17:12:14 +00:00
jmiller d60535ae64 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-25 17:12:14 +00:00
jmiller b9ef947feb chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-25 17:12:13 +00:00
jmiller e7ab83c5fb chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-25 17:12:12 +00:00
jmiller dc81cd7cc8 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-25 17:12:11 +00:00
jmiller 1c256bba7a chore: sync version-set.yml from Template-Generic [skip ci] 2026-06-24 11:51:45 +00:00
jmiller cd4dc6efd2 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-24 11:51:44 +00:00
jmiller 2e6b71ac97 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-24 11:51:41 +00:00
jmiller 5b4f84bad7 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-24 11:51:39 +00:00
jmiller 847bb9bd0f chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-24 11:51:37 +00:00
jmiller ebcaf44b63 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-24 11:51:32 +00:00
jmiller 17b05f9a13 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-24 11:51:30 +00:00
71 changed files with 935 additions and 296 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ on:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions: permissions:
contents: write contents: write
+13 -11
View File
@@ -7,7 +7,7 @@
# INGROUP: mokocli.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template # 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 # BRIEF: Universal build & release detects platform from manifest.xml
# #
# +=======================================================================+ # +=======================================================================+
@@ -52,7 +52,7 @@ on:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
@@ -75,6 +75,7 @@ jobs:
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1 fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools - name: Setup mokocli tools
env: env:
@@ -102,7 +103,7 @@ jobs:
php ${MOKO_CLI}/branch_rename.php \ php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ --api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}" --pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git - name: Checkout rc and configure git
@@ -121,7 +122,7 @@ jobs:
- name: Update RC release notes from CHANGELOG.md - name: Update RC release notes from CHANGELOG.md
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Extract [Unreleased] section from changelog # Extract [Unreleased] section from changelog
@@ -173,6 +174,7 @@ jobs:
with: with:
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes - name: Configure git for bot pushes
run: | run: |
@@ -269,7 +271,7 @@ jobs:
!startsWith(steps.platform.outputs.platform, 'joomla') !startsWith(steps.platform.outputs.platform, 'joomla')
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}" SEMVER_TAG="v${VERSION}"
@@ -294,7 +296,7 @@ jobs:
- name: Update release notes and promote changelog - name: Update release notes and promote changelog
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Get the stable release info (version and ID) # Get the stable release info (version and ID)
@@ -363,7 +365,7 @@ jobs:
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \ php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \ --version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
@@ -392,7 +394,7 @@ jobs:
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc) # Delete rc branch (ephemeral — created by promote-rc)
@@ -416,7 +418,7 @@ jobs:
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}" BRANCH_NAME="version/${VERSION}"
@@ -437,7 +439,7 @@ jobs:
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \ php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true --branch dev --path . 2>&1 || true
@@ -463,5 +465,5 @@ jobs:
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi fi
+6
View File
@@ -13,6 +13,12 @@
name: "Generic: Project CI" name: "Generic: Project CI"
on: on:
pull_request:
branches:
- main
- dev
- dev/**
- rc/**
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -0,0 +1,68 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
# VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
name: "Universal: CI Issue Reporter"
on:
workflow_call:
inputs:
gate:
description: "CI gate name (e.g. PR Validation, Repository Health)"
required: true
type: string
details:
description: "Human-readable failure description"
required: true
type: string
severity:
description: "error or warning"
required: false
type: string
default: "error"
workflow:
description: "Workflow name for the issue title"
required: false
type: string
default: ""
secrets:
MOKOGITEA_TOKEN:
required: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
report:
name: "Report: ${{ inputs.gate }}"
runs-on: ubuntu-latest
steps:
- name: Clone MokoCLI
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
- name: Report CI failure
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
/tmp/mokocli/cli/ci_issue_reporter.sh \
--gate "${{ inputs.gate }}" \
--details "${{ inputs.details }}" \
--severity "${{ inputs.severity }}" \
--workflow "${{ inputs.workflow }}"
+10 -10
View File
@@ -21,7 +21,7 @@ permissions:
contents: write contents: write
env: env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs: jobs:
cleanup: cleanup:
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Delete merged branches - name: Delete merged branches
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Merged Branch Cleanup ===" echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API # List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name') "${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0 DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main # Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}" echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true "${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
fi fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs - name: Clean old workflow runs
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
echo "=== Workflow Run Cleanup ===" echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs # Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \ "${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0 DELETED=0
for RUN_ID in $RUNS; do for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1)) DELETED=$((DELETED + 1))
done done
+5 -5
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation # INGROUP: mokocli.Automation
# VERSION: 01.43.26 # VERSION: 02.52.24
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
@@ -19,7 +19,7 @@ permissions:
issues: write issues: write
env: env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs: jobs:
create-branch: create-branch:
@@ -28,8 +28,8 @@ jobs:
steps: steps:
- name: Create branch and comment - name: Create branch and comment
run: | run: |
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}" ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}" ISSUE_TITLE="${{ github.event.issue.title }}"
@@ -58,7 +58,7 @@ jobs:
echo "Created branch: ${BRANCH}" echo "Created branch: ${BRANCH}"
# Comment on issue with branch link # Comment on issue with branch link
REPO_URL="${GITEA_URL}/${{ github.repository }}" REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`" BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
curl -sf -X POST \ curl -sf -X POST \
+10 -23
View File
@@ -496,39 +496,26 @@ jobs:
steps: steps:
- name: Trigger RC pre-release - name: Trigger RC pre-release
env: env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: | run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}" curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ────────────────────────────────────────────────────── # ── Issue Reporter ──────────────────────────────────────────────────────
report-issues: report-issues:
name: Report Issues name: Report Issues
runs-on: ubuntu-latest
needs: [branch-policy, validate] needs: [branch-policy, validate]
if: >- if: >-
always() && always() &&
needs.validate.result == 'failure' needs.validate.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
steps: with:
- name: Checkout gate: "PR Validation"
uses: actions/checkout@v4 workflow: "PR Check"
with: severity: error
sparse-checkout: automation/ci-issue-reporter.sh details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
sparse-checkout-cone-mode: false secrets: inherit
- name: "File issue for PR validation failure"
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
./automation/ci-issue-reporter.sh \
--gate "PR Validation" \
--workflow "PR Check" \
--severity error \
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
+6 -1
View File
@@ -7,7 +7,7 @@
# INGROUP: mokocli.Release # INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00 # VERSION: 05.02.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
@@ -59,6 +59,11 @@ jobs:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }} token: ${{ secrets.MOKOGITEA_TOKEN }}
ref: ${{ github.ref_name }} ref: ${{ github.ref_name }}
submodules: recursive
- name: Update submodules to main
run: |
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
- name: Setup mokocli tools - name: Setup mokocli tools
env: env:
+18 -13
View File
@@ -29,12 +29,20 @@ jobs:
steps: steps:
- name: Rename branch - name: Rename branch
env:
BRANCH: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
BRANCH="${{ github.event.pull_request.head.ref }}" set -euo pipefail
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then
echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1
fi
SUFFIX="${BRANCH#rc/}" SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}" DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches" API="${GITEA_URL}/api/v1/repos/${REPO}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch # Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
@@ -42,25 +50,22 @@ jobs:
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \ -d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true) "${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
else else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})" echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1
exit 1
fi fi
# Delete rc/ branch # Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection)
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');") ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));')
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \ STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \ -H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true) "${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY"
else else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})" echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY"
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
+25 -37
View File
@@ -77,7 +77,7 @@ jobs:
- name: Check actor permission (admin only) - name: Check actor permission (admin only)
id: perm id: perm
env: env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }} TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }} ACTOR: ${{ github.actor }}
run: | run: |
@@ -671,42 +671,30 @@ jobs:
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# Issue Reporter — file issues for failed gates # Issue Reporter — file issues for failed gates
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
report-issues: report-scripts:
name: "Report Issues" name: "Report: Scripts Governance"
runs-on: ubuntu-latest needs: [access_check, scripts_governance]
needs: [access_check, scripts_governance, repo_health]
if: >- if: >-
always() && always() &&
(needs.scripts_governance.result == 'failure' || needs.scripts_governance.result == 'failure'
needs.repo_health.result == 'failure') uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "Scripts Governance"
workflow: "Repo Health"
severity: error
details: "Scripts directory policy violations detected. Review required and allowed directories."
secrets: inherit
steps: report-health:
- name: Checkout name: "Report: Repository Health"
uses: actions/checkout@v4 needs: [access_check, repo_health]
with: if: >-
sparse-checkout: automation/ci-issue-reporter.sh always() &&
sparse-checkout-cone-mode: false needs.repo_health.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
- name: "File issues for failed gates" with:
env: gate: "Repository Health"
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} workflow: "Repo Health"
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} severity: error
run: | details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
chmod +x automation/ci-issue-reporter.sh secrets: inherit
REPORTER="./automation/ci-issue-reporter.sh"
WF="Repo Health"
report_gate() {
local gate="$1" result="$2" details="$3"
if [ "$result" = "failure" ]; then
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
fi
}
report_gate "Scripts Governance" \
"${{ needs.scripts_governance.result }}" \
"Scripts directory policy violations detected. Review required and allowed directories."
report_gate "Repository Health" \
"${{ needs.repo_health.result }}" \
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
+130
View File
@@ -0,0 +1,130 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
# VERSION: 01.00.00
# BRIEF: Set or reset the extension version across all version-bearing files
name: "Joomla: Set Version"
on:
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 01.00.00)"
required: true
type: string
branch:
description: "Branch to update (default: current)"
required: false
type: string
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
VERSION="${{ inputs.version }}"
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
exit 1
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Update manifest version
run: |
MANIFEST=""
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla extension manifest found — skipping manifest update"
else
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
fi
- name: Update README.md version
run: |
if [ -f "README.md" ]; then
if grep -qP '^\s*VERSION:\s*\d' README.md; then
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
echo "README.md version updated to ${VERSION}"
else
echo "::warning::No VERSION line found in README.md — skipping"
fi
fi
- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
# Check if this version already has an entry
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
else
# Insert new version entry after [Unreleased] or at the top after header
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
else
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
fi
echo "CHANGELOG.md: added entry for ${VERSION}"
fi
else
echo "::warning::No CHANGELOG.md found — skipping"
fi
- name: Update FILE INFORMATION blocks
run: |
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
while IFS= read -r -d '' FILE; do
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
echo "Updated FILE INFORMATION VERSION in ${FILE}"
fi
done
- name: Commit and push
run: |
git config user.name "Moko Consulting [bot]"
git config user.email "hello@mokoconsulting.tech"
git add -A
if git diff --cached --quiet; then
echo "No version changes detected — nothing to commit"
else
git commit -m "chore: set version to ${VERSION} [skip bump]
Authored-by: Moko Consulting"
git push
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
fi
+12 -4
View File
@@ -13,6 +13,7 @@
name: "Universal: Workflow Sync Trigger" name: "Universal: Workflow Sync Trigger"
on: on:
workflow_dispatch:
pull_request: pull_request:
types: [closed] types: [closed]
branches: branches:
@@ -26,8 +27,9 @@ jobs:
name: Sync workflows to live repos name: Sync workflows to live repos
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: >- if: >-
github.event.pull_request.merged == true && github.event_name == 'workflow_dispatch' ||
!contains(github.event.pull_request.title, '[skip sync]') (github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]'))
steps: steps:
- name: Determine platform from repo name - name: Determine platform from repo name
@@ -49,8 +51,14 @@ jobs:
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}" MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install PHP
run: |
if ! command -v php &> /dev/null; then
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
fi
- name: Install dependencies - name: Install dependencies
run: | run: |
+31 -134
View File
@@ -1,7 +1,36 @@
# Changelog # Changelog
## [Unreleased] ## [Unreleased]
## [02.52.24] --- 2026-06-30
## [02.52.24] --- 2026-06-30
## [02.52.22] --- 2026-06-30
### Added
- 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
## [01.45.00] --- 2026-06-28
## [01.43.35] --- 2026-06-28
### Added ### Added
- Customizable restore script filename per backup profile (reduces discoverability on remote servers) - 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 standalone mode: multi-ZIP selector when multiple backup archives are present
@@ -17,142 +46,10 @@
- MokoRestore cleanup and security messages now reference the actual script filename instead of hardcoded "restore.php" - MokoRestore cleanup and security messages now reference the actual script filename instead of hardcoded "restore.php"
### Fixed ### Fixed
- SSH key indicator detection and missing delete language key
- Bootstrap 5 modal conversion for snapshots view (data-bs-dismiss, modal-footer, getOrCreateInstance) - Bootstrap 5 modal conversion for snapshots view (data-bs-dismiss, modal-footer, getOrCreateInstance)
- ntfy default URL changed from ntfy.sh to ntfy.mokoconsulting.tech - 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 - 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 - 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 - 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 - MokoRestore stalling: unhandled promise rejections from network errors or non-JSON responses left UI in loading state
## [01.43.00] --- 2026-06-24
## [01.43.00] --- 2026-06-24
## [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
### 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
+1
View File
@@ -19,6 +19,7 @@ Full-site backup and restore for Joomla — database, files, and configuration.
- Stepped AJAX engine prevents timeout on shared hosting - Stepped AJAX engine prevents timeout on shared hosting
- AES-256 ZIP encryption with configurable password - AES-256 ZIP encryption with configurable password
- Configurable archive naming with placeholders ([HOST], [DATE], [SITE_NAME], etc.) - Configurable archive naming with placeholders ([HOST], [DATE], [SITE_NAME], etc.)
- Per-profile retention — configure max backup count and max age (days) per profile, with global defaults
- Data sanitization — optionally clear user passwords, emails, and sessions in backup - Data sanitization — optionally clear user passwords, emails, and sessions in backup
### Content Snapshots ### Content Snapshots
+241
View File
@@ -0,0 +1,241 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 02.52.24
BRIEF: Security vulnerability reporting and handling policy
-->
# Security Policy
## Purpose and Scope
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
## Supported Versions
Security updates are provided for the following versions:
| Version | Supported |
| ------- | ------------------ |
| 01.x.x | :white_check_mark: |
| < 01.0 | :x: |
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
## Reporting a Vulnerability
### Where to Report
**DO NOT** create public GitHub issues for security vulnerabilities.
Report security vulnerabilities privately to:
**Email**: `security@mokoconsulting.tech`
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
### What to Include
A complete vulnerability report should include:
1. **Description**: Clear explanation of the vulnerability
2. **Impact**: Potential security impact and severity assessment
3. **Affected Versions**: Which versions are vulnerable
4. **Reproduction Steps**: Detailed steps to reproduce the issue
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
6. **Suggested Fix**: Proposed remediation (if known)
7. **Disclosure Timeline**: Your expectations for public disclosure
### Response Timeline
* **Initial Response**: Within 3 business days
* **Assessment Complete**: Within 7 business days
* **Fix Timeline**: Depends on severity (see below)
* **Disclosure**: Coordinated with reporter
## Severity Classification
Vulnerabilities are classified using the following severity levels:
### Critical
* Remote code execution
* Authentication bypass
* Data breach or exposure of sensitive information
* **Fix Timeline**: 7 days
### High
* Privilege escalation
* SQL injection or command injection
* Cross-site scripting (XSS) with significant impact
* **Fix Timeline**: 14 days
### Medium
* Information disclosure (limited scope)
* Denial of service
* Security misconfigurations with moderate impact
* **Fix Timeline**: 30 days
### Low
* Security best practice violations
* Minor information leaks
* Issues requiring user interaction or complex preconditions
* **Fix Timeline**: 60 days or next release
## Remediation Process
1. **Acknowledgment**: Security team confirms receipt and begins investigation
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
3. **Development**: Security patch is developed and tested
4. **Review**: Patch undergoes security review and validation
5. **Release**: Fixed version is released with security advisory
6. **Disclosure**: Public disclosure follows coordinated timeline
## Security Advisories
Security advisories are published via:
* GitHub Security Advisories
* Release notes and CHANGELOG.md
* Email notification to project users (if mailing list is established)
Advisories include:
* CVE identifier (if applicable)
* Severity rating
* Affected versions
* Fixed versions
* Mitigation steps
* Attribution (with reporter consent)
## Security Best Practices
For projects using this template:
### Required Controls
* Enable GitHub security features (Dependabot, code scanning)
* Implement branch protection on `main`
* Require code review for all changes
* Enforce signed commits (recommended)
* Use secrets management (never commit credentials)
* Maintain security documentation
* Follow secure coding standards defined in MokoStandards
### Joomla Plugin Security
* Follow Joomla security best practices
* Validate and sanitize all user input
* Use Joomla's database API to prevent SQL injection
* Properly escape output to prevent XSS
* Implement proper access control checks
* Use Joomla's session and authentication APIs
* Keep Joomla and dependencies up to date
### CI/CD Security
* Validate all inputs
* Sanitize outputs
* Use least privilege access
* Pin dependencies with hash verification
* Scan for vulnerabilities in dependencies
* Audit third-party actions and tools
#### Automated Security Scanning
All repositories SHOULD implement:
**CodeQL Analysis**:
* Enabled for PHP and other supported languages
* Runs on: push to main, pull requests, weekly schedule
* Query sets: `security-extended` and `security-and-quality`
* Configuration: `.github/workflows/codeql-analysis.yml`
**Dependabot Security Updates**:
* Weekly scans for vulnerable dependencies
* Automated pull requests for security patches
* Configuration: `.github/dependabot.yml`
**Secret Scanning**:
* Enabled by default with push protection
* Prevents accidental credential commits
### Dependency Management
* Keep dependencies up to date
* Monitor security advisories for dependencies
* Remove unused dependencies
* Audit new dependencies before adoption
* Document security-critical dependencies
## Compliance and Governance
This security policy is aligned with MokoStandards. Deviations require documented justification.
Security policies are reviewed and updated at least annually or following significant security incidents.
## Attribution and Recognition
We acknowledge and appreciate responsible disclosure. With your permission, we will:
* Credit you in security advisories
* List you in CHANGELOG.md for the fix release
* Recognize your contribution publicly (if desired)
## Contact and Escalation
* **Security Team**: security@mokoconsulting.tech
* **Primary Contact**: hello@mokoconsulting.tech
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
## Out of Scope
The following are explicitly out of scope:
* Issues in third-party dependencies (report directly to maintainers)
* Social engineering attacks
* Physical security issues
* Denial of service via resource exhaustion without amplification
* Issues requiring physical access to systems
* Theoretical vulnerabilities without proof of exploitability
---
## Metadata
| Field | Value |
| ------------ | ------------------------------------------------------------------------------------------------------------ |
| Document | Security Policy |
| Path | /SECURITY.md |
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
| Owner | Moko Consulting |
| Scope | Security vulnerability handling |
| Status | Active |
| Effective | 2026-01-16 |
## Revision History
| Date | Change Description | Author |
| ---------- | ------------------------------------------------- | --------------- |
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
@@ -15,5 +15,6 @@
<action name="mokosuitebackup.backup.purge" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_PURGE" /> <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.compare" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_COMPARE" />
<action name="mokosuitebackup.backup.browse" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE" /> <action name="mokosuitebackup.backup.browse" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE" />
<action name="mokosuitebackup.backup.cancel" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_CANCEL" />
</section> </section>
</access> </access>
@@ -21,7 +21,7 @@
type="sql" type="sql"
label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE" label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE"
description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC" description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC"
query="SELECT id AS value, CONCAT(title, ' (#', id, ')') AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC" query="SELECT id AS value, CONCAT(title, ' (#', id, ')') AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY id ASC"
default="1" default="1"
> >
<option value="1">Default Backup Profile (#1)</option> <option value="1">Default Backup Profile (#1)</option>
@@ -15,6 +15,7 @@
> >
<option value="">COM_MOKOJOOMBACKUP_FILTER_STATUS_ALL</option> <option value="">COM_MOKOJOOMBACKUP_FILTER_STATUS_ALL</option>
<option value="complete">COM_MOKOJOOMBACKUP_STATUS_COMPLETE</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="running">COM_MOKOJOOMBACKUP_STATUS_RUNNING</option>
<option value="fail">COM_MOKOJOOMBACKUP_STATUS_FAIL</option> <option value="fail">COM_MOKOJOOMBACKUP_STATUS_FAIL</option>
<option value="pending">COM_MOKOJOOMBACKUP_STATUS_PENDING</option> <option value="pending">COM_MOKOJOOMBACKUP_STATUS_PENDING</option>
@@ -207,6 +207,7 @@ COM_MOKOJOOMBACKUP_TYPE_DIFFERENTIAL="Differential (changed files + full DB)"
; Status labels ; Status labels
COM_MOKOJOOMBACKUP_STATUS_COMPLETE="Complete" COM_MOKOJOOMBACKUP_STATUS_COMPLETE="Complete"
COM_MOKOJOOMBACKUP_STATUS_WARNING="Warning"
COM_MOKOJOOMBACKUP_STATUS_RUNNING="Running" COM_MOKOJOOMBACKUP_STATUS_RUNNING="Running"
COM_MOKOJOOMBACKUP_STATUS_FAIL="Failed" COM_MOKOJOOMBACKUP_STATUS_FAIL="Failed"
COM_MOKOJOOMBACKUP_STATUS_PENDING="Pending" COM_MOKOJOOMBACKUP_STATUS_PENDING="Pending"
@@ -450,6 +451,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_COMPARE_DESC="Allows users to compare two backup records side-by-side."
COM_MOKOSUITEBACKUP_ACTION_BACKUP_BROWSE="Browse Archives" 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_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 ; Snapshot ACL
COM_MOKOSUITEBACKUP_ACTION_SNAPSHOT_MANAGE="Manage Snapshots" COM_MOKOSUITEBACKUP_ACTION_SNAPSHOT_MANAGE="Manage Snapshots"
@@ -500,6 +503,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_SUCCESS="%d backup(s) purged successfully."
COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted." 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) ; Remote Destinations (multi-remote)
COM_MOKOJOOMBACKUP_REMOTE_DESTINATIONS="Remote Destinations" COM_MOKOJOOMBACKUP_REMOTE_DESTINATIONS="Remote Destinations"
COM_MOKOJOOMBACKUP_REMOTE_ADD="Add Destination" COM_MOKOJOOMBACKUP_REMOTE_ADD="Add Destination"
@@ -116,3 +116,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_INVALID_DATE="Invalid date. Please select a valid date."
COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully." COM_MOKOJOOMBACKUP_PURGE_SUCCESS="%d backup(s) purged successfully."
COM_MOKOJOOMBACKUP_PURGE_PARTIAL="%d backup(s) purged, but %d could not be deleted." 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."
@@ -7,7 +7,7 @@
--> -->
<extension type="component" method="upgrade"> <extension type="component" method="upgrade">
<name>MokoSuiteBackup</name> <name>MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -55,7 +55,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
`ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL', `ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL',
`ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)', `ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)',
`published` TINYINT(1) NOT NULL DEFAULT 1, `published` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT(11) NOT NULL DEFAULT 0,
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
`modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
@@ -66,7 +65,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_records` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1, `profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1,
`description` VARCHAR(255) NOT NULL DEFAULT '', `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', `origin` VARCHAR(20) NOT NULL DEFAULT 'backend' COMMENT 'backend, cli, api, scheduled',
`backup_type` VARCHAR(20) NOT NULL DEFAULT 'full' COMMENT 'full, database, files', `backup_type` VARCHAR(20) NOT NULL DEFAULT 'full' COMMENT 'full, database, files',
`archivename` VARCHAR(512) NOT NULL DEFAULT '', `archivename` VARCHAR(512) NOT NULL DEFAULT '',
@@ -128,12 +127,12 @@ INSERT IGNORE INTO `#__mokosuitebackup_profiles` (
`id`, `title`, `description`, `backup_type`, `id`, `title`, `description`, `backup_type`,
`archive_format`, `compression_level`, `split_size`, `backup_dir`, `archive_format`, `compression_level`, `split_size`, `backup_dir`,
`exclude_dirs`, `exclude_files`, `exclude_tables`, `exclude_dirs`, `exclude_files`, `exclude_tables`,
`published`, `ordering`, `created`, `modified` `published`, `created`, `modified`
) VALUES ( ) VALUES (
1, 'Default Backup Profile', 'Full site backup with default settings', 'full', 1, 'Default Backup Profile', 'Full site backup with default settings', 'full',
'zip', 5, 0, '[DEFAULT_DIR]', 'zip', 5, 0, '[DEFAULT_DIR]',
'administrator/components/com_mokosuitebackup/backups\ntmp\ncache\nlogs\nadministrator/logs', 'administrator/components/com_mokosuitebackup/backups\ntmp\ncache\nlogs\nadministrator/logs',
'.gitignore\n.htaccess.bak', '.gitignore\n.htaccess.bak',
'#__session', '#__session',
1, 1, NOW(), NOW() 1, NOW(), NOW()
); );
@@ -0,0 +1 @@
/* 01.43.29 — no schema changes */
@@ -0,0 +1 @@
/* 01.43.30 — no schema changes */
@@ -0,0 +1 @@
/* 01.43.31 — no schema changes */
@@ -0,0 +1 @@
/* 01.43.32 — no schema changes */
@@ -0,0 +1 @@
ALTER TABLE `#__mokosuitebackup_profiles` DROP COLUMN `ordering`;
@@ -0,0 +1 @@
/* 01.43.34 — no schema changes */
@@ -0,0 +1 @@
/* 01.43.35 — no schema changes */
@@ -0,0 +1 @@
/* 01.43.36 — no schema changes */
@@ -0,0 +1 @@
/* 01.43.37 — no schema changes */
@@ -0,0 +1 @@
/* 01.43.38 — no schema changes */
@@ -0,0 +1 @@
/* 01.44.00 — no schema changes */
@@ -0,0 +1 @@
/* 01.44.01 — no schema changes */
@@ -0,0 +1 @@
/* 01.45.00 — 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 */
@@ -84,6 +84,67 @@ class AjaxController extends BaseController
$this->sendJson($result); $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. * Browse server directories for the folder picker field.
* POST: task=ajax.browseDir&path=/some/path * POST: task=ajax.browseDir&path=/some/path
@@ -451,7 +512,7 @@ class AjaxController extends BaseController
return; 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']); $this->sendJson(['error' => true, 'message' => 'Archive not available']);
return; return;
@@ -747,7 +808,7 @@ class AjaxController extends BaseController
->select('COUNT(*)') ->select('COUNT(*)')
->from($db->quoteName('#__mokosuitebackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) ->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); $db->setQuery($query);
$count = (int) $db->loadResult(); $count = (int) $db->loadResult();
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -199,7 +199,7 @@ class BackupsController extends AdminController
->select($db->quoteName('id')) ->select($db->quoteName('id'))
->from($db->quoteName('#__mokosuitebackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff)) ->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); $db->setQuery($query);
$ids = $db->loadColumn(); $ids = $db->loadColumn();
@@ -235,6 +235,76 @@ class BackupsController extends AdminController
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false)); $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. * No-op target for the purge toolbar button.
* *
@@ -249,7 +249,6 @@ class AkeebaImporter
'remote_keep_local' => 1, 'remote_keep_local' => 1,
'include_mokorestore' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'), 'include_mokorestore' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'),
'published' => 1, 'published' => 1,
'ordering' => (int) $akProfile->id,
'created' => $now, 'created' => $now,
'modified' => $now, 'modified' => $now,
]; ];
@@ -375,7 +375,7 @@ class BackupEngine
// Final record update (includes fields needed by NotificationSender) // Final record update (includes fields needed by NotificationSender)
$update = (object) [ $update = (object) [
'id' => $recordId, 'id' => $recordId,
'status' => 'complete', 'status' => $uploadFailed ? 'warning' : 'complete',
'description' => $description, 'description' => $description,
'backup_type' => $profile->backup_type, 'backup_type' => $profile->backup_type,
'archivename' => $archiveName, 'archivename' => $archiveName,
@@ -547,7 +547,16 @@ class BackupEngine
*/ */
private function createUploaderFromParams(string $type, array $params): RemoteUploaderInterface 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) { return match ($type) {
'ftp' => new FtpUploader($fake), 'ftp' => new FtpUploader($fake),
@@ -597,7 +606,7 @@ class BackupEngine
->select($db->quoteName('manifest')) ->select($db->quoteName('manifest'))
->from($db->quoteName('#__mokosuitebackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . $profileId) ->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('manifest') . ' != ' . $db->quote(''))
->where($db->quoteName('backup_type') . ' = ' . $db->quote('full')) ->where($db->quoteName('backup_type') . ' = ' . $db->quote('full'))
->order($db->quoteName('backupstart') . ' DESC'); ->order($db->quoteName('backupstart') . ' DESC');
@@ -346,6 +346,9 @@ define('MOKOJOOMBACKUP_RESTORE', 1);
define('RESTORE_DIR', __DIR__); define('RESTORE_DIR', __DIR__);
define('BACKUP_FILE', RESTORE_DIR . '/site-backup.zip'); 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(); session_start();
if (empty($_SESSION['restore_token'])) { 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. // 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 // 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. // 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'] ?? ''; $securityCode = $_SESSION['security_code'] ?? '';
if (empty($securityCode)) { if (empty($securityCode)) {
$securityCode = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8)); $securityCode = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));
$_SESSION['security_code'] = $securityCode; $_SESSION['security_code'] = $securityCode;
$_SESSION['security_verified'] = false; $_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" $securityContent = "<?php die('MokoRestore Security Code: " . $securityCode . "'); ?>\n"
. "MokoRestore Security Verification\n" . "MokoRestore Security Verification\n"
. "==================================\n" . "==================================\n"
. "Code: " . $securityCode . "\n" . "Code: " . $securityCode . "\n"
. "Enter this code in the MokoRestore browser interface to proceed.\n" . "Enter this code in the MokoRestore browser interface to proceed.\n"
. "This file will be deleted automatically after verification.\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; $_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) { if ($inputCode === $securityCode) {
$_SESSION['security_verified'] = true; $_SESSION['security_verified'] = true;
error_log('MokoRestore: Security code VERIFIED');
// Delete the security file
if (is_file($securityFile)) { if (is_file($securityFile)) {
@unlink($securityFile); @unlink($securityFile);
error_log('MokoRestore: Security file deleted');
} }
echo json_encode(['success' => true, 'message' => 'Security verified']); echo json_encode(['success' => true, 'message' => 'Security verified']);
} else { } 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; exit;
@@ -414,7 +431,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
} }
if (!$securityVerified) { 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; exit;
} }
@@ -424,9 +441,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
@ignore_user_abort(true); @ignore_user_abort(true);
try { try {
error_log('MokoRestore: Action dispatched — ' . $_POST['action']);
$result = handleAction($_POST['action'], $_POST); $result = handleAction($_POST['action'], $_POST);
error_log('MokoRestore: Action ' . $_POST['action'] . ' completed — ' . ($result['success'] ? 'OK' : 'FAIL: ' . ($result['message'] ?? '')));
echo json_encode($result); echo json_encode($result);
} catch (Throwable $e) { } catch (Throwable $e) {
error_log('MokoRestore: Action ' . $_POST['action'] . ' EXCEPTION — ' . $e->getMessage());
echo json_encode(['success' => false, 'message' => $e->getMessage()]); echo json_encode(['success' => false, 'message' => $e->getMessage()]);
} }
@@ -551,10 +571,14 @@ function actionPreflight(): array
function actionExtract(array $data): 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)) { if (!file_exists(BACKUP_FILE)) {
throw new RuntimeException('Backup file not found: site-backup.zip'); 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(); $zip = new ZipArchive();
if ($zip->open(BACKUP_FILE) !== true) { if ($zip->open(BACKUP_FILE) !== true) {
@@ -591,6 +615,8 @@ function actionExtract(array $data): array
$count = $zip->numFiles; $count = $zip->numFiles;
$zip->close(); $zip->close();
error_log('MokoRestore: Extract — ' . $count . ' files extracted to ' . RESTORE_DIR);
// Pre-fill from configuration.php.bak (sanitized backup) or // Pre-fill from configuration.php.bak (sanitized backup) or
// configuration.php (legacy/unsanitized backup). Skip [SANITIZED:] values. // configuration.php (legacy/unsanitized backup). Skip [SANITIZED:] values.
$existingConfig = []; $existingConfig = [];
@@ -719,6 +745,8 @@ function actionDatabase(array $data): array
$user = $data['db_user'] ?? ''; $user = $data['db_user'] ?? '';
$pass = $data['db_pass'] ?? ''; $pass = $data['db_pass'] ?? '';
error_log('MokoRestore: Database import — host=' . $host . ', db=' . $name . ', user=' . $user);
if (empty($name) || empty($user)) { if (empty($name) || empty($user)) {
throw new RuntimeException('Database name and user are required'); throw new RuntimeException('Database name and user are required');
} }
@@ -726,9 +754,12 @@ function actionDatabase(array $data): array
$sqlFile = RESTORE_DIR . '/database.sql'; $sqlFile = RESTORE_DIR . '/database.sql';
if (!is_file($sqlFile)) { 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]; 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( $pdo = new PDO(
"mysql:host={$host};dbname={$name};charset=utf8mb4", "mysql:host={$host};dbname={$name};charset=utf8mb4",
$user, $user,
@@ -835,6 +866,14 @@ function actionDatabase(array $data): array
$msg .= " ({$errors} warnings)"; $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 [ return [
'success' => ($statements > 0 || $errors === 0), 'success' => ($statements > 0 || $errors === 0),
'message' => $msg, 'message' => $msg,
@@ -847,6 +886,7 @@ function actionDatabase(array $data): array
function actionConfig(array $data): array function actionConfig(array $data): array
{ {
error_log('MokoRestore: Config rebuild started');
$host = $data['db_host'] ?? 'localhost'; $host = $data['db_host'] ?? 'localhost';
$dbName = $data['db_name'] ?? ''; $dbName = $data['db_name'] ?? '';
$dbUser = $data['db_user'] ?? ''; $dbUser = $data['db_user'] ?? '';
@@ -867,6 +907,7 @@ function actionConfig(array $data): array
// debug, cache, SEF, editor, etc.). Fall back to existing config // debug, cache, SEF, editor, etc.). Fall back to existing config
// for legacy/unsanitized backups, or build from scratch if neither exists. // for legacy/unsanitized backups, or build from scratch if neither exists.
$basePath = is_file($bakPath) ? $bakPath : (is_file($configPath) ? $configPath : null); $basePath = is_file($bakPath) ? $bakPath : (is_file($configPath) ? $configPath : null);
error_log('MokoRestore: Config — base template: ' . ($basePath ?? 'none (building from scratch)'));
if ($basePath !== null) { if ($basePath !== null) {
$config = file_get_contents($basePath); $config = file_get_contents($basePath);
@@ -919,9 +960,12 @@ function actionConfig(array $data): array
} }
if (file_put_contents($configPath, $config) === false) { 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']; 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 // Remove .bak after successful rebuild
if (is_file($bakPath)) { if (is_file($bakPath)) {
@unlink($bakPath); @unlink($bakPath);
@@ -1175,6 +1219,8 @@ function actionResetAdmin(array $data): array
$userId = (int) ($data['admin_id'] ?? 0); $userId = (int) ($data['admin_id'] ?? 0);
$password = $data['new_password'] ?? ''; $password = $data['new_password'] ?? '';
error_log('MokoRestore: Admin password reset — user_id=' . $userId);
if ($userId < 1 || strlen($password) < 8) { if ($userId < 1 || strlen($password) < 8) {
throw new RuntimeException('Select an admin and enter a password (8+ characters)'); 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'); throw new RuntimeException('User not found or password unchanged');
} }
error_log('MokoRestore: Admin password reset — success');
return ['success' => true, 'message' => 'Admin password updated successfully']; return ['success' => true, 'message' => 'Admin password updated successfully'];
} }
@@ -1197,6 +1244,7 @@ function actionPostRestore(array $data): array
$prefix = getValidatedPrefix($data); $prefix = getValidatedPrefix($data);
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: []; $tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
$results = []; $results = [];
error_log('MokoRestore: Post-restore — ' . count($tasks) . ' task(s): ' . implode(', ', $tasks));
foreach ($tasks as $task) { foreach ($tasks as $task) {
try { try {
@@ -1319,6 +1367,7 @@ function actionProvision(array $data): array
$prefix = getValidatedPrefix($data); $prefix = getValidatedPrefix($data);
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: []; $tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
$results = []; $results = [];
error_log('MokoRestore: Provisioning — ' . count($tasks) . ' task(s): ' . implode(', ', $tasks));
foreach ($tasks as $task) { foreach ($tasks as $task) {
try { try {
@@ -1395,16 +1444,24 @@ function actionProvision(array $data): array
function actionCleanup(): array function actionCleanup(): array
{ {
error_log('MokoRestore: Cleanup started');
$removed = []; $removed = [];
foreach (['database.sql', 'site-backup.zip'] as $file) { foreach (['database.sql', 'site-backup.zip', 'mokorestore-security.php'] as $file) {
$path = RESTORE_DIR . '/' . $file; $path = RESTORE_DIR . '/' . $file;
if (is_file($path) && @unlink($path)) { if (is_file($path)) {
$removed[] = $file; 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 [ return [
'success' => true, 'success' => true,
'message' => 'Removed: ' . (empty($removed) ? '(none)' : implode(', ', $removed)) '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 --> <!-- Step 0: Security Verification -->
<div class="mr-panel <?php echo $securityVerified ? '' : 'visible'; ?>" id="panel0"> <div class="mr-panel <?php echo $securityVerified ? '' : 'visible'; ?>" id="panel0">
<h2>Security Verification</h2> <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="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"> <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 <span style="font-size:1.1rem">&#128274;</span> How to find the code
</div> </div>
<ol style="margin:0;padding-left:1.25rem;color:#475569;font-size:0.9rem;line-height:1.6"> <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>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> <li>Copy the 8-character code and enter it below</li>
</ol> </ol>
</div> </div>
@@ -165,7 +165,7 @@ class PreflightCheck
->select($db->quoteName('total_size')) ->select($db->quoteName('total_size'))
->from($db->quoteName('#__mokosuitebackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id) ->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') ->where($db->quoteName('total_size') . ' > 0')
->order($db->quoteName('backupstart') . ' DESC'); ->order($db->quoteName('backupstart') . ' DESC');
$db->setQuery($query, 0, 1); $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. * 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 private function checkRunningBackup(object $profile, object $db): void
{ {
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('COUNT(*)') ->select($db->quoteName(['id', 'backupstart', 'absolute_path']))
->from($db->quoteName('#__mokosuitebackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id) ->where($db->quoteName('profile_id') . ' = ' . (int) $profile->id)
->where($db->quoteName('status') . ' = ' . $db->quote('running')); ->where($db->quoteName('status') . ' = ' . $db->quote('running'));
$db->setQuery($query); $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 $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]; 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 . ')']; return ['success' => false, 'message' => 'Cannot restore from incomplete backup (status: ' . $record->status . ')'];
} }
@@ -647,7 +647,7 @@ class SteppedBackupEngine
$update = (object) [ $update = (object) [
'id' => $session->recordId, 'id' => $session->recordId,
'status' => 'complete', 'status' => $uploadFailed ? 'warning' : 'complete',
'backupend' => date('Y-m-d H:i:s'), 'backupend' => date('Y-m-d H:i:s'),
'total_size' => $totalSize, 'total_size' => $totalSize,
'checksum' => $checksum, 'checksum' => $checksum,
@@ -889,7 +889,16 @@ class SteppedBackupEngine
*/ */
private function createUploaderFromParams(string $type, array $params): RemoteUploaderInterface 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) { return match ($type) {
'ftp' => new FtpUploader($fake), 'ftp' => new FtpUploader($fake),
@@ -64,7 +64,7 @@ class SteppedRestoreEngine
return ['error' => true, 'message' => 'Backup record not found: ' . $recordId]; 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 . ')']; return ['error' => true, 'message' => 'Cannot restore from incomplete backup (status: ' . $record->status . ')'];
} }
@@ -70,7 +70,7 @@ class BackupStatusHelper
]) ])
->from($db->quoteName('#__mokosuitebackup_records', 'r')) ->from($db->quoteName('#__mokosuitebackup_records', 'r'))
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id') ->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'); ->order($db->quoteName('r.backupstart') . ' DESC');
if ($profileId !== null) { if ($profileId !== null) {
@@ -148,7 +148,7 @@ class BackupStatusHelper
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('status')) ->select($db->quoteName('status'))
->from($db->quoteName('#__mokosuitebackup_records')) ->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') ->order($db->quoteName('backupstart') . ' DESC')
->setLimit(50); ->setLimit(50);
@@ -156,7 +156,7 @@ class BackupStatusHelper
$streak = 0; $streak = 0;
foreach ($statuses as $s) { foreach ($statuses as $s) {
if ($s === 'complete') { if ($s === 'complete' || $s === 'warning') {
$streak++; $streak++;
} else { } else {
break; break;
@@ -30,7 +30,7 @@ class DashboardModel extends BaseDatabaseModel
->select('r.*, p.title AS profile_title') ->select('r.*, p.title AS profile_title')
->from($db->quoteName('#__mokosuitebackup_records', 'r')) ->from($db->quoteName('#__mokosuitebackup_records', 'r'))
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id') ->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'); ->order($db->quoteName('r.backupend') . ' DESC');
$db->setQuery($query, 0, 1); $db->setQuery($query, 0, 1);
@@ -75,7 +75,7 @@ class DashboardModel extends BaseDatabaseModel
->select('COUNT(*) AS total_count') ->select('COUNT(*) AS total_count')
->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size') ->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size')
->from($db->quoteName('#__mokosuitebackup_records')) ->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); $db->setQuery($query);
$stats = $db->loadObject(); $stats = $db->loadObject();
@@ -274,7 +274,7 @@ class DashboardModel extends BaseDatabaseModel
->select('COALESCE(SUM(r.total_size), 0) AS total_size') ->select('COALESCE(SUM(r.total_size), 0) AS total_size')
->from($db->quoteName('#__mokosuitebackup_records', 'r')) ->from($db->quoteName('#__mokosuitebackup_records', 'r'))
->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id') ->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')) ->group($db->quoteName('r.profile_id'))
->order('total_size DESC'); ->order('total_size DESC');
$db->setQuery($query); $db->setQuery($query);
@@ -25,7 +25,6 @@ class ProfilesModel extends ListModel
'title', 'a.title', 'title', 'a.title',
'backup_type', 'a.backup_type', 'backup_type', 'a.backup_type',
'published', 'a.published', 'published', 'a.published',
'ordering', 'a.ordering',
]; ];
} }
@@ -41,7 +41,7 @@ class HtmlView extends BaseHtmlView
$user = Factory::getApplication()->getIdentity(); $user = Factory::getApplication()->getIdentity();
if ($this->item->status === 'complete' if (\in_array($this->item->status, ['complete', 'warning'], true)
&& !empty($this->item->filesexist) && !empty($this->item->filesexist)
&& $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup') && $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')
) { ) {
@@ -113,6 +113,10 @@ class HtmlView extends BaseHtmlView
ToolbarHelper::custom('backups.compare', 'copy', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_COMPARE', true); 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')) { if ($user->authorise('core.delete', 'com_mokosuitebackup')) {
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'backups.delete'); ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'backups.delete');
} }
@@ -30,6 +30,7 @@ $ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false)
<?php <?php
$statusClass = match ($this->item->status) { $statusClass = match ($this->item->status) {
'complete' => 'badge bg-success', 'complete' => 'badge bg-success',
'warning' => 'badge bg-warning text-dark',
'running' => 'badge bg-info', 'running' => 'badge bg-info',
'fail' => 'badge bg-danger', 'fail' => 'badge bg-danger',
default => 'badge bg-secondary', default => 'badge bg-secondary',
@@ -92,6 +92,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<?php <?php
$statusClass = match ($item->status) { $statusClass = match ($item->status) {
'complete' => 'badge bg-success', 'complete' => 'badge bg-success',
'warning' => 'badge bg-warning text-dark',
'running' => 'badge bg-info', 'running' => 'badge bg-info',
'fail' => 'badge bg-danger', 'fail' => 'badge bg-danger',
default => 'badge bg-secondary', 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_LAST_BACKUP="Last Backup"
MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK="Success" MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK="Success"
MOD_MOKOSUITEBACKUP_CPANEL_STATUS_WARNING="Warning"
MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL="Failed" MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL="Failed"
MOD_MOKOSUITEBACKUP_CPANEL_NO_BACKUPS="No backups yet." MOD_MOKOSUITEBACKUP_CPANEL_NO_BACKUPS="No backups yet."
MOD_MOKOSUITEBACKUP_CPANEL_FILES_TABLES="%d files, %d tables" MOD_MOKOSUITEBACKUP_CPANEL_FILES_TABLES="%d files, %d tables"
@@ -8,7 +8,7 @@
--> -->
<extension type="module" client="administrator" method="upgrade"> <extension type="module" client="administrator" method="upgrade">
<name>mod_mokosuitebackup_cpanel</name> <name>mod_mokosuitebackup_cpanel</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-23</creationDate> <creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -51,10 +51,20 @@ $moduleId = 'mod-msb-cpanel-' . $displayData['module']->id;
<?php if ($latest) : ?> <?php if ($latest) : ?>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<div> <div>
<span class="badge <?php echo $latest['status'] === 'complete' ? 'bg-success' : 'bg-danger'; ?>"> <?php
<?php echo $latest['status'] === 'complete' $cpanelBadge = match ($latest['status']) {
? Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_OK') 'complete' => 'bg-success',
: Text::_('MOD_MOKOSUITEBACKUP_CPANEL_STATUS_FAIL'); ?> '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>
<span class="ms-1 small text-muted"> <span class="ms-1 small text-muted">
<?php echo htmlspecialchars($latest['profile'] ?? ''); ?> <?php echo htmlspecialchars($latest['profile'] ?? ''); ?>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="actionlog" method="upgrade"> <extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name> <name>Action Log - MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="console" method="upgrade"> <extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name> <name>Console - MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="content" method="upgrade"> <extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name> <name>Content - MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-04</creationDate> <creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade"> <extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name> <name>Quick Icon - MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="system" method="upgrade"> <extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name> <name>System - MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="task" method="upgrade"> <extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name> <name>Task - MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
--> -->
<extension type="plugin" group="webservices" method="upgrade"> <extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name> <name>Web Services - MokoSuiteBackup</name>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade"> <extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name> <name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename> <packagename>mokosuitebackup</packagename>
<version>01.43.26</version> <version>02.52.24</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>