Compare commits

...

169 Commits

Author SHA1 Message Date
git-actions[bot] 1f49c49ebd chore: promote changelog [Unreleased] → [02.67.00]
Generic: Standards Compliance / Secret Scanning (push) Has been cancelled
Generic: Standards Compliance / License Header Validation (push) Has been cancelled
Generic: Standards Compliance / Repository Structure Validation (push) Has been cancelled
Generic: Standards Compliance / Coding Standards Check (push) Has been cancelled
Generic: Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Generic: Standards Compliance / Documentation Quality Check (push) Has been cancelled
Generic: Standards Compliance / README Completeness Check (push) Has been cancelled
Generic: Standards Compliance / Git Repository Hygiene (push) Has been cancelled
Generic: Standards Compliance / Script Integrity Validation (push) Has been cancelled
Generic: Standards Compliance / Line Length Check (push) Has been cancelled
Generic: Standards Compliance / File Naming Standards (push) Has been cancelled
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
Generic: Standards Compliance / Code Complexity Analysis (push) Has been cancelled
Generic: Standards Compliance / Dead Code Detection (push) Has been cancelled
Generic: Standards Compliance / File Size Limits (push) Has been cancelled
Generic: Standards Compliance / Binary File Detection (push) Has been cancelled
Generic: Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Generic: Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Generic: Standards Compliance / Broken Link Detection (push) Has been cancelled
Generic: Standards Compliance / API Documentation Coverage (push) Has been cancelled
Generic: Standards Compliance / Accessibility Check (push) Has been cancelled
Generic: Standards Compliance / Performance Metrics (push) Has been cancelled
Generic: Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Generic: Standards Compliance / Repository Health Check (push) Has been cancelled
Generic: Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
Generic: Standards Compliance / Version Consistency Check (push) Successful in 45s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 43s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
2026-07-17 00:33:27 +00:00
git-actions[bot] 64875771a7 chore(release): build 02.67.00 [skip ci] 2026-07-17 00:33:16 +00:00
jmiller 605e45a967 Release: dev -> main (#303) — pre-update backup hardening + config lang fix
Generic: Standards Compliance / Secret Scanning (push) Failing after 5s
Generic: Standards Compliance / License Header Validation (push) Successful in 4s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 4s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 5s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 5s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 4s
Generic: Standards Compliance / README Completeness Check (push) Failing after 4s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 7s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 6s
Generic: Standards Compliance / Line Length Check (push) Successful in 5s
Generic: Standards Compliance / File Naming Standards (push) Successful in 4s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 4s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 50s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 6s
Generic: Standards Compliance / File Size Limits (push) Successful in 4s
Generic: Standards Compliance / Binary File Detection (push) Successful in 7s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 4s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 48s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 47s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 6s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 4s
Generic: Standards Compliance / Accessibility Check (push) Successful in 4s
Generic: Standards Compliance / Performance Metrics (push) Successful in 3s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 52s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 57s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 9s
Generic: Standards Compliance / Enterprise Readiness Check (push) Failing after 50s
Generic: Standards Compliance / Repository Health Check (push) Failing after 50s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-17 00:32:56 +00:00
mokogit-actions[bot] d7639ec3d9 chore(version): pre-release bump to 02.66.07-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 34s
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (pull_request) Has been cancelled
2026-07-17 00:26:33 +00:00
git-actions[bot] 9c37bc6303 chore(version): auto-bump patch 02.66.06-dev [skip ci] 2026-07-17 00:26:14 +00:00
jmiller c01b0ab112 Merge branch 'fix/preaction-skip-ttl' into dev
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
fix(preupdate): time-bound the pre-action skip flag

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 19:24:56 -05:00
jmiller 462aed0e51 fix(preupdate): time-bound the pre-action skip flag
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 7s
The one-shot "client already backed up" skip flag was a bare boolean with no
expiry. If a user ran the full-screen pre-update backup then abandoned the flow
(closed the tab before the re-fired update fired), the flag lingered for the
whole session and could suppress a LATER, unrelated pre-action backup — a real
update running with no backup.

Store the unix time the flag was armed (AjaxController::armPreactionSkip) and
honour the skip only within PREACTION_SKIP_TTL (300s), consuming it one-shot.
A stale flag is now ignored and the server backs up as normal.

Completes the pre-update double-backup hardening: server-side arming stops the
"ack fails -> double backup" direction; the TTL stops the "abandoned flow ->
skipped backup" direction. Fail-safe both ways.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 19:24:37 -05:00
mokogit-actions[bot] c7f0a122a4 chore(version): pre-release bump to 02.66.05-dev [skip ci] 2026-07-17 00:20:45 +00:00
git-actions[bot] 5fac72b5ed chore(version): auto-bump patch 02.66.04-dev [skip ci] 2026-07-17 00:20:27 +00:00
jmiller 3efadf941f Merge branch 'fix/preupdate-double-backup' into dev
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
fix(preupdate): arm pre-action skip flag server-side to stop double backup

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 19:18:56 -05:00
jmiller dabc556e18 fix(preupdate): arm pre-action skip flag server-side to stop double backup
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 32s
The one-shot flag that tells plg_system_mokosuitebackup to skip its synchronous
pre-update/pre-uninstall backup was set only by the client-side ajax.preupdateAck
POST, whose failure the runbackup screen swallows in a catch(). If that POST
failed, the re-fired extension update ran a SECOND backup behind the black
"update is running" overlay — the "backup runs twice" symptom.

Arm the skip flag server-side in AjaxController::step the moment a backup that
was launched as a pre-update/pre-uninstall front completes (keyed by the
msb_action the runbackup screen now passes on each step). This is authoritative
and independent of the fragile ack POST, which is kept as a redundant fallback.

Failure-safe by design: the flag is armed ONLY on genuine completion (done, no
error), so a failed or cancelled backup never arms it — a real update is never
left un-backed-up. Worst case is one redundant backup, never a missing one.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 19:18:30 -05:00
jmiller 6717f0ed68 Release: merge dev to main
Generic: Standards Compliance / Secret Scanning (push) Failing after 4s
Generic: Standards Compliance / License Header Validation (push) Successful in 5s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 4s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 5s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 5s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 5s
Generic: Standards Compliance / README Completeness Check (push) Failing after 4s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 7s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 6s
Generic: Standards Compliance / File Naming Standards (push) Successful in 4s
Generic: Standards Compliance / Line Length Check (push) Successful in 5s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 5s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 51s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 7s
Generic: Standards Compliance / File Size Limits (push) Successful in 4s
Generic: Standards Compliance / Binary File Detection (push) Successful in 7s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 4s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 52s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 52s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 4s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 4s
Generic: Standards Compliance / Accessibility Check (push) Successful in 4s
Generic: Standards Compliance / Performance Metrics (push) Successful in 4s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 53s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 55s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 9s
Generic: Standards Compliance / Enterprise Readiness Check (push) Failing after 50s
Generic: Standards Compliance / Repository Health Check (push) Failing after 50s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
- feat(manifest): component 'Component - MokoSuiteBackup' pretty name via <element> tag (#299)
- fix(lang): Options page shows raw COM_MOKOSUITEBACKUP_CONFIGURATION (#234)

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i

# Conflicts:
#	.mokogit/workflows/issue-branch.yml
#	CHANGELOG.md
#	SECURITY.md
#	source/packages/com_mokosuitebackup/mokosuitebackup.xml
#	source/packages/mod_mokosuitebackup_cpanel/mod_mokosuitebackup_cpanel.xml
#	source/packages/plg_actionlog_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_console_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_content_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_quickicon_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_system_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_task_mokosuitebackup/mokosuitebackup.xml
#	source/packages/plg_webservices_mokosuitebackup/mokosuitebackup.xml
#	source/pkg_mokosuitebackup.xml
2026-07-16 17:00:31 -05:00
mokogit-actions[bot] a6de828fec chore(version): pre-release bump to 02.66.03-dev [skip ci] 2026-07-16 21:56:44 +00:00
git-actions[bot] a5e0775b0b chore(version): auto-bump patch 02.66.02-dev [skip ci] 2026-07-16 21:56:27 +00:00
jmiller cba074b0aa Merge branch 'fix/config-lang-stem' into dev
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
fix(lang): Options page shows raw COM_MOKOSUITEBACKUP_CONFIGURATION (#234)

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 16:55:51 -05:00
jmiller 455689532e chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-07-16 21:38:35 +00:00
jmiller a9882b9b51 fix(lang): Options page shows raw COM_MOKOSUITEBACKUP_CONFIGURATION
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
Joomla's com_config derives the Options-page title and component name from
the extension element (com_mokosuitebackup), but the language files defined
those keys only under the legacy COM_MOKOJOOMBACKUP_ stem, so the Options
screen rendered the raw constant.

Add the element-derived COM_MOKOSUITEBACKUP and COM_MOKOSUITEBACKUP_CONFIGURATION
keys to all four language files (en-GB/en-US, .ini and .sys.ini), mirroring the
already-migrated COM_MOKOSUITEBACKUP_ACTION_* ACL keys. The 400+ explicitly
referenced COM_MOKOJOOMBACKUP_* keys are unaffected.

Refs #234

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 14:45:02 -05:00
mokogit-actions[bot] 2d382cd62b chore(version): pre-release bump to 02.66.01-dev [skip ci] 2026-07-16 19:30:54 +00:00
jmiller db9734b156 Merge pull request 'feat(manifest): component uses Type - Name pretty name via <element> tag' (#299) from feat/component-typename-display 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 30s
2026-07-16 19:27:52 +00:00
jmiller a4c8b72733 feat(manifest): give the component a "Type - Name" pretty name safely
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 9s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
All sub-extensions already use the "Type - Name" convention; the
component was the exception because its <name> derives the element
(com_ + sanitized name), so "Component - MokoSuiteBackup" would produce
com_component-mokosuitebackup and orphan the real component.

Fix: pin the element with an explicit <element>com_mokosuitebackup</element>
tag. Joomla's InstallerAdapter::getElement() reads <element> before
falling back to <name>, so the element stays com_mokosuitebackup (matches
the existing record on upgrade — no orphan) while the Extensions manager
now shows "Component - MokoSuiteBackup". Manifest filename stays
mokosuitebackup.xml. Verified against Joomla 6.1 InstallerAdapter/
ComponentAdapter and manifest XML is well-formed.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 14:27:24 -05:00
git-actions[bot] 92ba36bc8d chore: promote changelog [Unreleased] → [02.66.00]
Generic: Standards Compliance / Secret Scanning (push) Failing after 4s
Generic: Standards Compliance / License Header Validation (push) Successful in 4s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 3s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 5s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 4s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 5s
Generic: Standards Compliance / README Completeness Check (push) Failing after 4s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 5s
Generic: Standards Compliance / Line Length Check (push) Successful in 4s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 6s
Generic: Standards Compliance / File Naming Standards (push) Successful in 4s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 4s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 50s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 6s
Generic: Standards Compliance / File Size Limits (push) Successful in 3s
Generic: Standards Compliance / Binary File Detection (push) Successful in 6s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 4s
Generic: Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Generic: Standards Compliance / Broken Link Detection (push) Has been cancelled
Generic: Standards Compliance / API Documentation Coverage (push) Has been cancelled
Generic: Standards Compliance / Accessibility Check (push) Has been cancelled
Generic: Standards Compliance / Performance Metrics (push) Has been cancelled
Generic: Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Generic: Standards Compliance / Repository Health Check (push) Has been cancelled
Generic: Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 48s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 49s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 49s
2026-07-16 19:22:48 +00:00
git-actions[bot] 2b6f3d709f chore(release): build 02.66.00 [skip ci] 2026-07-16 19:22:38 +00:00
jmiller 102746e834 Merge pull request 'Release: dev -> main (package install redeclare fix + backlog)' (#302) from release/dev-to-main-r3 into main
Generic: Standards Compliance / Secret Scanning (push) Failing after 4s
Generic: Standards Compliance / License Header Validation (push) Successful in 4s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 4s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 6s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 5s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 5s
Generic: Standards Compliance / README Completeness Check (push) Failing after 5s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 6s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 6s
Generic: Standards Compliance / Line Length Check (push) Successful in 5s
Generic: Standards Compliance / File Naming Standards (push) Successful in 4s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 4s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 52s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 7s
Generic: Standards Compliance / File Size Limits (push) Successful in 4s
Generic: Standards Compliance / Binary File Detection (push) Successful in 7s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 4s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 52s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 52s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 4s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 5s
Generic: Standards Compliance / Accessibility Check (push) Successful in 4s
Generic: Standards Compliance / Performance Metrics (push) Successful in 4s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 54s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 57s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 9s
Generic: Standards Compliance / Enterprise Readiness Check (push) Failing after 52s
Generic: Standards Compliance / Repository Health Check (push) Failing after 51s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
2026-07-16 19:21:59 +00:00
mokogit-actions[bot] 1f189349f5 chore(version): pre-release bump to 02.65.04-dev [skip ci]
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 32s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-07-16 19:21:10 +00:00
jmiller 28adcd0778 Merge pull request 'fix(install): guard package script against Cannot redeclare class fatal' (#301) from fix/package-script-redeclare-guard 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 33s
2026-07-16 19:20:43 +00:00
mokogit-actions[bot] 3644a99d07 chore(version): pre-release bump to 02.65.03-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-07-16 19:17:07 +00:00
jmiller c70f024201 fix(install): guard package script against "Cannot redeclare class" fatal
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 44s
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Package updates could fatal with a compile-time "Cannot redeclare class
Pkg_MokoSuiteBackupInstallerScript" when Joomla extracts/includes the
package script.php twice in one request (two tmp/install_* dirs). Because
the class is a top-level unconditional declaration it is early-bound at
compile time, so a runtime return can't prevent the redeclare — wrapping
it in `if (!class_exists(...)) { ... }` makes it a conditional (runtime)
declaration, so the second include is a no-op instead of a fatal.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 14:16:09 -05:00
mokogit-actions[bot] 9677edcf8b chore(version): pre-release bump to 02.65.02-dev [skip ci] 2026-07-16 18:59:59 +00:00
git-actions[bot] 90e6fe07ce chore(version): auto-bump patch 02.65.01-dev [skip ci] 2026-07-16 18:59:43 +00:00
git-actions[bot] 87c8a9a2ca chore: promote changelog [Unreleased] → [02.65.00]
Generic: Standards Compliance / Secret Scanning (push) Failing after 4s
Generic: Standards Compliance / License Header Validation (push) Successful in 3s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 3s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 4s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 5s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 4s
Generic: Standards Compliance / README Completeness Check (push) Failing after 5s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 7s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 5s
Generic: Standards Compliance / Line Length Check (push) Successful in 4s
Generic: Standards Compliance / File Naming Standards (push) Successful in 4s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 45s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 5s
Generic: Standards Compliance / File Size Limits (push) Successful in 3s
Generic: Standards Compliance / Binary File Detection (push) Successful in 5s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 40s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 43s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 5s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 5s
Generic: Standards Compliance / Accessibility Check (push) Successful in 4s
Generic: Standards Compliance / Performance Metrics (push) Successful in 4s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 54s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 58s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 8s
Generic: Standards Compliance / Enterprise Readiness Check (push) Failing after 51s
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Generic: Standards Compliance / Repository Health Check (push) Failing after 51s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
2026-07-16 18:44:09 +00:00
git-actions[bot] 21bcb0a54b chore(release): build 02.65.00 [skip ci] 2026-07-16 18:43:59 +00:00
jmiller 84fd06bf2d Merge pull request 'Release: dev -> main (restore fixes, theming, schema-checker, quickicon hardening)' (#300) from release/dev-to-main-r2 into main
Generic: Standards Compliance / Secret Scanning (push) Failing after 4s
Generic: Standards Compliance / License Header Validation (push) Successful in 4s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 4s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 5s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 4s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 4s
Generic: Standards Compliance / README Completeness Check (push) Failing after 4s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 7s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 5s
Generic: Standards Compliance / File Naming Standards (push) Successful in 4s
Generic: Standards Compliance / Line Length Check (push) Successful in 6s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 5s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 50s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 6s
Generic: Standards Compliance / File Size Limits (push) Successful in 3s
Generic: Standards Compliance / Binary File Detection (push) Successful in 6s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 45s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 44s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 3s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 3s
Generic: Standards Compliance / Accessibility Check (push) Successful in 3s
Generic: Standards Compliance / Performance Metrics (push) Successful in 4s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 48s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 51s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 7s
Generic: Standards Compliance / Enterprise Readiness Check (push) Failing after 47s
Generic: Standards Compliance / Repository Health Check (push) Failing after 46s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
2026-07-16 18:43:04 +00:00
jmiller 9a82fc97f2 Merge remote-tracking branch 'origin/dev' into release/dev-to-main-r2
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 32s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Standards Compliance / Secret Scanning (pull_request) Failing after 4s
Generic: Standards Compliance / License Header Validation (pull_request) Successful in 4s
Generic: Standards Compliance / Repository Structure Validation (pull_request) Successful in 4s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 20s
Generic: Standards Compliance / Coding Standards Check (pull_request) Successful in 5s
Generic: Standards Compliance / Workflow Configuration Check (pull_request) Failing after 5s
Generic: Standards Compliance / Documentation Quality Check (pull_request) Successful in 4s
Generic: Standards Compliance / README Completeness Check (pull_request) Failing after 4s
Generic: Standards Compliance / Git Repository Hygiene (pull_request) Successful in 6s
Generic: Standards Compliance / Script Integrity Validation (pull_request) Successful in 6s
Generic: Standards Compliance / Line Length Check (pull_request) Successful in 5s
Generic: Standards Compliance / File Naming Standards (pull_request) Successful in 4s
Generic: Standards Compliance / Insecure Code Pattern Detection (pull_request) Successful in 4s
Generic: Standards Compliance / Version Consistency Check (pull_request) Successful in 49s
Generic: Standards Compliance / Dead Code Detection (pull_request) Successful in 8s
Generic: Standards Compliance / File Size Limits (pull_request) Successful in 4s
Generic: Standards Compliance / Binary File Detection (pull_request) Successful in 6s
Generic: Standards Compliance / TODO/FIXME Tracking (pull_request) Successful in 4s
Generic: Standards Compliance / Code Complexity Analysis (pull_request) Successful in 51s
Generic: Standards Compliance / Code Duplication Detection (pull_request) Successful in 52s
Generic: Standards Compliance / Broken Link Detection (pull_request) Successful in 4s
Generic: Standards Compliance / API Documentation Coverage (pull_request) Successful in 4s
Generic: Standards Compliance / Accessibility Check (pull_request) Successful in 4s
Generic: Standards Compliance / Performance Metrics (pull_request) Successful in 4s
Generic: Standards Compliance / Dependency Vulnerability Scanning (pull_request) Successful in 50s
Generic: Standards Compliance / Unused Dependencies Check (pull_request) Successful in 53s
Generic: Standards Compliance / Terraform Configuration Validation (pull_request) Successful in 8s
Generic: Standards Compliance / Enterprise Readiness Check (pull_request) Failing after 51s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Standards Compliance / Repository Health Check (pull_request) Failing after 52s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
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
Generic: Standards Compliance / Compliance Summary (pull_request) Has been cancelled
# Conflicts:
#	.mokogit/workflows/issue-branch.yml
2026-07-16 13:40:57 -05:00
mokogit-actions[bot] 22e8ebe832 chore(version): pre-release bump to 02.64.13-dev [skip ci] 2026-07-16 18:09:26 +00:00
jmiller b1b81fd107 Merge pull request 'fix(quickicon): never let the dashboard widget crash the admin page' (#298) from fix/quickicon-harden 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 22s
2026-07-16 18:09:12 +00:00
mokogit-actions[bot] 747e50bf62 chore(version): pre-release bump to 02.64.12-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
2026-07-16 18:09:03 +00:00
jmiller 7e58ca7598 fix(quickicon): never let the dashboard widget crash the admin page (#297)
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
The mod_quickicon helper array_merge()s a defaults array into each icon a
plugin returns; if a plugin's DB query throws or it returns a malformed
(unwrapped) icon, the whole admin dashboard can 500 with an array_merge
TypeError. The current handler is structurally correct (icons are wrapped
in a list, matching core plg_quickicon_joomlaupdate), so this hardens it:

- Extract the query/state logic into buildStatusIcon().
- Wrap it in try/catch — on any Throwable, log and contribute no icon
  instead of bubbling into the dashboard render.
- Document why the [$icon] list-wrapping is load-bearing.

Closes #297.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 13:07:26 -05:00
mokogit-actions[bot] 54d7710783 chore(version): pre-release bump to 02.64.11-dev [skip ci] 2026-07-16 18:07:20 +00:00
jmiller f3bcf9d79e Merge pull request 'fix(restore): surface DatabaseImporter errors + quote-aware split' (#294) from fix/databaseimporter-surface-errors 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 30s
2026-07-16 18:06:56 +00:00
jmiller 028e1c7e80 fix(restore): surface DatabaseImporter errors + quote-aware split (#188)
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
RestoreEngine ignored DatabaseImporter::hasErrors(), so the non-stepped
restore always reported success even when SQL statements failed. It now
logs the failures and returns a warning ("database may be incomplete").
DatabaseImporter's splitter is also quote/backtick/escape-aware so a ; or
newline inside a value no longer splits a statement, and comments are only
skipped between statements — matching the stepped-engine fix (#291).

Closes #188.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 13:06:44 -05:00
mokogit-actions[bot] c42962c919 chore(version): pre-release bump to 02.64.10-dev [skip ci] 2026-07-16 18:02:27 +00:00
mokogit-actions[bot] 3616f49570 chore(version): pre-release bump to 02.64.09-dev [skip ci] 2026-07-16 18:02:01 +00:00
jmiller ed2dbee579 Merge pull request 'fix: default ntfy server to ntfy.mokoconsulting.tech' (#293) from fix/ntfy-default-url into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
2026-07-16 18:01:33 +00:00
jmiller 95d2b3375c Merge pull request 'fix: snapshots restore type labels show raw type' (#295) from fix/snapshots-typelabel 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 37s
2026-07-16 18:01:24 +00:00
jmiller 2b6dbdac23 fix: snapshots restore type labels show raw type (#170)
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 4s
typeLabels[TYPE] referenced an undefined uppercase variable; the forEach
parameter is lowercase `type`, so every type label rendered as the raw
type string instead of its translated label.

Closes #170.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 12:04:02 -05:00
jmiller 313b96b070 fix: default ntfy server to ntfy.mokoconsulting.tech (#166)
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
Replaces the old public ntfy.sh default in the runtime defaults: profile
form default/hint, NotificationSender fallbacks, the ntfy_server column
default in install.mysql.sql, and the en-GB field description. ntfy.sh is
still documented as the public alternative.

Closes #166.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 11:57:26 -05:00
jmiller 57e7dd98b1 Merge pull request 'fix(restore): surface partial DB-import failures + quote-aware SQL split' (#292) from fix/restore-silent-sql-import into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
2026-07-16 16:49:14 +00:00
jmiller 9323de955f fix(restore): surface partial DB-import failures + quote-aware SQL split
Stepped restore silently swallowed every failed SQL statement and always
reported "Restore complete", so a half-imported database looked fine.

- SteppedRestoreEngine::stepDatabase() now records a failure count + first
  few messages (recordSqlError) instead of only error_log(), and the
  completion step returns the count with a warning; the modal shows
  "Restore Complete (with warnings)" and does NOT auto-return.
- New quote/backtick/escape-aware isCompleteStatement() replaces the naive
  "line ends with ;" terminator, so a ; or newline inside a string value no
  longer splits a statement mid-value.
- Comment/blank lines are only skipped BETWEEN statements, never inside an
  accumulating multi-line statement.

Relates to #188 (DatabaseImporter) and the standalone MokoRestore splitter;
tracked in #291.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 11:48:41 -05:00
jmiller b0590bef80 Merge pull request 'fix(restore): clean up staging + session state on cancel/abandon' (#287) from fix/restore-cancel-cleanup into dev 2026-07-16 16:47:00 +00:00
jmiller ab6bee7b51 fix(restore): clean up staging + session state on cancel/abandon
The stepped-restore Cancel button (added with the progress-modal controls)
stopped the client loop and reloaded, but left the server-side restore
session, *.restore.json state, and the extracted staging directory behind
with no reaper — so cancelled/abandoned restores leaked disk indefinitely.

- SteppedRestoreEngine::cancel() mirrors the failure-cleanup path (delete
  staging dir + restore state + session).
- AjaxController::restoreCancel() exposes it (token + restore ACL gated).
- Restore modal Cancel now calls ajax.restoreCancel with the session id
  before leaving (session id tracked in restoreSessionId).
- SteppedSession::cleanupOldSessions() also reaps orphaned
  mokosuitebackup-restore-* staging dirs >24h (backstop for browser-close).

Found by an audit of the restore path against recent changes; the schema
(retired sftp_*/VARCHAR include_mokorestore) and PHP 8.3 changes were
confirmed to NOT affect restore.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 11:46:31 -05:00
jmiller 388db8c7d0 Merge pull request 'feat(ui): re-brand standalone MokoRestore wizard to the black theme' (#286) from feat/mokorestore-black-theme into dev 2026-07-16 16:46:07 +00:00
jmiller f8e121d925 feat(ui): re-brand the standalone MokoRestore wizard to the black theme
The generated restore.php wizard used a light "Joomla-styled" blue theme.
Restyled its stylesheet to the full-black MokoSuite look that matches the
runbackup / backup-progress screens: black background + big header, dark
panels/cards, dark form inputs with readable contrast, MokoSuite blue/green
accents, dark step-bar/checks/alerts/progress/log, and all inline help-box
styles converted from light to the dark palette. No logic changes.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 11:12:49 -05:00
jmiller e88040f60d Merge pull request 'feat(ui): auto-continue + action buttons on the backup-progress modal' (#285) from feat/progress-modal-controls into dev 2026-07-16 15:54:55 +00:00
jmiller 4ab24b20a6 feat(ui): mirror progress-modal controls onto the restore modal
Restore-progress modal now matches the backup modal: auto-continue
checkbox, terminal buttons (Retry restore / Back to backups), and a
double-confirm Cancel. Restore's cancel warns that stopping mid-flight
may leave the site partially restored (no server rollback), and there is
no "view record" button (restore has no new record). startSteppedRestore
split into a re-runnable runSteppedRestore() so Retry works. Title via
safe DOM (no innerHTML).

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 10:52:02 -05:00
jmiller d0c12914a8 feat(ui): add auto-continue + action buttons to the backup-progress modal
Brings the full-screen runbackup screen's controls onto the black
backup-progress modal in the backups view:
- "Automatically return to the list when finished" checkbox (default on);
  unchecked leaves the completion screen up with buttons.
- Cancel button while running (double confirm) — stops the step loop and
  cancels the record via ajax.cancelBackup.
- Terminal buttons: View backup record + Back to backups on success;
  Retry + Back to backups on failure.
Title icon/text updated via safe DOM (no innerHTML). Restore modal
unchanged in this pass.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 10:46:53 -05:00
jmiller c77e921582 Merge pull request 'fix(lang): drop redundant backup word in admin-lock message' (#284) from fix/lock-message-wording into dev 2026-07-16 15:38:50 +00:00
jmiller cac691ec92 fix(lang): drop redundant "backup" in admin-lock message
"A MokoSuiteBackup backup is currently running" -> "A MokoSuiteBackup
is currently running". en-GB + en-US.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 10:38:39 -05:00
jmiller e0b5d72c40 Merge pull request 'chore(ci): converge dev with main full gitea->git rebrand' (#283) from chore/converge-full-rebrand into dev 2026-07-16 14:38:14 +00:00
jmiller 0b9cf6f4d8 chore(ci): converge dev with main's full gitea->git rebrand
Back-fills dev with the fuller rebrand that shipped on main in the
dev->main release (#282): MokoGitea->MokoGIT, GITEA_ORG->GIT_ORG,
mokogit-actions[bot], and the corrected .mokogit/CLAUDE.md path. dev's
earlier partial rename (#279) is superseded so dev and main no longer
diverge on CI config.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 09:38:06 -05:00
jmiller 775942e9f7 Merge pull request 'Release: dev → main — schema-checker fix + unified progress theme' (#282) from release/dev-to-main into main 2026-07-16 14:36:06 +00:00
jmiller 532c5171dc Merge remote-tracking branch 'origin/dev' into release/dev-to-main
# Conflicts:
#	.mokogit/.mokogit/CLAUDE.md
#	.mokogit/.mokogit/workflows/branch-cleanup.yml
#	.mokogit/.mokogit/workflows/issue-branch.yml
#	.mokogit/CLAUDE.md
#	.mokogit/workflows/auto-release.yml
#	.mokogit/workflows/branch-cleanup.yml
#	.mokogit/workflows/ci-joomla.yml
#	.mokogit/workflows/issue-branch.yml
#	.mokogit/workflows/pr-metadata-check.yml
#	.mokogit/workflows/pre-release.yml
#	.mokogit/workflows/rc-revert.yml
#	.mokogitea/CLAUDE.md
#	.mokogitea/workflows/branch-cleanup.yml
#	.mokogitea/workflows/issue-branch.yml
2026-07-16 09:35:02 -05:00
jmiller 2eeb9343b3 Merge pull request 'feat(ui): unify backup/restore progress modals with the runbackup theme' (#281) from feat/unified-progress-theme into dev 2026-07-16 14:25:28 +00:00
jmiller 57d94b701b docs(changelog): note unified backup/restore progress theming
Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 09:25:04 -05:00
jmiller 15992574c3 feat(ui): unify backup/restore progress modals with the runbackup theme
Gives the stepped backup-progress and restore-progress modals in the
backups view the same full-screen black, big-text, Atum-coloured
progress-bar look as the view=runbackup screen, so every backup progress
surface matches.

- New media/css/backup-progress.css: black is scoped to .msb-progress-modal
  (not html/body), reuses the runbackup .msb-rb-progress/.msb-rb-bar bar with
  animated/success/danger states.
- Registered com_mokosuitebackup.backup-progress asset; loaded in the backups
  view via addStyleSheet (useStyle does not emit for this component).
- Both modals: full-screen dialog, big heading, big status text, shared bar.
- setBarState() drives animated -> green(complete)/red(fail) for both bars.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 09:24:29 -05:00
jmiller 7c6fe53b64 Merge pull request 'fix(db): reconcile mokosuitebackup_profiles schema with update history' (#278) from fix/schema-checker-profiles into dev 2026-07-16 14:23:39 +00:00
jmiller b481390355 fix(db): retire sftp_* checker demands instead of re-adding columns
Review of PR #278 caught that script.php postflight
(dropLegacyRemoteColumns) and 02.52.25.sql deliberately purge the
legacy sftp_* columns on every install/update, so re-adding them to
install.mysql.sql was futile — postflight drops them right back,
leaving the ADD-COLUMN checks (01.35.00/01.36.00) still failing.

Corrected approach:
- Revert the install.mysql.sql sftp_* block.
- Rewrite the retired sftp_* ADD COLUMN statements in 01.35.00/01.36.00
  as bare ADD. MySQL treats ADD and ADD COLUMN identically, but Joomla's
  MysqlChangeItem only builds an ADD-COLUMN check for the `ADD COLUMN`
  form; bare `ADD` is skipped. Upgrades still add the columns (so the
  01.41.00 migration works) and 02.52.25/postflight still drop them, so
  they stay absent at rest and the checker no longer flags them.

include_mokorestore fixes (01.01.01 VARCHAR, 01.39.00 MODIFY) and the
02.64.05 bump are unchanged.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 08:38:00 -05:00
mokogitea-actions[bot] 7425f9d204 chore(version): pre-release bump to 02.64.06-dev [skip ci] 2026-07-16 08:38:00 -05:00
jmiller 3765c99769 fix(db): reconcile mokosuitebackup_profiles schema with update history
Joomla's Database maintenance view reported "One Problem" on
MokoSuiteBackup with four sub-issues in #__mokosuitebackup_profiles.
All were schema-definition drift, not data corruption (confirmed
against Joomla 6.1 core: ChangeSet/MysqlChangeItem check each update
statement independently, with no cross-file supersession).

- install.mysql.sql: restore the legacy sftp_* columns so the old
  ADD COLUMN checks (01.35.00/01.36.00) pass on fresh installs. The
  data itself lives in #__mokosuitebackup_remotes; these columns are
  retained only for checker consistency with upgraded sites.
- 01.01.01.sql: declare include_mokorestore as VARCHAR(20) (was
  TINYINT, later widened in 01.39.00). Without supersession the stale
  TINYINT check fails forever against the now-VARCHAR column.
- 01.39.00.sql: MODIFY COLUMN -> MODIFY. The checker misparses
  "MODIFY COLUMN x" as column-name="COLUMN".
- 02.64.05.sql: idempotent MODIFY to guarantee VARCHAR on stragglers;
  version bump 02.64.04 -> 02.64.05 with its matching update file.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 08:37:59 -05:00
jmiller 4006a7e4c8 Merge pull request 'chore(ci): rename .mokogitea workflow directory to .mokogit' (#279) from chore/rename-mokogit-dir into dev 2026-07-16 13:37:24 +00:00
jmiller 595a5d9f3f chore(ci): rename MOKOGITEA_TOKEN/URL to MOKOGIT_TOKEN/URL
Completes the .mokogit rebrand to match main: the server secret was
renamed MOKOGITEA_TOKEN -> MOKOGIT_TOKEN and the workflow env var
MOKOGITEA_URL -> MOKOGIT_URL. Without this the merged workflows would
reference an undefined secret and CI auth would fail.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 08:36:49 -05:00
jmiller d1a5a57847 chore(ci): rename .mokogitea workflow directory to .mokogit
Server-side Actions scanning path was reconfigured to .mokogit/workflows.
Renames the directory (history preserved) and updates every internal path
reference: trigger globs, reusable-workflow `uses:` includes, WORKFLOWS_DIR,
REPO_REQUIRED_ARTIFACTS, PATH headers, and .gitattributes export-ignore.
Scope: this repo only. Product name "MokoGitea", the actions bot, env vars,
and URLs are unaffected.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-16 07:47:14 -05:00
mokogitea-actions[bot] 120cca5594 chore(version): pre-release bump to 02.64.04-dev [skip ci] 2026-07-15 03:33:52 +00:00
jmiller 536a9b0c78 Merge pull request 'feat: lock admin UI while a backup is running' (#277) from feat/lock-admin-during-backup 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 49s
2026-07-15 03:33:15 +00:00
jmiller 257003fb74 feat: lock the admin UI while a backup is actively running
Generic: Project CI / Lint & Validate (pull_request) Successful in 13s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 10s
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
New "Lock admin during backup" option (on by default). While a stepped
backup is running, every admin page load except the full-screen backup
screen and the login page is replaced by a "backup in progress"
interstitial that auto-refreshes, so no other window can navigate or change
anything mid-backup.

Detection uses the stepped-backup session file heartbeat
(JPATH_ROOT/tmp/mokosuitebackup-sessions/mb_*.json is rewritten every step
and deleted on completion) — a file with an mtime within ~45s means a
backup is live. A crashed/abandoned backup goes stale and releases the lock
on its own, so the admin is never permanently locked. Implemented in the
system plugin's onAfterRoute (admin, HTML page loads only; AJAX/assets and
the backup's own step requests are unaffected).

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-14 22:33:07 -05:00
jmiller d3e0b32737 fix(ci): harden branch-cleanup.yml against Actions injection (sync from Template-Generic) 2026-07-15 03:25:22 +00:00
jmiller 353adf17c7 fix(ci): harden issue-branch.yml against Actions injection (sync from Template-Generic) 2026-07-15 03:25:21 +00:00
mokogitea-actions[bot] 254bf18c45 chore(version): pre-release bump to 02.64.03-dev [skip ci] 2026-07-15 03:17:23 +00:00
jmiller 6fc57eb163 Merge pull request 'fix(task): scheduled-task fieldset label renders raw string' (#276) from fix/scheduler-fieldset-label 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 24s
2026-07-15 03:16:58 +00:00
jmiller 9e7ebfe89e fix(task): scheduled-task fieldset label renders raw string
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 10s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 10s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
com_scheduler's task edit view renders a params fieldset legend as
Text::_($fieldset->label ?: 'COM_SCHEDULER_FIELDSET_'.$name). The
run_profile / run_snapshot fieldsets had no `label` attribute, so it fell
back to an undefined generated key (COM_SCHEDULER_RUN_PROFILE_FIELDSET_LABEL)
and showed the raw constant. Give each fieldset a label attribute pointing
to a plugin-defined constant, and define them in en-GB/en-US.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-14 22:16:47 -05:00
jmiller 4ea0761706 Merge pull request 'chore(rebrand): fix residual gitea (space-path + named files)' (#275) from chore/rebrand-gitea-residual into main 2026-07-15 01:40:19 +00:00
jmiller 828aa3100e chore(rebrand): fix residual gitea (space-path files + gitea-named files)
Claude-Session: https://claude.ai/code/session_01DQEMmJPe61ya7HDfA6BHP8
2026-07-14 20:40:16 -05:00
jmiller c7b38067f6 Merge pull request 'chore(rebrand): gitea -> git (MokoGIT, .mokogitea -> .mokogit)' (#274) from chore/rebrand-gitea-to-git into main 2026-07-15 01:21:37 +00:00
jmiller 745f85d512 chore(rebrand): gitea -> git (MokoGIT brand, .mokogitea -> .mokogit)
Case-sensitive gitea->git: brand MokoGitea->MokoGIT, MOKOGITEA_TOKEN->
MOKOGIT_TOKEN, GITEA_URL/ORG/REPO->GIT_*, gitea-actions[bot]->git-actions,
ntfy gitea-*->git-*, .mokogitea/->.mokogit/. Protected: literal .gitea and
git.mokoconsulting.tech.

Claude-Session: https://claude.ai/code/session_01DQEMmJPe61ya7HDfA6BHP8
2026-07-14 20:21:34 -05:00
jmiller 823a36edbb chore: sync version-set.yml from Template-Joomla [skip ci] 2026-07-14 21:16:41 +00:00
jmiller 96b811a95c chore: sync standards-compliance.yml from Template-Joomla [skip ci] 2026-07-14 21:16:40 +00:00
jmiller b16970a857 chore: sync repo-health.yml from Template-Joomla [skip ci] 2026-07-14 21:16:39 +00:00
jmiller fe74431b7e chore: sync rc-revert.yml from Template-Joomla [skip ci] 2026-07-14 21:16:38 +00:00
jmiller 6cad0ed325 chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-07-14 21:16:37 +00:00
jmiller 6fcd18742e chore: sync pr-metadata-check.yml from Template-Joomla [skip ci] 2026-07-14 21:16:36 +00:00
jmiller 204fbe9349 chore: sync pr-check.yml from Template-Joomla [skip ci] 2026-07-14 21:16:35 +00:00
jmiller 8228e2b851 chore: sync notify.yml from Template-Joomla [skip ci] 2026-07-14 21:16:34 +00:00
jmiller 5d053e3a23 chore: sync issue-branch.yml from Template-Joomla [skip ci] 2026-07-14 21:16:33 +00:00
jmiller 52d4f56158 chore: sync gitleaks.yml from Template-Joomla [skip ci] 2026-07-14 21:16:32 +00:00
jmiller f31f90eb86 chore: sync cleanup.yml from Template-Joomla [skip ci] 2026-07-14 21:16:31 +00:00
jmiller 6e068ebd1d chore: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-07-14 21:16:30 +00:00
jmiller 117816e5a5 chore: sync ci-issue-reporter.yml from Template-Joomla [skip ci] 2026-07-14 21:16:29 +00:00
jmiller 31948f3e16 chore: sync ci-generic.yml from Template-Joomla [skip ci] 2026-07-14 21:16:28 +00:00
jmiller 18804ab76e chore: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-07-14 21:16:27 +00:00
jmiller ef69edb18d chore: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-07-14 21:16:26 +00:00
jmiller e97a5c17c7 chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-07-14 21:16:25 +00:00
jmiller 4ea1d311ac chore: sync auto-bump.yml from Template-Joomla [skip ci] 2026-07-14 21:16:24 +00:00
mokogitea-actions[bot] 8df658c5da chore(version): pre-release bump to 02.64.02-dev [skip ci] 2026-07-14 20:40:49 +00:00
gitea-actions[bot] 6c79443be0 chore(version): auto-bump patch 02.64.01-dev [skip ci] 2026-07-14 20:40:32 +00:00
gitea-actions[bot] 78d120d428 chore: promote changelog [Unreleased] → [02.64.00]
Generic: Standards Compliance / Secret Scanning (push) Failing after 4s
Generic: Standards Compliance / License Header Validation (push) Successful in 3s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 3s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 4s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 4s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 3s
Generic: Standards Compliance / README Completeness Check (push) Failing after 3s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 4s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 5s
Generic: Standards Compliance / Line Length Check (push) Successful in 3s
Generic: Standards Compliance / File Naming Standards (push) Successful in 4s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 41s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 4s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 6s
Generic: Standards Compliance / File Size Limits (push) Successful in 4s
Generic: Standards Compliance / Binary File Detection (push) Successful in 6s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 43s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 41s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 3s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 3s
Generic: Standards Compliance / Accessibility Check (push) Successful in 4s
Generic: Standards Compliance / Performance Metrics (push) Successful in 3s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 41s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 50s
Generic: Standards Compliance / Enterprise Readiness Check (push) Failing after 42s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 10s
Generic: Standards Compliance / Repository Health Check (push) Failing after 45s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
2026-07-14 20:33:23 +00:00
gitea-actions[bot] aa2473e4ac chore(release): build 02.64.00 [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-07-14 20:33:14 +00:00
jmiller 78556d2775 Merge pull request 'Release: dev -> main (runbackup UX + safety)' (#273) from dev into main
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Standards Compliance / Secret Scanning (push) Failing after 5s
Generic: Standards Compliance / License Header Validation (push) Successful in 4s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 10s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 4s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 5s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 4s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 4s
Generic: Standards Compliance / README Completeness Check (push) Failing after 5s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 6s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 6s
Generic: Standards Compliance / Line Length Check (push) Successful in 5s
Generic: Standards Compliance / File Naming Standards (push) Successful in 3s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 50s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 5s
Generic: Standards Compliance / File Size Limits (push) Successful in 3s
Generic: Standards Compliance / Binary File Detection (push) Successful in 6s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 4s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 50s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 50s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 3s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 4s
Generic: Standards Compliance / Accessibility Check (push) Successful in 4s
Generic: Standards Compliance / Performance Metrics (push) Successful in 4s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 44s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 50s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 8s
Generic: Standards Compliance / Enterprise Readiness Check (push) Failing after 42s
Generic: Standards Compliance / Repository Health Check (push) Failing after 42s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
2026-07-14 20:32:59 +00:00
mokogitea-actions[bot] 46dabae61c chore(version): pre-release bump to 02.63.11-dev [skip ci]
Universal: Build & Release / Promote to RC (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
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 27s
2026-07-14 20:32:52 +00:00
gitea-actions[bot] e21574a917 chore(version): auto-bump patch 02.63.10-dev [skip ci] 2026-07-14 20:32:41 +00:00
jmiller 79a914f930 Merge remote-tracking branch 'origin/dev' into dev
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
2026-07-14 15:32:27 -05:00
jmiller fe90f0024b @
chore: back-merge main into dev (reconcile release version stamps)

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
2026-07-14 15:31:52 -05:00
mokogitea-actions[bot] e9eb94ccfd chore(version): pre-release bump to 02.63.09-dev [skip ci] 2026-07-14 20:30:44 +00:00
jmiller 7e1a76a7cb Merge pull request 'fix(runbackup): failed pre-update backup blocks the update' (#272) from fix/no-update-on-error 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 19s
2026-07-14 20:30:15 +00:00
jmiller c48fd3b8eb fix(runbackup): a failed pre-update backup blocks the update
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Generic: Project CI / Tests (pull_request) Failing after 7s
Generic: Project CI / Lint & Validate (pull_request) Successful in 11s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
Universal: PR Check / Validate PR (pull_request) Failing after 18s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
If the pre-update/uninstall backup fails, the update must not run. The
completion screen no longer offers "Continue without backup" on error —
the only options are Retry (re-run the backup) or Back to dashboard (bail
out without updating). The status message makes clear the update was not
started.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-14 15:30:03 -05:00
mokogitea-actions[bot] f5423bab5f chore(version): pre-release bump to 02.63.07-dev [skip ci] 2026-07-14 20:18:42 +00:00
jmiller 4b17842496 Merge pull request 'feat(runbackup): cancel button with double confirmation' (#271) from feat/runbackup-cancel 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 22s
2026-07-14 20:18:25 +00:00
jmiller 85c0967e89 feat(runbackup): cancel button with double confirmation
Generic: Project CI / Lint & Validate (pull_request) Successful in 9s
Generic: Project CI / Tests (pull_request) Failing after 5s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 16s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Adds a "Cancel backup" button on the backup progress screen (visible while
the backup runs). Cancelling requires TWO confirmations; on confirm it
stops the stepped-backup loop, best-effort cancels the record server-side
(ajax.cancelBackup), and returns to the dashboard WITHOUT continuing any
pending update. The button is hidden once the backup completes or fails.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-14 15:17:38 -05:00
mokogitea-actions[bot] d89812cbfb chore(version): pre-release bump to 02.63.06-dev [skip ci] 2026-07-14 15:35:56 +00:00
jmiller 79efde483e Merge pull request 'feat(runbackup): do-not-close warning + update-running overlay' (#270) from feat/runbackup-dontclose-warning 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 23s
2026-07-14 15:35:39 +00:00
jmiller c567890bfa feat(runbackup): "do not close window" warning + "update is running" overlay
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Generic: Project CI / Tests (pull_request) Failing after 5s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 17s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
- Backup progress screen shows a prominent warning not to close or switch
  windows until the backup finishes (hidden once it completes/errors).
- After the backup hands back to an extension update, a full-screen
  "update is running — please wait" overlay is shown while the re-fired
  update POST processes, so the user knows it is working (not stuck).

Adds language strings (component + plugin) and a cfg.updateRunningText option.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
2026-07-14 10:35:23 -05:00
mokogitea-actions[bot] ba070f004d chore(version): pre-release bump to 02.63.05-dev [skip ci] 2026-07-14 15:13:02 +00:00
jmiller ce15cecf85 Merge pull request 'fix(installer): extension update auto-continues after pre-update backup' (#269) from fix/extension-refire into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
2026-07-14 15:12:30 +00:00
jmiller 2b4af3ce28 @
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
Generic: Project CI / Tests (pull_request) Failing after 5s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 9s
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 18s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
fix(installer): extension update now auto-continues after pre-update backup

Two bugs in the client-side resume path made the extension update return
to the selection screen (requiring a manual re-select + re-submit) instead
of continuing after the backup:

1. The re-fire clicked the OUTER <joomla-toolbar-button> (which carries the
   task attr), but the web component binds its click handler to the INNER
   <button>. Clicking the wrapper did nothing. Now clicks the inner element.
2. list-selection toolbar buttons start DISABLED and only enable when
   boxchecked fires a `change` event. We set boxchecked.value but never
   dispatched change, so the button stayed disabled and executeTask() no-opd.
2026-07-14 10:11:32 -05:00
jmiller a1cba42f43 chore: sync version-set.yml from Template-Joomla [skip ci] 2026-07-14 04:45:47 +00:00
jmiller 9eef484585 chore: sync repo-health.yml from Template-Joomla [skip ci] 2026-07-14 04:45:46 +00:00
jmiller ff14a1dcba chore: sync rc-revert.yml from Template-Joomla [skip ci] 2026-07-14 04:45:45 +00:00
jmiller 925b27f2dc chore: sync pre-release.yml from Template-Joomla [skip ci] 2026-07-14 04:45:44 +00:00
jmiller 4e519b4e7c chore: sync pr-metadata-check.yml from Template-Joomla [skip ci] 2026-07-14 04:45:43 +00:00
jmiller 118c1cbe2f chore: sync pr-check.yml from Template-Joomla [skip ci] 2026-07-14 04:45:42 +00:00
jmiller 05aec9b315 chore: sync notify.yml from Template-Joomla [skip ci] 2026-07-14 04:45:40 +00:00
jmiller b345e42cd0 chore: sync gitleaks.yml from Template-Joomla [skip ci] 2026-07-14 04:45:39 +00:00
jmiller f99e39efa3 chore: sync ci-joomla.yml from Template-Joomla [skip ci] 2026-07-14 04:45:38 +00:00
jmiller ae5e1f48dd chore: sync ci-issue-reporter.yml from Template-Joomla [skip ci] 2026-07-14 04:45:36 +00:00
jmiller 2ddd6c27cd chore: sync ci-generic.yml from Template-Joomla [skip ci] 2026-07-14 04:45:35 +00:00
jmiller 362e75a9a8 chore: sync cascade-dev.yml from Template-Joomla [skip ci] 2026-07-14 04:45:34 +00:00
jmiller cb4c227853 chore: sync branch-cleanup.yml from Template-Joomla [skip ci] 2026-07-14 04:45:33 +00:00
jmiller b8e8487f6b chore: sync auto-release.yml from Template-Joomla [skip ci] 2026-07-14 04:45:32 +00:00
jmiller 7abe3ef035 chore: sync auto-bump.yml from Template-Joomla [skip ci] 2026-07-14 04:45:31 +00:00
jmiller 344881809d Merge pull request 'chore(ci): remove template-only sync workflows from root' (#268) from chore/remove-template-sync-leaks into main
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Require Docs Update (pull_request) Has been cancelled
Universal: PR Check / Wiki Update Reminder (pull_request) Has been cancelled
Universal: PR Check / Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Has been cancelled
Cascade Main -> Dev / Cascade main -> dev (push) Has been cancelled
Generic: Standards Compliance / Secret Scanning (push) Has been cancelled
Generic: Standards Compliance / License Header Validation (push) Has been cancelled
Generic: Standards Compliance / Repository Structure Validation (push) Has been cancelled
Generic: Standards Compliance / Coding Standards Check (push) Has been cancelled
Generic: Standards Compliance / Version Consistency Check (push) Has been cancelled
Generic: Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Generic: Standards Compliance / Documentation Quality Check (push) Has been cancelled
Generic: Standards Compliance / README Completeness Check (push) Has been cancelled
Generic: Standards Compliance / Git Repository Hygiene (push) Has been cancelled
Generic: Standards Compliance / Script Integrity Validation (push) Has been cancelled
Generic: Standards Compliance / Line Length Check (push) Has been cancelled
Generic: Standards Compliance / File Naming Standards (push) Has been cancelled
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
Generic: Standards Compliance / Code Complexity Analysis (push) Has been cancelled
Generic: Standards Compliance / Code Duplication Detection (push) Has been cancelled
Generic: Standards Compliance / Dead Code Detection (push) Has been cancelled
Generic: Standards Compliance / File Size Limits (push) Has been cancelled
Generic: Standards Compliance / Binary File Detection (push) Has been cancelled
Generic: Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Generic: Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Generic: Standards Compliance / Broken Link Detection (push) Has been cancelled
Generic: Standards Compliance / API Documentation Coverage (push) Has been cancelled
Generic: Standards Compliance / Accessibility Check (push) Has been cancelled
Generic: Standards Compliance / Performance Metrics (push) Has been cancelled
Generic: Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Generic: Standards Compliance / Repository Health Check (push) Has been cancelled
Generic: Standards Compliance / Terraform Configuration Validation (push) 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: Standards Compliance / Compliance Summary (push) Has been cancelled
2026-07-13 20:46:34 +00:00
jmiller 47d4023108 chore(ci): remove template-only sync workflows from root
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
Generic: Project CI / Lint & Validate (pull_request) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Lint & Validate (pull_request) Has been cancelled
Joomla: Extension CI / Release Readiness Check (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: PR Check / Require Docs Update (pull_request) Has been cancelled
Universal: PR Check / Wiki Update Reminder (pull_request) Has been cancelled
Universal: PR Check / Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Generic: Standards Compliance / Secret Scanning (pull_request) Has been cancelled
Generic: Standards Compliance / License Header Validation (pull_request) Has been cancelled
Generic: Standards Compliance / Repository Structure Validation (pull_request) Has been cancelled
Generic: Standards Compliance / Coding Standards Check (pull_request) Has been cancelled
Generic: Standards Compliance / Version Consistency Check (pull_request) Has been cancelled
Generic: Standards Compliance / Workflow Configuration Check (pull_request) Has been cancelled
Generic: Standards Compliance / Documentation Quality Check (pull_request) Has been cancelled
Generic: Standards Compliance / README Completeness Check (pull_request) Has been cancelled
Generic: Standards Compliance / Git Repository Hygiene (pull_request) Has been cancelled
Generic: Standards Compliance / Script Integrity Validation (pull_request) Has been cancelled
Generic: Standards Compliance / Line Length Check (pull_request) Has been cancelled
Generic: Standards Compliance / File Naming Standards (pull_request) Has been cancelled
Generic: Standards Compliance / Insecure Code Pattern Detection (pull_request) Has been cancelled
Generic: Standards Compliance / Code Complexity Analysis (pull_request) Has been cancelled
Generic: Standards Compliance / Code Duplication Detection (pull_request) Has been cancelled
Generic: Standards Compliance / Dead Code Detection (pull_request) Has been cancelled
Generic: Standards Compliance / File Size Limits (pull_request) Has been cancelled
Generic: Standards Compliance / Binary File Detection (pull_request) Has been cancelled
Generic: Standards Compliance / TODO/FIXME Tracking (pull_request) Has been cancelled
Generic: Standards Compliance / Dependency Vulnerability Scanning (pull_request) Has been cancelled
Generic: Standards Compliance / Unused Dependencies Check (pull_request) Has been cancelled
Generic: Standards Compliance / Broken Link Detection (pull_request) Has been cancelled
Generic: Standards Compliance / API Documentation Coverage (pull_request) Has been cancelled
Generic: Standards Compliance / Accessibility Check (pull_request) Has been cancelled
Generic: Standards Compliance / Performance Metrics (pull_request) Has been cancelled
Generic: Standards Compliance / Enterprise Readiness Check (pull_request) Has been cancelled
Generic: Standards Compliance / Repository Health Check (pull_request) Has been cancelled
Generic: Standards Compliance / Terraform Configuration Validation (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
RC Revert / Rename rc/ back to dev/ (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
Generic: Standards Compliance / Compliance Summary (pull_request) Has been cancelled
Template-only (gated Template-*), dead no-ops in child repos, inherited at scaffold. Real sync lives in templates' workflows/custom/.
2026-07-13 20:46:29 +00:00
jmiller 373662b0d3 chore: sync standards-compliance.yml from Template-Generic [skip ci] 2026-07-13 19:51:49 +00:00
jmiller c7ea43edcf chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-13 16:22:34 +00:00
jmiller dbdb3a0e8a chore: sync cascade-dev.yml from Template-Generic [skip ci] 2026-07-13 16:22:32 +00:00
jmiller 1127b23d1e chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-13 16:22:30 +00:00
mokogitea-actions[bot] 1c41cdc03b chore(version): pre-release bump to 02.63.02-dev [skip ci] 2026-07-13 15:56:55 +00:00
jmiller 79b23215eb Merge pull request 'feat(runbackup): record link new window + harden leave warning' (#267) from feat/runbackup-record-newtab 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 24s
2026-07-13 15:56:37 +00:00
jmiller bd2effd89c @
Generic: Project CI / Tests (pull_request) Failing after 5s
Universal: PR Check / Require Docs Update (pull_request) Has been skipped
Universal: PR Check / Wiki Update Reminder (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Project CI / Lint & Validate (pull_request) Successful in 8s
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 17s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
feat(runbackup): open record link in new window; harden leave warning

- The "View backup record" link on the pre-update completion screen now
  opens in a new window (target=_blank rel=noopener), so viewing the
  record does not leave the "Continue the update" flow.
- beforeunload leave-warning now also returns the message (broader browser
  support) so closing the window mid-backup reliably prompts a warning.

Claude-Session: https://claude.ai/code/session_01WbGBN9VyRK61zczYWcCQ2i
@
2026-07-13 10:56:29 -05:00
jmiller 1c646332e2 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-07-13 15:50:07 +00:00
jmiller 2deaa4faa4 chore: sync version-set.yml from Template-Generic [skip ci] 2026-07-13 15:50:07 +00:00
jmiller 629b8be470 chore: sync sync-on-merge.yml from Template-Generic [skip ci] 2026-07-13 15:50:06 +00:00
jmiller d2dd8a8da4 chore: sync standards-compliance.yml from Template-Generic [skip ci] 2026-07-13 15:50:05 +00:00
jmiller ba90ce3365 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-07-13 15:50:04 +00:00
jmiller 2de8ab942b chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-07-13 15:50:04 +00:00
jmiller 3ccad868bf chore: sync pre-release.yml from Template-Generic [skip ci] 2026-07-13 15:50:03 +00:00
jmiller 4606f21a05 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-07-13 15:50:02 +00:00
jmiller aae23b46f0 chore: sync notify.yml from Template-Generic [skip ci] 2026-07-13 15:50:01 +00:00
jmiller 08135beaf0 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-07-13 15:50:01 +00:00
jmiller 2cd83e0369 chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-07-13 15:50:00 +00:00
jmiller a52ed25325 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-07-13 15:49:59 +00:00
jmiller 3c00fc2422 chore: sync ci-issue-reporter.yml from Template-Generic [skip ci] 2026-07-13 15:49:58 +00:00
jmiller c08015c324 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-07-13 15:49:58 +00:00
jmiller 90aa2cd58b chore: sync cascade-dev.yml from Template-Generic [skip ci] 2026-07-13 15:49:57 +00:00
jmiller f17367456a chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-07-13 15:49:57 +00:00
jmiller 92c4818410 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-07-13 15:49:56 +00:00
jmiller a5db8fba5b chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-07-13 15:49:55 +00:00
jmiller a133d80f5f chore: sync standards-compliance.yml from Template-Generic [skip ci] 2026-07-13 15:29:14 +00:00
mokogitea-actions[bot] 6da7ef0b69 chore(version): pre-release bump to 02.63.01-dev [skip ci] 2026-07-13 15:06:15 +00:00
jmiller 65cf9fbf9f chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-07-13 10:12:01 +00:00
mokogitea-actions[bot] cbc8627fb7 chore(version): pre-release bump to 02.62.05-dev [skip ci] 2026-07-13 09:50:29 +00:00
gitea-actions[bot] f1dfc18f3d chore(version): auto-bump patch 02.62.04-dev [skip ci] 2026-07-13 09:50:15 +00:00
107 changed files with 6489 additions and 1764 deletions
+1 -1
View File
@@ -49,7 +49,7 @@
*.eot binary
# Export ignore (not included in archives)
.mokogitea/ export-ignore
.mokogit/ export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
+3 -3
View File
@@ -57,10 +57,10 @@ Joomla **package** with four sub-extensions:
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
- **Attribution**: `Authored-by: Moko Consulting`
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Workflow directory**: `.mokogit/` (not `.gitea/` or `.github/`)
- **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the MokoGitea wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/wiki)
- **Wiki**: documentation lives in the MokoGIT wiki, not `docs/` files
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/.mokogit/wiki)
## Coding Standards
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/auto-bump.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogit/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
@@ -22,7 +22,7 @@ on:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
@@ -34,17 +34,16 @@ jobs:
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request') &&
!startsWith(github.event.repository.name, 'Template-')
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.MOKOGIT_TOKEN }}
fetch-depth: 1
- name: Setup mokocli tools
- name: Setup MokoCLI tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
@@ -53,7 +52,7 @@ jobs:
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/MokoCLI.git" \
"https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
/tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
@@ -63,5 +62,5 @@ jobs:
run: |
php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
--token "${{ secrets.MOKOGIT_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
@@ -3,18 +3,18 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/auto-release.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.01.00
# BRIEF: Universal build & release detects platform from manifest.xml
# BRIEF: Universal build & release detects platform from MokoGIT repo metadata (API)
#
# +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +=======================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | Reads MokoGIT repo metadata (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages |
@@ -31,7 +31,7 @@ on:
branches:
- main
paths-ignore:
- '.mokogitea/workflows/**'
- '.mokogit/workflows/**'
- '*.md'
- 'wiki/**'
- '.editorconfig'
@@ -52,9 +52,9 @@ on:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
@@ -77,13 +77,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.MOKOGIT_TOKEN }}
fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools
- name: Setup MokoCLI tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
@@ -95,7 +95,7 @@ jobs:
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet
@@ -104,58 +104,30 @@ jobs:
- name: Rename branch to rc
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}"
FROM="${{ github.event.pull_request.head.ref || 'dev' }}"
PR="${{ github.event.pull_request.number }}"
# Resolve the source branch HEAD commit.
SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \
|| { echo "::error::Source branch ${FROM} not found"; exit 1; }
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
# Point rc at the source commit. If rc already exists (a protected branch that
# cannot be deleted), force-update its ref in place instead of delete+recreate:
# deleting a protected branch fails, which then makes the recreate return HTTP 409.
if curl -sf -o /dev/null -H "$AUTH" "${API_BASE}/branches/rc"; then
echo "rc exists - force-updating to ${FROM} (${SRC_SHA})"
curl -sf -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
"${API_BASE}/git/refs/heads/rc" -d "{\"sha\":\"${SRC_SHA}\",\"force\":true}" \
|| { echo "::error::Failed to force-update rc (CI token needs force-push on the protected rc branch)"; exit 1; }
else
echo "Creating rc from ${FROM}"
curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \
"${API_BASE}/branches" -d "{\"new_branch_name\":\"rc\",\"old_branch_name\":\"${FROM}\"}" \
|| { echo "::error::Failed to create rc from ${FROM}"; exit 1; }
fi
# Repoint the PR at rc, then delete the old source branch (non-fatal).
if [ -n "$PR" ]; then
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
"${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true
fi
curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true
echo "Renamed ${FROM} -> rc"
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGIT_TOKEN }}" \
--api-base "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogit-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
--token "${{ secrets.MOKOGIT_TOKEN }}"
- name: Update RC release notes from CHANGELOG.md
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
# Extract [Unreleased] section from changelog
NOTES=""
@@ -208,15 +180,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.MOKOGIT_TOKEN }}
fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes
run: |
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogit-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
@@ -231,9 +203,9 @@ jobs:
fi
echo "No conflict markers found"
- name: Setup mokocli tools
- name: Setup MokoCLI tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
@@ -246,7 +218,7 @@ jobs:
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet
@@ -280,7 +252,7 @@ jobs:
fi
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable ${BUMP_FLAG} --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
--token "${{ secrets.MOKOGIT_TOKEN }}"
- name: "Read published version"
id: version
@@ -307,8 +279,8 @@ jobs:
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
SEMVER_TAG="v${VERSION}"
echo "Creating semver tag: ${SEMVER_TAG}"
@@ -332,8 +304,8 @@ jobs:
- name: Update release notes and promote changelog
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
# Get the stable release info (version and ID)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
@@ -382,7 +354,7 @@ jobs:
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content if ('## [' + version + ']') in content else content.replace(old, new, 1)
content = content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
@@ -401,10 +373,10 @@ jobs:
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
@@ -430,8 +402,8 @@ jobs:
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
@@ -446,8 +418,8 @@ jobs:
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
@@ -467,9 +439,9 @@ jobs:
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
@@ -493,5 +465,5 @@ jobs:
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| 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
echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Universal
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# PATH: /.mokogit/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
@@ -30,14 +30,25 @@ jobs:
steps:
- name: Delete source branch
# SECURITY: the PR head ref is attacker-controllable. Pass it (and the
# repo/token) through env so the Actions engine cannot splice it into the
# shell source, and validate it to a safe charset before use.
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
REPO: ${{ github.repository }}
API_BASE: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
set -euo pipefail
if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
echo "::error::unsafe branch name; refusing to proceed"; exit 1
fi
API="${API_BASE}/api/v1/repos/${REPO}/branches"
# URL-encode the branch name's slashes (no PHP dependency on the runner)
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
-H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
+65
View File
@@ -0,0 +1,65 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Release
# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11.
#
# +========================================================================+
# | CASCADE MAIN -> DEV |
# +========================================================================+
# | dev mirrors main and is reset on every release. |
# | delete+recreate cannot run against a protected branch, so this |
# | force-pushes main -> dev. The automation identity is force-push |
# | allowlisted on dev via branch-protection.yml. |
# | Runs AFTER the release workflow completes, so dev picks up the |
# | fully version-bumped + changelog-promoted main (not the pre-bump |
# | state). Force-push (not merge) => no version-file conflicts. |
# +========================================================================+
name: "Cascade Main -> Dev"
on:
workflow_run:
workflows: ["Universal: Build & Release"]
types: [completed]
workflow_dispatch:
concurrency:
group: cascade-dev-${{ github.repository }}
cancel-in-progress: true
permissions:
contents: write
jobs:
cascade:
name: Reset dev to main
if: >-
!startsWith(github.event.repository.name, 'Template-') &&
(github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
steps:
- name: Force dev to main
env:
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
git init -q sync && cd sync
git config user.email "mokogit-actions[bot]@mokoconsulting.tech"
git config user.name "mokogit-actions[bot]"
git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git"
git fetch -q --depth=1 origin main
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
# dev exists -> force it to match main (dev is a mirror of main)
git push --force origin FETCH_HEAD:refs/heads/dev
echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY"
else
# dev missing (e.g. renamed to rc mid-cycle) -> recreate from main
git push origin FETCH_HEAD:refs/heads/dev
echo "dev created from main" >> "$GITHUB_STEP_SUMMARY"
fi
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.CI
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/ci-generic.yml
# PATH: /.mokogit/workflows/ci-generic.yml
# VERSION: 01.00.00
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
@@ -131,11 +131,10 @@ jobs:
test:
name: Tests
runs-on: ubuntu-latest
# Independent job (no `needs: lint`): the Gitea Actions scheduler does not
# offer the dependent 2nd job of a needs-chain to runners, so it stalls in
# "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
# directly (same condition lint uses) instead of gating on lint's result.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
needs: lint
# Run only when lint succeeded; always() forces evaluation so a skipped
# lint (e.g. template repos) skips this job cleanly instead of hanging.
if: ${{ always() && needs.lint.result == 'success' }}
steps:
- name: Checkout
@@ -3,12 +3,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogit/workflows/ci-issue-reporter.yml
# VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
# BRIEF: Reusable workflow — creates/updates a MokoGIT issue when a CI gate fails.
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
name: "Universal: CI Issue Reporter"
@@ -35,7 +35,7 @@ on:
type: string
default: ""
secrets:
MOKOGITEA_TOKEN:
MOKOGIT_TOKEN:
required: true
env:
@@ -49,16 +49,16 @@ jobs:
steps:
- name: Clone MokoCLI
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_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
MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_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' }}
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
MOKOGIT_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 \
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.mokogitea/workflows/cleanup.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogit/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
@@ -21,7 +21,7 @@ permissions:
contents: write
env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.MOKOGIT_TOKEN }}
- name: Delete merged branches
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
API="${MOKOGIT_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)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
RUNS=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /.mokogitea/workflows/gitleaks.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
#
@@ -34,7 +34,7 @@ permissions:
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }}
jobs:
gitleaks:
@@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 02.63.00
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Automation
# VERSION: 02.67.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
@@ -19,7 +19,7 @@ permissions:
issues: write
env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
create-branch:
@@ -27,14 +27,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Create branch and comment
# SECURITY: never interpolate github.event.* into a run: script — the
# Actions engine splices the raw value into the shell source (command
# injection via a crafted issue title). Pass everything through env so
# the values arrive as ordinary shell variables that are not re-parsed.
env:
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
REPO: ${{ github.repository }}
ISSUE_NUM: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
TOKEN="${MOKOGIT_TOKEN}"
API="${MOKOGIT_URL}/api/v1/repos/${REPO}"
# Build slug from title: lowercase, replace non-alnum with dash, trim
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
# Build slug from title: lowercase, replace non-alnum with dash, trim.
# printf (not echo) so a title beginning with "-" is not read as flags.
SLUG=$(printf '%s' "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
# Check dev branch exists
@@ -58,7 +66,7 @@ jobs:
echo "Created branch: ${BRANCH}"
# Comment on issue with branch link
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
REPO_URL="${MOKOGIT_URL}/${REPO}"
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
curl -sf -X POST \
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.mokogitea/workflows/notify.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogit/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
@@ -26,7 +26,7 @@ permissions:
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }}
jobs:
notify:
@@ -46,13 +46,13 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
curl -sS \
-H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \
-H "Priority: default" \
-H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
"${NTFY_URL}/${NTFY_TOPIC}"
- name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure'
@@ -61,10 +61,10 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}"
curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
curl -sS \
-H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \
-H "Priority: high" \
-H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
"${NTFY_URL}/${NTFY_TOPIC}"
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/pr-check.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge
@@ -47,15 +47,15 @@ jobs:
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
@@ -86,8 +86,7 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
@@ -97,80 +96,6 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
require-docs:
name: Require Docs Update
runs-on: ubuntu-latest
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
if: ${{ github.base_ref == 'main' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require README.md and CHANGELOG.md in the PR diff
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
if [ -z "$CHANGED" ]; then
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
fi
echo "Changed files in PR:"
echo "$CHANGED"
MISSING=""
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
if [ -n "$MISSING" ]; then
echo "::error::PRs into main must update: ${MISSING}"
{
echo "## Docs Update Required"
echo ""
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
echo ""
echo "Not updated in this PR: **${MISSING}**"
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "Docs update present (README.md + CHANGELOG.md)"
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
wiki-reminder:
name: Wiki Update Reminder
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' }}
steps:
- name: Remind to update the wiki
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -uo pipefail
{
echo "## Wiki Update Reminder"
echo ""
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
echo ""
echo "- ${SERVER}/${REPO}/wiki"
echo ""
echo "_Non-blocking reminder._"
} >> "$GITHUB_STEP_SUMMARY"
# Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
if [ "${existing:-0}" -eq 0 ]; then
curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
-d '{"body":"<!-- wiki-reminder -->\n\n**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_"}' >/dev/null 2>&1 || true
fi
fi
echo "Wiki reminder emitted (non-blocking)."
# ── Secret Scanning ──────────────────────────────────────────────────
gitleaks:
name: Secret Scan
@@ -210,7 +135,7 @@ jobs:
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
@@ -224,7 +149,7 @@ jobs:
- name: Detect platform
id: platform
run: |
# Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used.
# Platform comes from the MokoGIT metadata API (public GET); manifest.xml is no longer used.
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
[ -z "$PLATFORM" ] && PLATFORM="generic"
@@ -258,9 +183,8 @@ jobs:
while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Scan the whole file for the JEXEC/JPATH guard: it is placed after
# the SPDX/file-header docblock, which commonly runs past 20 lines.
if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
# Check first 10 lines for JEXEC or JPATH guard
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
@@ -351,7 +275,7 @@ jobs:
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (MokoSuite site)"
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
@@ -361,10 +285,10 @@ jobs:
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
# Block legacy raw/branch update server URLs on MokoGitea
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
# Block legacy raw/branch update server URLs on MokoGIT
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true)
if [ -n "$RAW_URLS" ]; then
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the MokoGitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGIT. Use the MokoGIT Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS"
exit 1
fi
@@ -578,12 +502,12 @@ jobs:
steps:
- name: Trigger RC pre-release
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
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\"}}"
curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
@@ -594,7 +518,7 @@ jobs:
if: >-
always() &&
needs.validate.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
uses: ./.mokogit/workflows/ci-issue-reporter.yml
with:
gate: "PR Validation"
workflow: "PR Check"
@@ -3,12 +3,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
# VERSION: 01.00.00
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
# BRIEF: Validate MokoGIT metadata matches Joomla extension manifest on PRs
name: "Joomla: Metadata Validation"
@@ -20,22 +20,24 @@ permissions:
contents: read
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
validate-metadata:
name: "Validate Joomla Metadata"
runs-on: ubuntu-latest
# Skip on template repos (Template-*) — they have no real extension manifest to validate.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup mokocli tools
- name: Setup MokoCLI tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
@@ -55,14 +57,14 @@ jobs:
- name: Validate metadata against Joomla manifest
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \
--token "${GITEA_TOKEN}" \
--org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \
--api-base "${GITEA_URL}/api/v1" \
--token "${MOKOGIT_TOKEN}" \
--org "${GIT_ORG}" \
--repo "${GIT_REPO}" \
--api-base "${MOKOGIT_URL}/api/v1" \
--ci
if [ $? -ne 0 ]; then
@@ -3,11 +3,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/pre-release.yml
# VERSION: 05.02.01
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.02.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release"
@@ -40,9 +40,9 @@ permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
@@ -61,7 +61,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.MOKOGIT_TOKEN }}
ref: ${{ github.ref_name }}
submodules: recursive
@@ -69,9 +69,9 @@ jobs:
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:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
@@ -84,7 +84,7 @@ jobs:
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
fi
rm -rf /tmp/mokocli
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoCLI.git
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
@@ -156,30 +156,24 @@ jobs:
fi
# Commit version bump
git config --local user.email "mokogitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
git config --local user.name "mokogit-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
# Push the bump commit, but do NOT fail the release if the target branch
# is protected and the release identity is not on the push allowlist.
# The build proceeds from the in-tree bumped version regardless; if the
# push is rejected, the next run simply re-bumps from the same base.
if ! git push origin HEAD 2>&1; then
echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway."
fi
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
--repo "${GIT_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
@@ -197,18 +191,18 @@ jobs:
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
--repo "${GIT_REPO}" --branch "${{ github.ref_name }}" --prerelease
- name: Update release notes from CHANGELOG.md
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
@@ -219,7 +213,7 @@ jobs:
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
@@ -231,7 +225,7 @@ jobs:
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Authorization': 'token ${{ secrets.MOKOGIT_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
@@ -245,21 +239,21 @@ jobs:
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
--repo "${GIT_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# updates.xml is generated dynamically by MokoGIT license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
@@ -280,4 +274,4 @@ jobs:
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/rc-revert.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogit/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
@@ -25,16 +25,15 @@ jobs:
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/') &&
!startsWith(github.event.repository.name, 'Template-')
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- 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 }}
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
run: |
set -euo pipefail
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
@@ -43,7 +42,7 @@ jobs:
fi
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${GITEA_URL}/api/v1/repos/${REPO}/branches"
API="${GIT_URL}/api/v1/repos/${REPO}/branches"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
@@ -6,10 +6,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/repo-health.yml
# DEFGROUP: MokoGIT.Workflow
# INGROUP: MokoCLI.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
@@ -45,7 +45,7 @@ env:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogit/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
@@ -56,7 +56,7 @@ env:
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .mokogitea/workflows
WORKFLOWS_DIR: .mokogit/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -77,7 +77,7 @@ jobs:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
@@ -88,14 +88,14 @@ jobs:
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller|mokogitea-actions[bot])
jmiller|mokogit-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
;;
*)
# Detect platform and check permissions via API
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
API_BASE="${GITHUB_API_URL:-${GIT_API_URL:-https://api.github.com}}"
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
@@ -605,7 +605,7 @@ jobs:
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGIT |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
@@ -677,7 +677,7 @@ jobs:
if: >-
always() &&
needs.scripts_governance.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
uses: ./.mokogit/workflows/ci-issue-reporter.yml
with:
gate: "Scripts Governance"
workflow: "Repo Health"
@@ -691,7 +691,7 @@ jobs:
if: >-
always() &&
needs.repo_health.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
uses: ./.mokogit/workflows/ci-issue-reporter.yml
with:
gate: "Repository Health"
workflow: "Repo Health"
File diff suppressed because it is too large Load Diff
@@ -3,10 +3,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow.Template
# INGROUP: MokoStandards.CI
# DEFGROUP: MokoGIT.Workflow.Template
# INGROUP: MokoCLI.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
# PATH: /.mokogit/workflows/version-set.yml
# VERSION: 01.00.00
# BRIEF: Set or reset the extension version across all version-bearing files
@@ -34,7 +34,6 @@ jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Validate version format
@@ -49,7 +48,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
token: ${{ secrets.MOKOGIT_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
-106
View File
@@ -1,106 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoStandards.Cascade
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/cascade-dev.yml
# VERSION: 02.00.00
# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify
name: "Cascade Main -> Dev"
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
pull-requests: write
env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
# ntfy destination is configured via repo or org variables (org vars are inherited).
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }}
jobs:
cascade:
name: Cascade main -> dev
runs-on: ubuntu-latest
steps:
- name: Open main -> dev PR (auto-merge if clean, else notify)
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -uo pipefail
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
AUTH="Authorization: token ${TOKEN}"
jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
# 0. dev must exist
if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
echo "No dev branch - nothing to cascade."; exit 0
fi
# 1. is main ahead of dev?
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
if [ "${AHEAD:-0}" -eq 0 ]; then
echo "dev already up to date with main."; exit 0
fi
echo "main is ${AHEAD} commit(s) ahead of dev."
# 2. reuse an open main->dev PR, else create one
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "")
if [ -z "$PR" ]; then
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
-d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}')
PR=$(printf '%s' "$RESP" | jqnum number)
if [ -z "$PR" ]; then
echo "::warning::Could not open cascade PR: $RESP"; exit 0
fi
echo "Opened cascade PR #${PR}"
else
echo "Reusing open cascade PR #${PR}"
fi
# 3. wait for MokoGitea to compute mergeability (conflict detection)
MERGEABLE=""
for _ in 1 2 3 4 5 6; do
MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable)
case "$MERGEABLE" in True|False) break ;; esac
sleep 3
done
echo "mergeable=${MERGEABLE}"
notify() {
curl -sS \
-H "Title: ${REPO}: dev cascade needs manual merge" \
-H "Tags: warning,twisted_rightwards_arrows" \
-H "Priority: high" \
-H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \
-d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
}
# 4. auto-merge only if conflict-free; otherwise notify
if [ "$MERGEABLE" = "True" ]; then
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
else
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
notify "could not be auto-merged (HTTP ${CODE})."
fi
else
echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification."
notify "has conflicts and cannot be merged automatically."
fi
-903
View File
@@ -1,903 +0,0 @@
# 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
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
name: "Joomla: Extension CI"
on:
pull_request:
branches:
- main
- 'dev/**'
workflow_dispatch:
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint-and-validate:
name: Lint & Validate
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
php -v && composer --version
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then
echo "mokocli already available on runner — skipping clone"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
fi
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: PHP syntax check
run: |
ERRORS=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
FOUND=1
while IFS= read -r -d '' FILE; do
OUTPUT=$(php -l "$FILE" 2>&1)
if echo "$OUTPUT" | grep -q "Parse error"; then
echo "::error file=${FILE}::${OUTPUT}"
ERRORS=$((ERRORS + 1))
fi
done < <(find "$DIR" -name "*.php" -print0)
fi
done
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
fi
- name: XML manifest validation
run: |
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find the extension manifest (XML with <extension tag)
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -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 "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Validate well-formed XML
php -r "
\$xml = @simplexml_load_file('$MANIFEST');
if (\$xml === false) {
echo 'INVALID';
exit(1);
}
echo 'VALID';
" > /tmp/xml_result 2>&1
XML_RESULT=$(cat /tmp/xml_result)
if [ "$XML_RESULT" != "VALID" ]; then
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
fi
# Check required tags: name, version, author
for TAG in name version author; do
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
fi
done
# Namespace is required for components/plugins but not packages
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ "$EXT_TYPE" != "package" ]; then
if ! grep -q "<namespace" "$MANIFEST" 2>/dev/null; then
echo "Missing required tag: \`<namespace>\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Found required tag: \`<namespace>\`" >> $GITHUB_STEP_SUMMARY
fi
else
echo "Package extension — \`<namespace>\` not required." >> $GITHUB_STEP_SUMMARY
fi
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check language files referenced in manifest
run: |
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -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 [ -n "$MANIFEST" ]; then
# Extract language file references from manifest
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
if [ -z "$LANG_FILES" ]; then
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
else
while IFS= read -r LANG_FILE; do
LANG_FILE=$(echo "$LANG_FILE" | xargs)
if [ -z "$LANG_FILE" ]; then
continue
fi
# Check in common locations
FOUND=0
for BASE in "." "src" "htdocs"; do
if [ -f "${BASE}/${LANG_FILE}" ]; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
fi
done <<< "$LANG_FILES"
fi
else
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
fi
if [ "${ERRORS}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Check index.html files in directories
run: |
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
MISSING=0
CHECKED=0
for DIR in src/ htdocs/; do
if [ -d "$DIR" ]; then
while IFS= read -r -d '' SUBDIR; do
CHECKED=$((CHECKED + 1))
if [ ! -f "${SUBDIR}/index.html" ]; then
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$DIR" -type d -print0)
fi
done
if [ "${CHECKED}" -eq 0 ]; then
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
elif [ "${MISSING}" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
fi
- name: Check config.xml and access.xml for components
run: |
echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find all component manifests (XML with type="component")
COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<extension[^>]*type="component"' {} ; 2>/dev/null || true)
if [ -z "$COMP_MANIFESTS" ]; then
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $COMP_MANIFESTS; do
COMP_DIR=$(dirname "$MANIFEST")
COMP_NAME=$(basename "$COMP_DIR")
echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY
# Check access.xml exists
ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ACCESS_FILE" ]; then
echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
for ACTION in core.admin core.manage; do
if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then
echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
# Check config.xml exists
CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$CONFIG_FILE" ]; then
echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then
echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SQL schema validation
run: |
echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find SQL files in source/htdocs
SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$SQL_FILES" ]; then
echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $SQL_FILES; do
# Basic syntax check: balanced parentheses, no empty files
SIZE=$(wc -c < "$FILE" | tr -d ' ')
if [ "$SIZE" -eq 0 ]; then
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
# Check for common SQL errors
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
continue
fi
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
done
# Check update SQL files follow version numbering pattern
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$UPDATE_DIR" ]; then
BAD_NAMES=0
for UFILE in "$UPDATE_DIR"/*.sql; do
[ ! -f "$UFILE" ] && continue
BASENAME=$(basename "$UFILE" .sql)
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
BAD_NAMES=$((BAD_NAMES + 1))
fi
done
if [ "$BAD_NAMES" -gt 0 ]; then
ERRORS=$((ERRORS + BAD_NAMES))
fi
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Manifest file references check
run: |
echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY
ERRORS=0
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -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 "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
else
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check <filename> references
FILENAMES=$(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILENAMES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <folder> references
FOLDERS=$(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FOLDERS; do
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
# Check <file> references in package manifests (ZIP files won't exist in source)
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ "$EXT_TYPE" != "package" ]; then
FILES=$(grep -oP '<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
for F in $FILES; do
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Form XML validation
run: |
echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY
ERRORS=0
FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$FORM_FILES" ]; then
echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $FORM_FILES; do
if command -v php &> /dev/null; then
if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then
echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
# Check for valid Joomla form structure
if ! grep -qE '<form|<field|<fieldset' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: no \`<form>\`, \`<field>\`, or \`<fieldset>\` elements found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: Deprecated Joomla API check
continue-on-error: true
run: |
echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
# Joomla 3/4 deprecated patterns that break in Joomla 6
PATTERNS=(
'JFactory::'
'JText::'
'JHtml::'
'JRoute::'
'JUri::'
'JLog::'
'JTable::'
'JInput'
'CMSFactory::\$application'
'JApplicationCms'
)
for PATTERN in "${PATTERNS[@]}"; do
HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true)
if [ -n "$HITS" ]; then
COUNT=$(echo "$HITS" | wc -l)
echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + COUNT))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY
else
echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Template output escaping check
continue-on-error: true
run: |
echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY
WARNINGS=0
TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$TMPL_FILES" ]; then
echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY
else
echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY
for FILE in $TMPL_FILES; do
# Check for unescaped output: <?= $var ?> or echo $var without escape()
UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$UNESCAPED" ]; then
HITS=$(echo "$UNESCAPED" | wc -l)
echo "- \`${FILE}\`: ${HITS} unescaped \`<?= \$var ?>\` output(s) — use \`<?= \$this->escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
# Check for echo without escaping in template context
RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true)
if [ -n "$RAW_ECHO" ]; then
HITS=$(echo "$RAW_ECHO" | wc -l)
echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY
WARNINGS=$((WARNINGS + HITS))
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$WARNINGS" -gt 0 ]; then
echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY
else
echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Namespace consistency check
run: |
echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Find component/plugin manifests with <namespace> tags
MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<namespace' {} \; 2>/dev/null || true)
if [ -z "$MANIFESTS" ]; then
echo "No manifests with \`<namespace>\` found — skipping." >> $GITHUB_STEP_SUMMARY
else
for MANIFEST in $MANIFESTS; do
NS_PATH=$(grep -oP '<namespace[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
[ -z "$NS_PATH" ] && continue
MANIFEST_DIR=$(dirname "$MANIFEST")
echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
# Check PHP files have matching namespace
while IFS= read -r -d '' PHP_FILE; do
FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1)
[ -z "$FILE_NS" ] && continue
# Namespace should start with the manifest namespace path
if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then
echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null)
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY
fi
- name: SPDX license header check
continue-on-error: true
run: |
echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY
MISSING=0
SRC_DIR=""
for DIR in source/ src/ htdocs/; do
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
else
TOTAL=0
while IFS= read -r -d '' FILE; do
TOTAL=$((TOTAL + 1))
if ! head -10 "$FILE" | grep -qi "SPDX"; then
echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
MISSING=$((MISSING + 1))
fi
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0)
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$MISSING" -gt 0 ]; then
echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY
else
echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Service provider check
run: |
echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY
ERRORS=0
PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
if [ -z "$PROVIDERS" ]; then
echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY
else
for FILE in $PROVIDERS; do
# Must return a ServiceProviderInterface
if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY
fi
# Must have return statement
if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then
echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${ERRORS}" -gt 0 ]; then
echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
fi
release-readiness:
name: Release Readiness Check
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.base_ref == 'main'
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate release readiness
run: |
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
ERRORS=0
# Extract version from README.md
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
if [ -z "$README_VERSION" ]; then
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Find the extension manifest
MANIFEST=""
for XML_FILE in $(find . -maxdepth 2 -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 "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# Check <version> matches README VERSION
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
if [ -z "$MANIFEST_VERSION" ]; then
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check extension type, element, client attributes
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
if [ -z "$EXT_TYPE" ]; then
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
else
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
fi
# Element check (component/module/plugin name)
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_ELEMENT" -eq 0 ]; then
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Client attribute for site/admin modules and plugins
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
if [ "$HAS_CLIENT" -eq 0 ]; then
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
fi
fi
# Check updates.xml exists
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
else
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
# Check CHANGELOG.md exists
if [ -f "CHANGELOG.md" ]; then
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
else
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS + 1))
fi
echo "" >> $GITHUB_STEP_SUMMARY
if [ $ERRORS -gt 0 ]; then
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
fi
test:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
needs: lint-and-validate
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php }}
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
--no-interaction \
--prefer-dist \
--optimize-autoloader
else
echo "No composer.json found — skipping dependency install"
fi
- name: Run tests
run: |
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
else
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
else
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
fi
static-analysis:
name: PHPStan Analysis
runs-on: ubuntu-latest
needs: lint-and-validate
continue-on-error: true
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
php -v && composer --version
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
fi
- name: Install PHPStan
run: |
if ! command -v vendor/bin/phpstan &> /dev/null; then
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
composer global require phpstan/phpstan --no-interaction
fi
- name: Run PHPStan
run: |
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
PHPSTAN="vendor/bin/phpstan"
if [ ! -f "$PHPSTAN" ]; then
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
fi
# Determine source directory
SRC_DIR=""
for DIR in src/ htdocs/ lib/; do
if [ -d "$DIR" ]; then
SRC_DIR="$DIR"
break
fi
done
if [ -z "$SRC_DIR" ]; then
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
exit 0
fi
# Use repo phpstan.neon if present, otherwise use baseline config
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
else
ARGS="$ARGS --level=3"
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
fi
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
EXIT=${PIPESTATUS[0]}
if [ $EXIT -eq 0 ]; then
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
else
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
pre-release:
name: Build RC Pre-Release
runs-on: ubuntu-latest
needs: [lint-and-validate, test]
if: github.event_name == 'pull_request'
steps:
- name: Trigger pre-release build
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
run: |
curl -s -X POST \
"${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
-32
View File
@@ -1,32 +0,0 @@
name: Sync Workflows to Repos
on:
push:
branches:
- main
paths:
- '.mokogitea/workflows/**'
jobs:
sync:
runs-on: ubuntu-latest
if: ${{ startsWith(github.event.repository.name, 'Template-') }}
steps:
- name: Checkout mokocli
uses: actions/checkout@v4
with:
repository: MokoConsulting/MokoCLI
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Setup PHP
uses: https://git.mokoconsulting.tech/MokoConsulting/.mokogitea/raw/branch/main/actions/setup-php@v1
with:
php-version: '8.1'
- name: Install dependencies
run: composer install --no-dev --no-interaction
- name: Sync workflows to generic repos
run: php automation/bulk_sync.php --platform generic --org MokoConsulting --workflows-only --auto-merge --token "${{ secrets.MOKOGITEA_TOKEN }}"
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
@@ -1,82 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
# VERSION: 01.01.00
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
name: "Universal: Workflow Sync Trigger"
on:
workflow_dispatch:
pull_request:
types: [closed]
branches:
- main
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
startsWith(github.event.repository.name, 'Template-') &&
(github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')))
steps:
- name: Determine platform from repo name
id: platform
run: |
REPO="${{ github.event.repository.name }}"
case "$REPO" in
Template-Joomla) PLATFORM="joomla" ;;
Template-Dolibarr) PLATFORM="dolibarr" ;;
Template-Go) PLATFORM="go" ;;
Template-MCP) PLATFORM="mcp" ;;
Template-Generic) PLATFORM="" ;;
*) PLATFORM="" ;;
esac
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform: ${PLATFORM:-all}"
- name: Clone mokocli
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
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
run: |
cd /tmp/mokocli
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
- name: Run workflow sync
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
ARGS="--token ${MOKOGITEA_TOKEN}"
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
ARGS="${ARGS} --phase repos"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ -n "$PLATFORM" ]; then
ARGS="${ARGS} --platform-filter ${PLATFORM}"
fi
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
+24 -29
View File
@@ -1,39 +1,34 @@
# Changelog
## [Unreleased]
## [02.63.00] --- 2026-07-13
## [02.67.00] --- 2026-07-17
## [02.62.00] --- 2026-07-13
## [02.61.00] --- 2026-07-13
## [02.60.00] --- 2026-07-12
## [02.59.00] --- 2026-07-12
## [02.67.00] --- 2026-07-17
### Fixed
- Pre-update backup now fires for **every** extension update/uninstall, not just the first one in a 10-minute window. A coarse 600-second session throttle (armed after any pre-action backup — even after updating MokoSuiteBackup itself or a core Joomla update) was disabling the client-side interceptor **and** the server-side fallback for *all* extensions for 10 minutes, so distinct updates silently went un-backed-up (looked like "only the backup extension triggers it"). Replaced with **one backup per Update action**: a single batch/click still backs up once (deduped per-request), the client re-fire is skipped via a **one-shot** action-keyed flag (consumed once), and the core-update path uses its own dedicated key so it no longer suppresses extension backups.
- Pre-update backup on the **Joomla Update** page is now **seamless** (Akeeba-style): after the full-screen backup runs, the plugin auto-ticks Joomla's "I have taken a backup" checkbox and clicks Install, so the update continues automatically instead of stopping for a second manual click.
- Pre-update full-screen backup now actually fires on the **Joomla Update** page. The "Install the update" button posts to `option=com_joomlaupdate&layout=update` (Joomla's confirm/updating page) — carrying **only `layout=update`**, no `view`/`task` — so the previous match on `view=update`/`update.install` never triggered. The plugin now intercepts `layout=update` (as well as `view=update` and the legacy `update.install`) and returns the browser there flagged `is_backed_up=1`.
- Pre-update / pre-uninstall skip flag is now **time-bounded**. It records the moment it was armed and is honoured only within a short TTL (5 min), always consumed one-shot. Previously a bare boolean: if a user ran the full-screen pre-update backup then abandoned the flow (closed the tab before the update re-fired), the flag lingered for the whole session and could silently suppress a later, unrelated pre-action backup — i.e. a real update running with **no** backup. Now a stale flag is ignored and the server backs up as normal.
- Pre-update / pre-uninstall backup can no longer run **twice**. The one-shot "skip the synchronous server-side backup" flag is now armed **server-side** the moment the full-screen backup completes (in `AjaxController::step`, keyed by the `msb_action` it is fronting), instead of relying solely on the best-effort client `ajax.preupdateAck` POST (whose failure was silently swallowed in a `catch()`). If that ack ever fails to land, the re-fired extension update/uninstall no longer triggers a second backup behind the "update is running" overlay. The flag is armed **only** on genuine completion (`done`, no error), so a failed or cancelled backup still lets the server back up before the action — the failure mode stays "one extra backup", never "no backup".
- Component **Options** page showed the raw `COM_MOKOSUITEBACKUP_CONFIGURATION` constant instead of a translated title. Joomla's `com_config` derives the Options-page title (and the component name) from the extension *element* (`com_mokosuitebackup`), but the language files defined those keys only under the legacy `COM_MOKOJOOMBACKUP_` stem. Added the element-derived `COM_MOKOSUITEBACKUP` and `COM_MOKOSUITEBACKUP_CONFIGURATION` keys to all four language files (en-GB/en-US, `.ini` and `.sys.ini`), matching the already-migrated `COM_MOKOSUITEBACKUP_ACTION_*` ACL keys. The 400+ explicitly-referenced `COM_MOKOJOOMBACKUP_*` keys are unaffected and left as-is. (#234)
## [02.66.00] --- 2026-07-16
## [02.65.00] --- 2026-07-16
## [02.65.00] --- 2026-07-16
### Added
- **Snapshot transfer + master→slave injection (#237):** download a content snapshot as a portable **`.msbsnap`** file (zip of `manifest.json` + `snapshot.json` + a sha256 integrity check); **import** one on another site via a new Import button on the Snapshots page (materialises a local snapshot record you can then restore); and a **direct injection API**`POST /api/index.php/v1/mokosuitebackup/snapshot/inject` — so a **master** site can push a snapshot straight into a **slave**'s Web Services API. Two conflict modes ship now: **overwrite** (replace matching items by ID — master wins) and **create** (skip existing, add only new), selectable per inject call or defaulted in **Options → Snapshot Transfer**. A receiving site must opt in via the new *Allow Snapshot Injection* setting. A third **duplicate** mode inserts everything as brand-new records with **remapped IDs** (articles, categories, modules — plus their tags, custom-field values and featured flags) using Joomla's Table API so assets/UCM/nested-sets stay valid; each item is inserted defensively (a bad item is skipped and logged, never fatal).
- Full-screen backup now also fronts **extension updates and uninstalls** (Extensions → Update / Manage), not just core Joomla updates. Because `com_installer`'s `update.update` / `manage.remove` are POST actions (CSRF token + a checked `cid[]` list) that a server-side redirect can't cleanly resume, this is done client-side: the toolbar Update/Uninstall click is intercepted, the selection is captured, the full-screen backup runs, and on return the original selection is restored and the real POST form is re-submitted. Gated by `backup_before_update` / `backup_before_uninstall`, super-user only, and deduped to one backup per Update action.
- Manual **"Backup Now"** completion now offers a **View backup record** button (links straight to the record that was just created). It appears only for manual backups — the pre-update / pre-uninstall flow hands control back to Joomla instead.
- **Full-screen backup screen** (`view=runbackup`) for both pre-update and manual backups, modelled on Akeeba's Backup-on-Update — replaces the earlier popup-modal approach. Clicking Joomla's **Install the update** now redirects (server-side, Akeeba-style) to a dedicated full-page backup screen that runs the stepped backup with a real progress bar and then **automatically continues the update** via a validated `returnurl` (flagged `is_backed_up=1` so the backup isn't repeated). Crucially, **no backup runs synchronously inside the update request** — which is what white-screened large sites. The dashboard **Backup Now** now opens the same full-screen screen instead of an inline modal. (#196)
- Retention now prunes **remote** copies too: when a backup is pruned by age/count, its archive is deleted from every enabled remote destination (SFTP / FTP / S3 / Google Drive), not just the local copy. Each uploader gained an idempotent `delete()` method (already-absent file = success), and removal is best-effort — a failing destination is logged but never blocks local pruning. The shared standalone `restore.php` is intentionally left in place (every backup overwrites it, so newer backups still depend on it). (#229)
### Changed
- Consolidated backup plumbing into shared helpers (#230):
- New `RemoteUploaderFactory` replaces the `createUploaderFromParams()` copy that was duplicated in `BackupEngine` and `SteppedBackupEngine`.
- `RetentionManager` is now the single retention authority — it takes the global `max_age_days`/`max_backups` fallback and gained `pruneOrphans()`; the system plugin's hourly cleanup delegates to it and its duplicate `deleteBackupRecord()` logic is removed.
- The backend controller, Web Services API controller, and legacy `cli/mokosuitebackup.php` now run backups through the shared `BackupRunner` (gaining the normalized complete/warning/fail status) instead of instantiating `BackupEngine` directly.
- Backup- and restore-progress modals (backups view) now have the same controls as the full-screen runbackup screen: an "automatically return to the list when finished" checkbox, a Cancel button (double-confirmed) while running, and terminal action buttons. Backup: View backup record / Back to backups on success, Retry / Back on failure. Restore matches, with a stronger cancel warning (a restore cannot be rolled back mid-flight) and no record link.
### Fixed
- Package installer is now **honest about success**: the postflight no longer prints "installed successfully" / next-steps when the install actually failed or only partially completed. Joomla logs a failed child extension but still runs the package postflight, so the postflight now (a) verifies every bundled child declared in the manifest actually registered in `#__extensions` (matched by element + type, and folder/group for plugins) and (b) verifies the component's schema tables — derived dynamically from the installed `install.mysql.sql` — actually exist. If anything is missing it enqueues an error listing what's missing and stops before the success message. Fail-open: any manifest/DB/IO glitch is treated as "nothing missing" so a transient error never turns a good install into a false failure. Unconditional housekeeping (e.g. download-key restore) still runs regardless.
- Pre-update full-screen backup screen now actually triggers on **Joomla 6** (and 4/5). The redirect matched only the legacy `update.install` task, which Joomla 4/5/6 don't use — they server-side-redirect to the **updating page `view=update`**, which then extracts the downloaded package from JavaScript. The plugin now intercepts the `view=update` page **load** (the last point before any files change) and returns the browser there flagged `is_backed_up=1` so the extraction proceeds after the backup. (`update.finalise` is intentionally not intercepted — by then the files are already extracted.)
- Pre-update/uninstall backup no longer **white-screens** the update on large sites. The synchronous backup that runs inside the extension update/uninstall request now raises `max_execution_time`/`memory_limit` (and `ignore_user_abort`) like the web-cron path, so it can't exhaust the request's default limits mid-backup. (Core Joomla updates additionally get a full-screen backup screen — see below.)
- Archive names for **CLI/console-triggered backups** no longer come out as `joomla.invalid_…`. The `[HOST]` placeholder took `$_SERVER['HTTP_HOST']` verbatim, but Joomla's console fills that with the reserved sentinel host `joomla.invalid`; the resolver now treats that (like empty/`localhost`) as unusable and falls back to the configured `live_site` host, then the system hostname. (Set the site's *live_site* to get the exact domain in CLI-built names.)
- Standalone restore script generation no longer aborts backups with `str_replace() expects at least 3 arguments, 2 given`. `MokoRestore::generateStandaloneScript()` had a `str_replace()` call (the "Backup Archive" pre-check rewrite) that was missing its `$php` subject argument, so **every** standalone-mode backup fatally errored while "Generating standalone restore.php…" — the archive still finalized and uploaded, but no `restore.php` was ever produced (the true root cause behind #226). (#226)
- Remote upload: the standalone restore script upload is no longer silent — its result is now checked and logged, and a failed restore-script upload marks the backup as `warning` (previously the result was discarded, so a missing restore script on the remote went unreported while the archive still showed success). (#226)
- ntfy default server URL now points to `ntfy.mokoconsulting.tech` (was the public `ntfy.sh`) in the profile form default/hint, `NotificationSender` fallbacks, the `ntfy_server` column default, and the field description. (#166)
- Non-stepped restore (`RestoreEngine`) now surfaces database-import failures instead of always reporting a clean success: it checks `DatabaseImporter::hasErrors()`, logs the failed statements, and returns a `warning` with a "database may be incomplete" message. `DatabaseImporter`'s statement splitter is also now quote-aware (a `;`/newline inside a value no longer splits a statement) with comments only skipped between statements. (#188)
- Restore Cancel no longer orphans server-side state. The stepped-restore modal's Cancel button now calls a new `ajax.restoreCancel` action that deletes the restore staging directory and session/state files, and `SteppedSession::cleanupOldSessions()` gained a backstop that reaps orphaned `mokosuitebackup-restore-*` staging dirs (for restores abandoned by closing the browser). Previously a cancelled/abandoned restore left the extracted staging dir behind indefinitely.
- Stepped restore no longer reports a partial database import as a clean "Restore complete". `SteppedRestoreEngine::stepDatabase()` now counts failed statements and surfaces the total (the completion message warns and the modal keeps the result on-screen instead of auto-returning). The statement splitter is also quote-aware — a `;` or newline inside a string value no longer splits a statement mid-value — and comment lines are only skipped between statements. (Sibling of #188 for the stepped/standalone paths; see #291.)
- Database view "One Problem" on MokoSuiteBackup: reconciled the `#__mokosuitebackup_profiles` schema-checker output with the update-file history. Corrected the stale `TINYINT` definition in `01.01.01.sql` to the final `VARCHAR(20)` type, fixed the `MODIFY COLUMN` parser trip-up in `01.39.00.sql`, and rewrote the retired `sftp_*` `ADD COLUMN` statements in `01.35.00.sql`/`01.36.00.sql` as bare `ADD` so Joomla's checker no longer demands columns that `02.52.25` and the postflight purge deliberately remove. Joomla's schema checker now reports no problems.
### Changed
- Component now shows the "Type - Name" pretty name `Component - MokoSuiteBackup` in the Extensions manager, consistent with the other sub-extensions. Achieved safely with an `<element>com_mokosuitebackup</element>` tag in the manifest (Joomla reads `<element>` before `<name>` for the extension element), so the element stays `com_mokosuitebackup` and no duplicate/orphaned component is created.
- Backup/restore progress modals in the backups view now use the same full-screen black, big-text, animated Atum-coloured progress-bar theme as the `runbackup` screen, so all backup progress surfaces match. New `media/css/backup-progress.css` scopes the black to the modal (the list page is unaffected until a modal opens).
- Standalone MokoRestore wizard (generated `restore.php`) re-branded to the full-black MokoSuite theme — black background, big header, dark panels/forms, and MokoSuite accent colours — so the restore experience matches the rest of the system instead of the old light blue "Joomla-styled" look.
## [02.64.00] --- 2026-07-14
+1 -1
View File
@@ -4,7 +4,7 @@ Thank you for your interest in contributing to MokoSuiteBackup.
## Getting Started
1. Fork the repository on MokoGitea
1. Fork the repository on MokoGIT
2. Create a feature branch from `dev` (`feature/your-feature`)
3. Make your changes following the coding standards below
4. Submit a pull request targeting `dev`
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md
VERSION: 02.63.00
VERSION: 02.67.00
BRIEF: Security vulnerability reporting and handling policy
-->
@@ -162,6 +162,17 @@
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="lock_admin_during_backup"
type="radio"
label="COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN"
description="COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN_DESC"
default="1"
class="btn-group"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="cleanup" label="COM_MOKOJOOMBACKUP_CONFIG_CLEANUP">
@@ -292,8 +292,8 @@
label="COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER"
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER_DESC"
maxlength="512"
default="https://ntfy.sh"
hint="https://ntfy.sh"
default="https://ntfy.mokoconsulting.tech"
hint="https://ntfy.mokoconsulting.tech"
/>
<field
name="ntfy_token"
@@ -7,6 +7,13 @@
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
COM_MOKOJOOMBACKUP_CONFIGURATION="MokoSuiteBackup Options"
; Framework-derived keys: Joomla builds the component name and the Options-page
; title from the element (com_mokosuitebackup), NOT from the legacy
; COM_MOKOJOOMBACKUP_ stem, so they must exist under the new stem too (mirrors
; the COM_MOKOSUITEBACKUP_ACTION_* ACL keys). Without these the Options screen
; shows the raw COM_MOKOSUITEBACKUP_CONFIGURATION constant. See #234.
COM_MOKOSUITEBACKUP="MokoSuiteBackup"
COM_MOKOSUITEBACKUP_CONFIGURATION="MokoSuiteBackup Options"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
; Submenu
@@ -259,7 +266,7 @@ COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC="<strong>Push Notifications (ntfy)</st
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC="ntfy Topic"
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC_DESC="The ntfy topic to publish notifications to. Leave blank to disable push notifications."
COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER="ntfy Server"
COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER_DESC="URL of the ntfy server. Default is the public ntfy.sh service. Use your own server URL for self-hosted instances."
COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER_DESC="URL of the ntfy server. Default is the Moko Consulting ntfy server (ntfy.mokoconsulting.tech). Use your own server URL for self-hosted instances, or https://ntfy.sh for the public service."
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN="Access Token"
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN_DESC="Optional access token for private ntfy topics. Leave blank for public topics."
@@ -335,6 +342,8 @@ COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT="Auto-run on Update click"
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT_DESC="When enabled, clicking the Update button on the Joomla Update or Extensions update page first pops the backup progress modal, runs the pre-update backup, then automatically continues the update. When disabled, a notice with a manual 'Back up now' button is shown instead."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN="Lock admin during backup"
COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN_DESC="While a backup is actively running, show a \"backup in progress\" screen on all other admin pages until it finishes, so nothing changes mid-backup. The backup screen itself and the login page stay accessible. Auto-releases if a backup stops responding."
; Full-screen backup progress view (runbackup)
COM_MOKOJOOMBACKUP_RUNBACKUP_TITLE="Backing Up"
@@ -350,6 +359,13 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record"
COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it."
COM_MOKOJOOMBACKUP_RUNBACKUP_AUTOCONTINUE="Automatically continue the update when the backup finishes"
COM_MOKOJOOMBACKUP_RUNBACKUP_DONT_CLOSE="Please do not close this window or switch to another window until the backup finishes."
COM_MOKOJOOMBACKUP_RUNBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL="Cancel backup"
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM1="Cancel the backup that is in progress?"
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM2="Are you sure? The backup will be stopped and the update will NOT continue."
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCELLING="Cancelling…"
COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED_NO_UPDATE="Backup failed — the update was not started. Retry the backup, or go back without updating."
COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_UPDATE="Continue the update"
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
@@ -7,6 +7,9 @@
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
; Element-derived aliases (see the .ini file for the rationale). #234
COM_MOKOSUITEBACKUP="MokoSuiteBackup"
COM_MOKOSUITEBACKUP_CONFIGURATION="MokoSuiteBackup Options"
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
@@ -7,6 +7,13 @@
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
; Framework-derived keys: Joomla builds the component name and the Options-page
; title from the element (com_mokosuitebackup), NOT from the legacy
; COM_MOKOJOOMBACKUP_ stem, so they must exist under the new stem too (mirrors
; the COM_MOKOSUITEBACKUP_ACTION_* ACL keys). Without these the Options screen
; shows the raw COM_MOKOSUITEBACKUP_CONFIGURATION constant. See #234.
COM_MOKOSUITEBACKUP="MokoSuiteBackup"
COM_MOKOSUITEBACKUP_CONFIGURATION="MokoSuiteBackup Options"
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
@@ -57,6 +64,8 @@ COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT="Auto-run on Update click"
COM_MOKOJOOMBACKUP_CONFIG_PREUPDATE_AUTO_INTERCEPT_DESC="When enabled, clicking the Update button on the Joomla Update or Extensions update page first pops the backup progress modal, runs the pre-update backup, then automatically continues the update. When disabled, a notice with a manual 'Back up now' button is shown instead."
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN="Lock admin during backup"
COM_MOKOJOOMBACKUP_CONFIG_LOCK_ADMIN_DESC="While a backup is actively running, show a \"backup in progress\" screen on all other admin pages until it finishes, so nothing changes mid-backup. The backup screen itself and the login page stay accessible. Auto-releases if a backup stops responding."
; Full-screen backup progress view (runbackup)
COM_MOKOJOOMBACKUP_RUNBACKUP_TITLE="Backing Up"
@@ -72,6 +81,13 @@ COM_MOKOJOOMBACKUP_RUNBACKUP_BACK_TO_DASHBOARD="Back to dashboard"
COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD="View backup record"
COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING="A backup is in progress. Leaving now will cancel it."
COM_MOKOJOOMBACKUP_RUNBACKUP_AUTOCONTINUE="Automatically continue the update when the backup finishes"
COM_MOKOJOOMBACKUP_RUNBACKUP_DONT_CLOSE="Please do not close this window or switch to another window until the backup finishes."
COM_MOKOJOOMBACKUP_RUNBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL="Cancel backup"
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM1="Cancel the backup that is in progress?"
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM2="Are you sure? The backup will be stopped and the update will NOT continue."
COM_MOKOJOOMBACKUP_RUNBACKUP_CANCELLING="Cancelling…"
COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED_NO_UPDATE="Backup failed — the update was not started. Retry the backup, or go back without updating."
COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_UPDATE="Continue the update"
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
@@ -6,6 +6,9 @@
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_SHORT="Backup"
; Element-derived aliases (see the .ini file for the rationale). #234
COM_MOKOSUITEBACKUP="MokoSuiteBackup"
COM_MOKOSUITEBACKUP_CONFIGURATION="MokoSuiteBackup Options"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
@@ -0,0 +1,173 @@
/*
* @package MokoSuiteBackup
* @subpackage com_mokosuitebackup
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* Full-screen backup/restore progress modals (backups view). Gives the stepped
* backup and restore modals the same full-black, big-text, Atum-coloured
* progress-bar treatment as the runbackup screen (media/css/runbackup.css), so
* all backup progress surfaces share one look. The black is scoped to the modal
* (via .msb-progress-modal) rather than html/body, so the list page behind it is
* unaffected until the modal opens.
*/
/* Accent colours pulled from the Atum admin template's own custom properties,
with Bootstrap and then a literal as progressive fallbacks — identical to the
runbackup screen so the two match whatever theme the site runs. */
.msb-progress-modal {
--msb-accent: var(--template-link-color, var(--bs-link-color, #2a69b8));
--msb-accent2: var(--template-special-color, var(--msb-accent));
--msb-success: var(--template-success-color, var(--bs-success, #2f7d32));
--msb-danger: var(--template-danger-color, var(--bs-danger, #c52128));
--msb-track: var(--template-bg-light, var(--bs-gray-200, #edf0f5));
}
/* Full-screen black takeover, mirroring the runbackup page. */
.msb-progress-modal .modal-dialog {
max-width: 100%;
width: 100%;
min-height: 100%;
margin: 0;
}
.msb-progress-modal .modal-content {
min-height: 100vh;
border: 0;
border-radius: 0;
background-color: #000;
color: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.msb-progress-modal .modal-header {
border: 0;
padding: 0 0 1.5rem;
justify-content: center;
text-align: center;
}
/* Big heading, like the runbackup title. */
.msb-progress-modal .modal-title {
color: #fff;
font-size: clamp(1.75rem, 4vw, 2.75rem);
font-weight: 700;
letter-spacing: .01em;
}
.msb-progress-modal .modal-body {
width: 100%;
max-width: 680px;
padding: 0;
text-align: center;
}
.msb-progress-modal .msb-pm-status {
color: #fff;
font-size: 1.25rem;
margin: 1rem 0 .25rem;
}
.msb-progress-modal .msb-pm-phase {
color: rgba(255, 255, 255, .6);
font-size: .95rem;
margin: 0;
}
/* Auto-continue opt-out, matching the runbackup screen's checkbox. */
.msb-progress-modal .msb-pm-autocontinue {
margin-top: 1.5rem;
color: rgba(255, 255, 255, .85);
font-size: 1rem;
}
.msb-progress-modal .msb-pm-autocontinue label {
display: inline-flex;
align-items: center;
gap: .5rem;
cursor: pointer;
}
.msb-progress-modal .msb-pm-autocontinue input {
width: 1.1rem;
height: 1.1rem;
}
/* Terminal-state action buttons and the running-state cancel button. */
.msb-progress-modal .msb-pm-actions,
.msb-progress-modal .msb-pm-cancel {
margin-top: 1.5rem;
display: flex;
gap: .75rem;
justify-content: center;
flex-wrap: wrap;
}
.msb-progress-modal .msb-pm-actions .btn,
.msb-progress-modal .msb-pm-cancel .btn {
min-width: 10rem;
}
.msb-progress-modal .msb-pm-warn {
color: #ffd23f;
font-weight: 600;
font-size: 1rem;
margin-bottom: 1.25rem;
}
/* Progress bar — mirrors runbackup.css .msb-rb-progress/.msb-rb-bar. */
.msb-progress-modal .msb-rb-progress {
height: 2.75rem;
background: var(--msb-track);
border-radius: .35rem;
overflow: hidden;
}
.msb-progress-modal .msb-rb-bar {
position: relative;
height: 100%;
width: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 600;
font-size: 1rem;
white-space: nowrap;
background-color: var(--msb-accent);
background-image: linear-gradient(90deg, var(--msb-accent), var(--msb-accent2));
transition: width .35s ease;
}
.msb-progress-modal .msb-rb-bar.is-animated::after {
content: "";
position: absolute;
inset: 0;
background-image: linear-gradient(45deg, rgba(255, 255, 255, .18) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .18) 50%, rgba(255, 255, 255, .18) 75%, transparent 75%, transparent);
background-size: 1rem 1rem;
animation: msb-pm-stripes 1s linear infinite;
}
.msb-progress-modal .msb-rb-bar.is-success {
background-image: none;
background-color: var(--msb-success);
}
.msb-progress-modal .msb-rb-bar.is-danger {
background-image: none;
background-color: var(--msb-danger);
}
@keyframes msb-pm-stripes {
from {
background-position: 1rem 0;
}
to {
background-position: 0 0;
}
}
@@ -14,6 +14,11 @@
"name": "com_mokosuitebackup.runbackup",
"type": "style",
"uri": "com_mokosuitebackup/css/runbackup.css"
},
{
"name": "com_mokosuitebackup.backup-progress",
"type": "style",
"uri": "com_mokosuitebackup/css/backup-progress.css"
}
]
}
@@ -7,17 +7,18 @@
-->
<extension type="component" method="upgrade">
<!--
IMPORTANT: a component's <name> drives the Joomla-derived extension
element (com_ + sanitized name), so it MUST stay element-safe:
"MokoSuiteBackup" -> com_mokosuitebackup. Do NOT apply the "Type - Name"
display convention here — "Component - MokoSuiteBackup" derives
com_component-mokosuitebackup, which registers a duplicate component and
orphans the real one. Packages are exempt because they set the element
via <packagename>; plugins/modules are exempt because <name> is only a
display label there.
The <element> tag PINS this extension's element to com_mokosuitebackup.
Joomla's InstallerAdapter::getElement() reads <element> BEFORE falling
back to <name> (libraries/src/Installer/InstallerAdapter.php), so the
display <name> can safely use the "Type - Name" convention without the
element being derived as com_component-mokosuitebackup (which would
register a duplicate component and orphan the real one). Without the
<element> tag, "Component - MokoSuiteBackup" would break the install.
NB: the manifest filename must stay mokosuitebackup.xml (short element).
-->
<name>MokoSuiteBackup</name>
<version>02.63.00</version>
<element>com_mokosuitebackup</element>
<name>Component - MokoSuiteBackup</name>
<version>02.67.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
`retention_days` INT(11) NOT NULL DEFAULT 0 COMMENT 'Delete backups older than N days; 0 = unlimited',
`retention_count` INT(11) NOT NULL DEFAULT 0 COMMENT 'Keep newest N backups; 0 = unlimited',
`ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name',
`ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL',
`ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.mokoconsulting.tech' COMMENT 'ntfy server URL',
`ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)',
`published` TINYINT(1) NOT NULL DEFAULT 1,
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
@@ -1 +1,5 @@
ALTER TABLE `#__mokosuitebackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive';
-- Renames include_kickstart -> include_mokorestore. Declared VARCHAR(20) here to
-- match the final column type (widened from TINYINT in 01.39.00). Joomla's schema
-- checker does no cross-file supersession, so an earlier TINYINT definition would
-- be flagged forever against the now-VARCHAR column.
ALTER TABLE `#__mokosuitebackup_profiles` CHANGE `include_kickstart` `include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0' COMMENT 'Include MokoRestore standalone restore script in archive';
@@ -1,10 +1,16 @@
-- MokoSuiteBackup 01.35.00 — SFTP support with key file storage
--
-- These columns are legacy: 01.41.00 migrates them into #__mokosuitebackup_remotes
-- and 02.52.25 + the postflight dropLegacyRemoteColumns() purge them, so they do not
-- exist at rest. Written as bare `ADD` (not `ADD COLUMN`) on purpose: MySQL treats
-- the two identically, but Joomla's schema checker only builds an ADD-COLUMN check
-- for the `ADD COLUMN` form. Bare `ADD` is skipped by the checker, so it no longer
-- reports these retired columns as "missing" while the upgrade still adds them.
ALTER TABLE `#__mokosuitebackup_profiles`
ADD COLUMN `sftp_host` VARCHAR(255) NOT NULL DEFAULT '' AFTER `ftp_ssl`,
ADD COLUMN `sftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 22 AFTER `sftp_host`,
ADD COLUMN `sftp_username` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_port`,
ADD COLUMN `sftp_password` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_username`,
ADD COLUMN `sftp_key_data` MEDIUMTEXT AFTER `sftp_password`,
ADD COLUMN `sftp_passphrase` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_key_data`,
ADD COLUMN `sftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups' AFTER `sftp_passphrase`;
ADD `sftp_host` VARCHAR(255) NOT NULL DEFAULT '' AFTER `ftp_ssl`,
ADD `sftp_port` INT(5) UNSIGNED NOT NULL DEFAULT 22 AFTER `sftp_host`,
ADD `sftp_username` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_port`,
ADD `sftp_password` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_username`,
ADD `sftp_key_data` MEDIUMTEXT AFTER `sftp_password`,
ADD `sftp_passphrase` VARCHAR(255) NOT NULL DEFAULT '' AFTER `sftp_key_data`,
ADD `sftp_path` VARCHAR(512) NOT NULL DEFAULT '/backups' AFTER `sftp_passphrase`;
@@ -1,4 +1,6 @@
-- MokoSuiteBackup 01.36.00 — SFTP auth type column
--
-- Legacy column (see 01.35.00). Bare `ADD` so Joomla's schema checker skips it —
-- the column is purged by 02.52.25 + postflight and must not exist at rest.
ALTER TABLE `#__mokosuitebackup_profiles`
ADD COLUMN `sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key' AFTER `sftp_username`;
ADD `sftp_auth_type` VARCHAR(20) NOT NULL DEFAULT 'key' AFTER `sftp_username`;
@@ -1,5 +1,8 @@
-- MokoSuiteBackup 01.39.00 — Change include_mokorestore from TINYINT to VARCHAR
-- Needed to support 'standalone' value alongside 0/1
-- NB: MySQL syntax is `MODIFY <col>`, not `MODIFY COLUMN <col>`. Joomla's schema
-- checker parses `MODIFY COLUMN x` as column-name="COLUMN", type="x", producing a
-- bogus "wrong type for column 'COLUMN'" problem. Keep it as bare MODIFY.
ALTER TABLE `#__mokosuitebackup_profiles`
MODIFY COLUMN `include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0';
MODIFY `include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0';
@@ -0,0 +1 @@
/* 02.62.04 — no schema changes */
@@ -0,0 +1 @@
/* 02.62.05 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.01 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.02 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.05 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.06 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.07 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.09 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.10 — no schema changes */
@@ -0,0 +1 @@
/* 02.63.11 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.00 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.01 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.02 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.03 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.04 — no schema changes */
@@ -0,0 +1,6 @@
-- MokoSuiteBackup 02.64.05 — Schema-checker reconciliation for #__mokosuitebackup_profiles
-- Ensures include_mokorestore is VARCHAR(20) on any install (idempotent; no-op if
-- already VARCHAR). Paired with retro-fixes to 01.01.01/01.39.00 and the legacy
-- sftp_* columns restored to install.mysql.sql so the Database view reports no problems.
ALTER TABLE `#__mokosuitebackup_profiles`
MODIFY `include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0';
@@ -0,0 +1 @@
/* 02.64.06 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.09 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.10 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.11 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.12 — no schema changes */
@@ -0,0 +1 @@
/* 02.64.13 — no schema changes */
@@ -0,0 +1 @@
/* 02.65.00 — no schema changes */
@@ -0,0 +1 @@
/* 02.65.01 — no schema changes */
@@ -0,0 +1 @@
/* 02.65.02 — no schema changes */
@@ -0,0 +1 @@
/* 02.65.03 — no schema changes */
@@ -0,0 +1 @@
/* 02.65.04 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.00 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.01 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.02 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.03 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.04 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.05 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.06 — no schema changes */
@@ -0,0 +1 @@
/* 02.66.07 — no schema changes */
@@ -0,0 +1 @@
/* 02.67.00 — no schema changes */
@@ -81,6 +81,19 @@ class AjaxController extends BaseController
$engine = new SteppedBackupEngine();
$result = $engine->runStep($sessionId);
/* Server-authoritative pre-action skip. When a backup that was launched as a
pre-update / pre-uninstall front COMPLETES here, arm the one-shot skip flag
on the SERVER — rather than relying solely on the client-side
ajax.preupdateAck POST, which is best-effort and swallows its own failure
in a catch(). This guarantees the imminent re-fired update/uninstall does
not trigger a SECOND synchronous backup even if that ack never lands.
Armed ONLY on genuine completion (done, no error), so a failed or cancelled
backup never arms it — preserving "no successful pre-action backup ⇒ the
server still backs up before the action". */
if (!empty($result['done']) && empty($result['error'])) {
$this->armPreactionSkip($this->input->getCmd('action', ''));
}
$this->sendJson($result);
}
@@ -116,16 +129,35 @@ class AjaxController extends BaseController
action so an update ack never suppresses a later, unrelated uninstall.
For a core Joomla update (no action) neither flag is set — a core
update doesn't fire onExtensionBefore(Update|Uninstall). */
$action = $this->app->getInput()->getCmd('action', '');
$this->armPreactionSkip($this->app->getInput()->getCmd('action', ''));
$this->sendJson(['error' => false, 'message' => 'Pre-action backup acknowledged']);
}
/**
* Arm the one-shot session skip flag for a specific pre-action, so the
* imminent re-fired extension update/uninstall skips its synchronous
* server-side backup exactly once (runPreActionBackup consumes it). Keyed by
* action so an update never suppresses a later, unrelated uninstall. A blank
* or unknown action (core Joomla update, manual "Backup Now") arms nothing.
*
* Called from two places: ajax.step on genuine completion (authoritative) and
* ajax.preupdateAck (best-effort client ack) — either one arming the flag is
* enough, and re-arming it is harmless.
*
* Stores the unix time it was armed (not a bare bool) so the plugin can honour
* the skip only within a short TTL and never let a stale flag from an abandoned
* backup suppress a genuine, unrelated pre-action backup later in the session.
*/
private function armPreactionSkip(string $action): void
{
$session = Factory::getSession();
if ($action === 'update') {
$session->set('mokosuitebackup.skip_preaction_backup_before_update', true);
$session->set('mokosuitebackup.skip_preaction_backup_before_update', time());
} elseif ($action === 'uninstall') {
$session->set('mokosuitebackup.skip_preaction_backup_before_uninstall', true);
$session->set('mokosuitebackup.skip_preaction_backup_before_uninstall', time());
}
$this->sendJson(['error' => false, 'message' => 'Pre-action backup acknowledged']);
}
/**
@@ -509,6 +541,39 @@ class AjaxController extends BaseController
$this->sendJson($result);
}
/**
* Cancel an in-progress stepped restore and clean up its staging directory
* and session/state files (prevents orphaned artefacts on a client cancel).
* POST: task=ajax.restoreCancel&session_id=mb_...
*/
public function restoreCancel(): void
{
if (!Session::checkToken('get') && !Session::checkToken('post')) {
$this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
return;
}
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
$this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
return;
}
$sessionId = $this->input->getString('session_id', '');
if (empty($sessionId)) {
$this->sendJson(['error' => true, 'message' => 'Missing session_id']);
return;
}
$engine = new SteppedRestoreEngine();
$result = $engine->cancel($sessionId);
$this->sendJson($result);
}
/**
* Browse archive contents without extracting.
* POST: task=ajax.browseArchive&id=123
@@ -61,34 +61,37 @@ class DatabaseImporter
while (($line = fgets($handle)) !== false) {
$trimmed = trim($line);
// Skip empty lines
if ($trimmed === '') {
continue;
}
// Skip single-line comments
if (str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) {
continue;
}
// Handle multi-line comments
if (str_starts_with($trimmed, '/*')) {
$inMultiLineComment = true;
}
if ($inMultiLineComment) {
if (str_contains($trimmed, '*/')) {
$inMultiLineComment = false;
// Comment/blank skipping applies only BETWEEN statements — never
// inside an accumulating statement, where a line may be part of a
// multi-line string value that happens to start with --, # or /*.
if ($currentStatement === '') {
if ($trimmed === '') {
continue;
}
continue;
if (str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) {
continue;
}
if (str_starts_with($trimmed, '/*')) {
$inMultiLineComment = true;
}
if ($inMultiLineComment) {
if (str_contains($trimmed, '*/')) {
$inMultiLineComment = false;
}
continue;
}
}
// Accumulate the statement
$currentStatement .= $line;
// Check if statement is complete (ends with semicolon)
if (str_ends_with($trimmed, ';')) {
// A statement ends at a ';' that is NOT inside a quoted string
// literal, so a ';' or newline in a value does not split it early.
if ($this->isCompleteStatement($currentStatement)) {
$statement = trim($currentStatement);
$currentStatement = '';
@@ -136,6 +139,52 @@ class DatabaseImporter
return $statementsExecuted;
}
/**
* Whether an accumulated SQL string is a complete statement: it ends with
* ';' that is NOT inside a quoted string literal. Tracks single/double/
* backtick quoting (with backslash escapes; MySQL '' doubling falls out of
* the toggle naturally) so a ';' or newline inside a value does not
* terminate the statement early.
*/
private function isCompleteStatement(string $sql): bool
{
$len = \strlen($sql);
$inSingle = false;
$inDouble = false;
$inBacktick = false;
for ($i = 0; $i < $len; $i++) {
$ch = $sql[$i];
if ($inSingle) {
if ($ch === '\\') { $i++; continue; }
if ($ch === "'") { $inSingle = false; }
continue;
}
if ($inDouble) {
if ($ch === '\\') { $i++; continue; }
if ($ch === '"') { $inDouble = false; }
continue;
}
if ($inBacktick) {
if ($ch === '`') { $inBacktick = false; }
continue;
}
if ($ch === "'") { $inSingle = true; }
elseif ($ch === '"') { $inDouble = true; }
elseif ($ch === '`') { $inBacktick = true; }
}
if ($inSingle || $inDouble || $inBacktick) {
return false;
}
return str_ends_with(rtrim($sql), ';');
}
/**
* Non-fatal errors from the last import(), if any. A non-empty result
* means the restore completed with problems and should be treated as a
@@ -232,13 +232,13 @@ REPL,
if (backups.length === 0) {
var alert = document.createElement('div');
alert.style.cssText = 'padding:12px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#dc2626;';
alert.style.cssText = 'padding:12px;background:rgba(224,49,49,0.12);border:1px solid rgba(224,49,49,0.4);border-radius:6px;color:#ff8787;';
alert.textContent = 'No ZIP files found in this directory. Upload a backup archive first.';
list.appendChild(alert);
} else if (backups.length === 1) {
hiddenInput.value = backups[0].name;
var found = document.createElement('div');
found.style.cssText = 'padding:12px;background:#dcfce7;border:1px solid #bbf7d0;border-radius:6px;color:#16a34a;';
found.style.cssText = 'padding:12px;background:rgba(47,158,68,0.15);border:1px solid rgba(47,158,68,0.35);border-radius:6px;color:#69db7c;';
var strong = document.createElement('strong');
strong.textContent = backups[0].name;
found.appendChild(document.createTextNode('Found: '));
@@ -247,13 +247,13 @@ REPL,
list.appendChild(found);
} else {
var hint = document.createElement('div');
hint.style.cssText = 'padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;color:#1d4ed8;margin-bottom:8px;font-size:0.9em;';
hint.style.cssText = 'padding:8px 12px;background:rgba(42,105,184,0.12);border:1px solid rgba(42,105,184,0.35);border-radius:6px;color:#a5d8ff;margin-bottom:8px;font-size:0.9em;';
hint.textContent = 'Multiple backup archives found \u2014 select which one to restore:';
list.appendChild(hint);
backups.forEach(function(b, i) {
var label = document.createElement('label');
label.style.cssText = 'display:flex;align-items:center;padding:10px 12px;margin:4px 0;border:1px solid #e2e8f0;border-radius:6px;cursor:pointer;transition:background 0.15s;';
label.onmouseover = function() { this.style.background = '#f8fafc'; };
label.style.cssText = 'display:flex;align-items:center;padding:10px 12px;margin:4px 0;border:1px solid #2a2f37;border-radius:6px;cursor:pointer;transition:background 0.15s;';
label.onmouseover = function() { this.style.background = '#1e2229'; };
label.onmouseout = function() { this.style.background = ''; };
var radio = document.createElement('input');
radio.type = 'radio';
@@ -268,7 +268,7 @@ REPL,
nameStrong.textContent = b.name;
info.appendChild(nameStrong);
var meta = document.createElement('div');
meta.style.cssText = 'font-size:0.85em;color:#64748b;margin-top:2px;';
meta.style.cssText = 'font-size:0.85em;color:#9aa4b2;margin-top:2px;';
meta.textContent = (b.size / 1048576).toFixed(1) + ' MB \u2014 ' + b.date;
info.appendChild(meta);
label.appendChild(info);
@@ -1507,92 +1507,107 @@ PHP_BACKEND;
<title>MokoRestore — Site Installer</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:#f0f4f8;color:#333;line-height:1.6}
.mr-header{background:linear-gradient(135deg,#1a3c5e 0%,#2d6a9f 100%);color:#fff;padding:1.5rem 2rem;text-align:center}
.mr-header h1{font-size:1.6rem;font-weight:700;margin-bottom:0.2rem}
.mr-header p{font-size:0.9rem;opacity:0.85}
/* Full-black MokoSuite theme, matching the runbackup / backup-progress screens. */
:root{
--mr-bg:#000;
--mr-panel:#15181d;
--mr-input:#1e2229;
--mr-border:#2a2f37;
--mr-text:#f1f3f5;
--mr-muted:#9aa4b2;
--mr-accent:#2a69b8;
--mr-accent2:#3b82f6;
--mr-success:#2f9e44;
--mr-danger:#e03131;
--mr-warn:#f0b429;
}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background:var(--mr-bg);color:var(--mr-text);line-height:1.6}
.mr-header{background:#000;color:#fff;padding:2.25rem 2rem;text-align:center;border-bottom:1px solid var(--mr-border)}
.mr-header h1{font-size:2.5rem;font-weight:700;letter-spacing:.01em;margin-bottom:0.25rem}
.mr-header p{font-size:1rem;color:var(--mr-muted)}
.mr-container{max-width:800px;margin:1.5rem auto;padding:0 1rem}
/* Step progress bar */
.mr-steps{display:flex;justify-content:center;margin-bottom:1.5rem;gap:0;flex-wrap:wrap}
.mr-step{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 1rem;font-size:0.8rem;color:#94a3b8;cursor:default;position:relative}
.mr-step .mr-num{width:28px;height:28px;border-radius:50%;border:2px solid #cbd5e1;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.75rem;background:#fff;flex-shrink:0}
.mr-step.active{color:#1e40af;font-weight:600}
.mr-step.active .mr-num{border-color:#2563eb;background:#2563eb;color:#fff}
.mr-step.done{color:#16a34a}
.mr-step.done .mr-num{border-color:#16a34a;background:#16a34a;color:#fff}
.mr-step::after{content:'';width:24px;height:2px;background:#cbd5e1;margin-left:0.5rem}
.mr-step{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 1rem;font-size:0.8rem;color:var(--mr-muted);cursor:default;position:relative}
.mr-step .mr-num{width:28px;height:28px;border-radius:50%;border:2px solid var(--mr-border);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:0.75rem;background:var(--mr-input);color:var(--mr-muted);flex-shrink:0}
.mr-step.active{color:#fff;font-weight:600}
.mr-step.active .mr-num{border-color:var(--mr-accent);background:var(--mr-accent);color:#fff}
.mr-step.done{color:var(--mr-success)}
.mr-step.done .mr-num{border-color:var(--mr-success);background:var(--mr-success);color:#fff}
.mr-step::after{content:'';width:24px;height:2px;background:var(--mr-border);margin-left:0.5rem}
.mr-step:last-child::after{display:none}
.mr-step.done::after{background:#16a34a}
.mr-step.done::after{background:var(--mr-success)}
/* Panels */
.mr-panel{display:none;background:#fff;border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,0.1);padding:2rem;margin-bottom:1rem}
.mr-panel{display:none;background:var(--mr-panel);border:1px solid var(--mr-border);border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,0.5);padding:2rem;margin-bottom:1rem}
.mr-panel.visible{display:block}
.mr-panel h2{font-size:1.2rem;color:#1e293b;margin-bottom:0.25rem}
.mr-panel .mr-desc{color:#64748b;font-size:0.9rem;margin-bottom:1.5rem}
.mr-panel h2{font-size:1.4rem;color:#fff;margin-bottom:0.25rem}
.mr-panel .mr-desc{color:var(--mr-muted);font-size:0.9rem;margin-bottom:1.5rem}
/* Form elements */
.mr-field{margin-bottom:1rem}
.mr-field label{display:block;font-weight:600;font-size:0.85rem;color:#475569;margin-bottom:0.3rem}
.mr-field input,.mr-field select{width:100%;padding:0.6rem 0.75rem;border:1px solid #d1d5db;border-radius:6px;font-size:0.9rem;color:#1e293b;background:#fff;transition:border-color 0.2s}
.mr-field input:focus,.mr-field select:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px rgba(37,99,235,0.1)}
.mr-field .mr-hint{font-size:0.8rem;color:#94a3b8;margin-top:0.2rem}
.mr-field label{display:block;font-weight:600;font-size:0.85rem;color:#cfd6df;margin-bottom:0.3rem}
.mr-field input,.mr-field select{width:100%;padding:0.6rem 0.75rem;border:1px solid var(--mr-border);border-radius:6px;font-size:0.9rem;color:var(--mr-text);background:var(--mr-input);transition:border-color 0.2s}
.mr-field input::placeholder{color:#69707c}
.mr-field input:focus,.mr-field select:focus{outline:none;border-color:var(--mr-accent);box-shadow:0 0 0 3px rgba(42,105,184,0.25)}
.mr-field .mr-hint{font-size:0.8rem;color:var(--mr-muted);margin-top:0.2rem}
.mr-row{display:flex;gap:1rem}
.mr-row .mr-field{flex:1}
/* Buttons */
.mr-btn{display:inline-flex;align-items:center;gap:0.4rem;padding:0.65rem 1.5rem;border:none;border-radius:6px;font-size:0.9rem;font-weight:600;cursor:pointer;transition:all 0.2s}
.mr-btn:disabled{opacity:0.5;cursor:not-allowed}
.mr-btn-primary{background:#2563eb;color:#fff}
.mr-btn-primary:hover:not(:disabled){background:#1d4ed8}
.mr-btn-success{background:#16a34a;color:#fff}
.mr-btn-success:hover:not(:disabled){background:#15803d}
.mr-btn-danger{background:#dc2626;color:#fff}
.mr-btn-danger:hover:not(:disabled){background:#b91c1c}
.mr-btn-outline{background:transparent;color:#2563eb;border:1px solid #2563eb}
.mr-btn-outline:hover:not(:disabled){background:#eff6ff}
.mr-actions{display:flex;justify-content:space-between;align-items:center;margin-top:1.5rem;padding-top:1rem;border-top:1px solid #e2e8f0}
.mr-btn-primary{background:var(--mr-accent);color:#fff}
.mr-btn-primary:hover:not(:disabled){filter:brightness(1.15)}
.mr-btn-success{background:var(--mr-success);color:#fff}
.mr-btn-success:hover:not(:disabled){filter:brightness(1.15)}
.mr-btn-danger{background:var(--mr-danger);color:#fff}
.mr-btn-danger:hover:not(:disabled){filter:brightness(1.15)}
.mr-btn-outline{background:transparent;color:#74c0fc;border:1px solid var(--mr-accent)}
.mr-btn-outline:hover:not(:disabled){background:rgba(42,105,184,0.15)}
.mr-actions{display:flex;justify-content:space-between;align-items:center;margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--mr-border)}
/* Check list */
.mr-checks{list-style:none;margin:0;padding:0}
.mr-checks li{display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;border-bottom:1px solid #f1f5f9;font-size:0.9rem}
.mr-checks li{display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;border-bottom:1px solid var(--mr-border);font-size:0.9rem}
.mr-checks li:last-child{border-bottom:none}
.mr-check-icon{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0}
.mr-check-ok{background:#dcfce7;color:#16a34a}
.mr-check-warn{background:#fef9c3;color:#a16207}
.mr-check-fail{background:#fef2f2;color:#dc2626}
.mr-check-info{background:#e0f2fe;color:#0284c7}
.mr-check-label{flex:1;font-weight:500}
.mr-check-value{color:#64748b;font-size:0.85rem}
.mr-check-hint{font-size:0.8rem;color:#94a3b8}
.mr-check-ok{background:rgba(47,158,68,0.18);color:#69db7c}
.mr-check-warn{background:rgba(240,180,41,0.18);color:#ffd43b}
.mr-check-fail{background:rgba(224,49,49,0.18);color:#ff8787}
.mr-check-info{background:rgba(42,105,184,0.18);color:#74c0fc}
.mr-check-label{flex:1;font-weight:500;color:var(--mr-text)}
.mr-check-value{color:var(--mr-muted);font-size:0.85rem}
.mr-check-hint{font-size:0.8rem;color:var(--mr-muted)}
/* Alert */
.mr-alert{padding:0.75rem 1rem;border-radius:6px;font-size:0.9rem;margin-bottom:1rem;display:flex;align-items:flex-start;gap:0.5rem}
.mr-alert-warn{background:#fef3c7;color:#92400e;border:1px solid #fde68a}
.mr-alert-danger{background:#fef2f2;color:#991b1b;border:1px solid #fecaca}
.mr-alert-success{background:#f0fdf4;color:#166534;border:1px solid #bbf7d0}
.mr-alert-info{background:#eff6ff;color:#1e40af;border:1px solid #bfdbfe}
.mr-alert-warn{background:rgba(240,180,41,0.12);color:#ffe08a;border:1px solid rgba(240,180,41,0.35)}
.mr-alert-danger{background:rgba(224,49,49,0.12);color:#ffc9c9;border:1px solid rgba(224,49,49,0.4)}
.mr-alert-success{background:rgba(47,158,68,0.12);color:#b2f2bb;border:1px solid rgba(47,158,68,0.35)}
.mr-alert-info{background:rgba(42,105,184,0.12);color:#a5d8ff;border:1px solid rgba(42,105,184,0.35)}
/* Progress */
.mr-progress{background:#e2e8f0;border-radius:8px;overflow:hidden;height:8px;margin:1rem 0}
.mr-progress-bar{height:100%;background:linear-gradient(90deg,#2563eb,#3b82f6);transition:width 0.4s;border-radius:8px}
.mr-status{font-size:0.85rem;color:#64748b;min-height:1.2rem}
.mr-progress{background:var(--mr-input);border-radius:8px;overflow:hidden;height:8px;margin:1rem 0}
.mr-progress-bar{height:100%;background:linear-gradient(90deg,var(--mr-accent),var(--mr-accent2));transition:width 0.4s;border-radius:8px}
.mr-status{font-size:0.85rem;color:var(--mr-muted);min-height:1.2rem}
/* Provisioning checkboxes */
.mr-provision-list{list-style:none;padding:0;margin:0}
.mr-provision-list li{display:flex;align-items:center;gap:0.75rem;padding:0.6rem 0.5rem;border-bottom:1px solid #f1f5f9;font-size:0.9rem;cursor:pointer;border-radius:4px}
.mr-provision-list li:hover{background:#f8fafc}
.mr-provision-list li{display:flex;align-items:center;gap:0.75rem;padding:0.6rem 0.5rem;border-bottom:1px solid var(--mr-border);font-size:0.9rem;cursor:pointer;border-radius:4px}
.mr-provision-list li:hover{background:var(--mr-input)}
.mr-provision-list li:last-child{border-bottom:none}
.mr-provision-list input[type=checkbox]{width:18px;height:18px;accent-color:#2563eb}
.mr-provision-desc{font-size:0.8rem;color:#94a3b8;margin-left:auto}
.mr-provision-list input[type=checkbox]{width:18px;height:18px;accent-color:var(--mr-accent)}
.mr-provision-desc{font-size:0.8rem;color:var(--mr-muted);margin-left:auto}
/* Log */
.mr-log{background:#1e293b;color:#94a3b8;border-radius:8px;padding:1rem;font-family:'SF Mono',SFMono-Regular,Consolas,monospace;font-size:0.8rem;max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;margin-top:1rem;display:none}
.mr-log{background:#0d0f12;color:var(--mr-muted);border:1px solid var(--mr-border);border-radius:8px;padding:1rem;font-family:'SF Mono',SFMono-Regular,Consolas,monospace;font-size:0.8rem;max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;margin-top:1rem;display:none}
.mr-log.visible{display:block}
/* Footer */
.mr-footer{text-align:center;padding:1.5rem;color:#94a3b8;font-size:0.8rem}
.mr-footer a{color:#2563eb;text-decoration:none}
.mr-footer{text-align:center;padding:1.5rem;color:var(--mr-muted);font-size:0.8rem}
.mr-footer a{color:#74c0fc;text-decoration:none}
/* Spinner */
.mr-spinner{display:inline-block;width:16px;height:16px;border:2px solid rgba(255,255,255,0.3);border-radius:50%;border-top-color:#fff;animation:spin 0.6s linear infinite}
@@ -1629,11 +1644,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<div class="mr-panel <?php echo $securityVerified ? '' : 'visible'; ?>" id="panel0">
<h2>Security Verification</h2>
<p class="mr-desc">To prevent unauthorized access, enter the security code from the file <code>mokorestore-security.php</code> in your site root.</p>
<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="border:1px solid #2a2f37;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#1e2229">
<div style="font-weight:600;font-size:0.9rem;color:#cfd6df;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
<span style="font-size:1.1rem">&#128274;</span> How to find the code
</div>
<ol style="margin:0;padding-left:1.25rem;color:#475569;font-size:0.9rem;line-height:1.6">
<ol style="margin:0;padding-left:1.25rem;color:#9aa4b2;font-size:0.9rem;line-height:1.6">
<li>Connect to your server via FTP, SSH, or file manager</li>
<li>Open <code>mokorestore-security.php</code> in the site root directory</li>
<li>Copy the 8-character code and enter it below</li>
@@ -1666,7 +1681,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<h2>Extract Backup</h2>
<p class="mr-desc">Extract site-backup.zip into the current directory.</p>
<div class="mr-field">
<label>Archive Password <span style="font-weight:normal;color:#94a3b8">(leave blank if not encrypted)</span></label>
<label>Archive Password <span style="font-weight:normal;color:#9aa4b2">(leave blank if not encrypted)</span></label>
<input type="password" id="archivePassword" placeholder="Optional decryption password">
</div>
<div class="mr-progress"><div class="mr-progress-bar" id="extractProgress" style="width:0%"></div></div>
@@ -1694,8 +1709,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<strong>Merge</strong> = keep existing table, INSERT IGNORE new rows.
<strong>Data Only</strong> = keep schema, INSERT data as-is (assumes matching structure).
</div>
<div id="tableResolutionList" style="max-height:400px;overflow-y:auto;border:1px solid #e2e8f0;border-radius:8px;margin-top:1rem">
<div style="padding:1rem;color:#94a3b8;text-align:center">Scanning tables...</div>
<div id="tableResolutionList" style="max-height:400px;overflow-y:auto;border:1px solid #2a2f37;border-radius:8px;margin-top:1rem">
<div style="padding:1rem;color:#9aa4b2;text-align:center">Scanning tables...</div>
</div>
<input type="hidden" id="tableResolutions" value="{}">
<div class="mr-status" id="tableScanStatus"></div>
@@ -1735,21 +1750,21 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<h2>Site Configuration</h2>
<p class="mr-desc">Configure your site settings. Credentials were removed from the backup for security &mdash; enter the correct values for this server.</p>
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#f8fafc">
<div style="font-weight:600;font-size:0.9rem;color:#334155;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
<div style="border:1px solid #2a2f37;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#1e2229">
<div style="font-weight:600;font-size:0.9rem;color:#cfd6df;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
<span style="font-size:1.1rem">&#127760;</span> General
</div>
<div class="mr-field"><label>Site Name</label><input type="text" id="siteName" value="Joomla Site"></div>
<div class="mr-field">
<label>Live Site URL <span style="font-weight:normal;color:#94a3b8">(optional)</span></label>
<label>Live Site URL <span style="font-weight:normal;color:#9aa4b2">(optional)</span></label>
<input type="text" id="liveSite" placeholder="https://example.com">
<div class="mr-hint">Leave blank to auto-detect. Set this if using a reverse proxy or custom domain.</div>
</div>
</div>
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#f8fafc">
<div style="font-weight:600;font-size:0.9rem;color:#334155;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
<span style="font-size:1.1rem">&#9993;</span> Mail / SMTP <span style="font-weight:normal;font-size:0.8rem;color:#94a3b8">&mdash; leave blank if using PHP mail()</span>
<div style="border:1px solid #2a2f37;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#1e2229">
<div style="font-weight:600;font-size:0.9rem;color:#cfd6df;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
<span style="font-size:1.1rem">&#9993;</span> Mail / SMTP <span style="font-weight:normal;font-size:0.8rem;color:#9aa4b2">&mdash; leave blank if using PHP mail()</span>
</div>
<div class="mr-field"><label>SMTP Host</label><input type="text" id="smtpHost" placeholder="smtp.example.com"></div>
<div class="mr-row">
@@ -1758,8 +1773,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
</div>
</div>
<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="border:1px solid #2a2f37;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#1e2229">
<div style="font-weight:600;font-size:0.9rem;color:#cfd6df;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
<span style="font-size:1.1rem">&#128736;</span> Server
</div>
<div class="mr-field" style="display:flex;align-items:center;gap:0.5rem">
@@ -2031,7 +2046,7 @@ async function runPreflight() {
li.appendChild(val);
if (c.warn && c.hint) {
var hint = document.createElement('div');
hint.style.cssText = 'font-size:0.85em;color:#a16207;margin-top:4px;padding:4px 8px;background:#fef9c3;border-radius:4px;';
hint.style.cssText = 'font-size:0.85em;color:#ffd43b;margin-top:4px;padding:4px 8px;background:rgba(240,180,41,0.12);border-radius:4px;';
hint.textContent = c.hint;
li.appendChild(hint);
}
@@ -2113,7 +2128,7 @@ async function scanTables() {
const r = await post('scanTables');
if (!r.success || !r.tables || r.tables.length === 0) {
container.innerHTML = '<div style="padding:1rem;color:#94a3b8;text-align:center">No tables found in database.sql (or file not present). You can skip this step.</div>';
container.innerHTML = '<div style="padding:1rem;color:#9aa4b2;text-align:center">No tables found in database.sql (or file not present). You can skip this step.</div>';
setStatus('tableScanStatus', r.tables ? 'No tables found' : (r.message || 'Scan failed'), r.success ? '' : 'error');
log(r.message || 'No tables found');
return;
@@ -2134,16 +2149,16 @@ function renderTableList() {
tableList.forEach(function(name) {
var row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:0.5rem 0.75rem;border-bottom:1px solid #f1f5f9;font-size:0.85rem;';
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:0.5rem 0.75rem;border-bottom:1px solid #2a2f37;font-size:0.85rem;';
var label = document.createElement('span');
label.style.cssText = 'font-family:monospace;color:#334155;word-break:break-all;flex:1;margin-right:0.75rem;';
label.style.cssText = 'font-family:monospace;color:#cfd6df;word-break:break-all;flex:1;margin-right:0.75rem;';
label.textContent = name;
var sel = document.createElement('select');
sel.dataset.table = name;
sel.className = 'table-mode-select';
sel.style.cssText = 'padding:0.3rem 0.5rem;border:1px solid #d1d5db;border-radius:4px;font-size:0.8rem;min-width:120px;background:#fff;';
sel.style.cssText = 'padding:0.3rem 0.5rem;border:1px solid #2a2f37;border-radius:4px;font-size:0.8rem;min-width:120px;background:var(--mr-input);';
var modes = [
['replace', 'Replace'],
@@ -153,7 +153,7 @@ class NotificationSender
private static function sendNtfy(object $profile, object $record, bool $success): bool
{
$topic = trim($profile->ntfy_topic ?? '');
$server = trim($profile->ntfy_server ?? 'https://ntfy.sh');
$server = trim($profile->ntfy_server ?? 'https://ntfy.mokoconsulting.tech');
$token = trim($profile->ntfy_token ?? '');
if ($topic === '') {
@@ -428,7 +428,7 @@ class NotificationSender
private static function sendRestoreNtfy(object $profile, string $type, array $details): bool
{
$topic = trim($profile->ntfy_topic ?? '');
$server = trim($profile->ntfy_server ?? 'https://ntfy.sh');
$server = trim($profile->ntfy_server ?? 'https://ntfy.mokoconsulting.tech');
$token = trim($profile->ntfy_token ?? '');
if ($topic === '') {
@@ -122,6 +122,8 @@ class RestoreEngine
}
// Step 4: Import database
$dbErrorCount = 0;
if ($restoreDb) {
$sqlFile = $this->stagingDir . '/database.sql';
@@ -130,6 +132,16 @@ class RestoreEngine
$importer = new DatabaseImporter();
$tableCount = $importer->import($sqlFile);
$this->log('Database imported: ' . $tableCount . ' statements executed');
if ($importer->hasErrors()) {
$dbErrors = $importer->getErrors();
$dbErrorCount = count($dbErrors);
$this->log('WARNING: ' . $dbErrorCount . ' database statement(s) FAILED during import — the database may be incomplete:');
foreach (array_slice($dbErrors, 0, 5) as $err) {
$this->log(' - ' . substr($err, 0, 200));
}
}
} else {
$this->log('No database.sql found in archive — skipping database restore');
}
@@ -172,7 +184,10 @@ class RestoreEngine
return [
'success' => true,
'message' => 'Restore complete from: ' . basename($archivePath),
'warning' => $dbErrorCount > 0,
'message' => $dbErrorCount > 0
? 'Restore finished, but ' . $dbErrorCount . ' database statement(s) failed — the database may be incomplete. Check the log.'
: 'Restore complete from: ' . basename($archivePath),
'log' => implode("\n", $this->log),
];
} catch (\Throwable $e) {
@@ -185,15 +185,25 @@ class SteppedRestoreEngine
break;
case 'complete':
$sqlErrors = (int) ($restoreState['sql_errors'] ?? 0);
$this->destroyRestoreState($sessionId);
$session->destroy();
$completeMsg = 'Restore complete: ' . $session->archiveName;
if ($sqlErrors > 0) {
$completeMsg .= ' — WARNING: ' . $sqlErrors
. ' database statement(s) failed during import; the restored database may be incomplete. Check the site log.';
}
return [
'session_id' => $sessionId,
'phase' => 'complete',
'progress' => 100,
'message' => 'Restore complete: ' . $session->archiveName,
'message' => $completeMsg,
'done' => true,
'sql_errors' => $sqlErrors,
];
}
@@ -230,6 +240,39 @@ class SteppedRestoreEngine
}
}
/**
* Cancel an in-progress restore: delete the staging directory and the
* session/state files so a client-side cancel does not leave orphaned
* artefacts (staging dir + *.restore.json) behind. Best-effort a missing
* or already-finished session is treated as success.
*
* @param string $sessionId The stepped-restore session id
*
* @return array ['error' => bool, 'message' => string]
*/
public function cancel(string $sessionId): array
{
$restoreState = $this->loadRestoreState($sessionId);
if ($restoreState !== null) {
$stagingDir = $restoreState['staging_dir'] ?? '';
if (!empty($stagingDir) && is_dir($stagingDir)) {
$this->recursiveDelete($stagingDir);
}
}
$this->destroyRestoreState($sessionId);
$session = SteppedSession::load($sessionId);
if ($session) {
$session->destroy();
}
return ['error' => false, 'message' => 'Restore cancelled'];
}
/**
* Extract phase: extract archive to staging directory.
*/
@@ -480,34 +523,37 @@ class SteppedRestoreEngine
while (($line = fgets($handle)) !== false) {
$trimmed = trim($line);
// Skip empty lines
if ($trimmed === '') {
continue;
}
// Skip single-line comments
if (str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) {
continue;
}
// Handle multi-line comments
if (str_starts_with($trimmed, '/*')) {
$inMultiLineComment = true;
}
if ($inMultiLineComment) {
if (str_contains($trimmed, '*/')) {
$inMultiLineComment = false;
// Comment/blank skipping applies only BETWEEN statements — never inside
// an accumulating statement, where a line may be part of a multi-line
// string value that happens to start with --, # or /*.
if ($currentStatement === '') {
if ($trimmed === '') {
continue;
}
continue;
if (str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) {
continue;
}
if (str_starts_with($trimmed, '/*')) {
$inMultiLineComment = true;
}
if ($inMultiLineComment) {
if (str_contains($trimmed, '*/')) {
$inMultiLineComment = false;
}
continue;
}
}
// Accumulate the statement
$currentStatement .= $line;
// Check if statement is complete (ends with semicolon)
if (str_ends_with($trimmed, ';')) {
// A statement ends at a ';' that is NOT inside a quoted string literal,
// so a ';' or newline embedded in a value does not split it prematurely.
if ($this->isCompleteStatement($currentStatement)) {
$statement = trim($currentStatement);
$currentStatement = '';
@@ -522,7 +568,7 @@ class SteppedRestoreEngine
$db->setQuery($statement);
$db->execute();
} catch (\Exception $e) {
error_log('MokoSuiteBackup SQL import warning: ' . $e->getMessage());
$this->recordSqlError($state, $e->getMessage());
}
$statementsExecuted++;
@@ -553,7 +599,7 @@ class SteppedRestoreEngine
$db->execute();
$state['sql_executed']++;
} catch (\Exception $e) {
error_log('MokoSuiteBackup SQL import warning (final): ' . $e->getMessage());
$this->recordSqlError($state, $e->getMessage());
}
}
@@ -562,8 +608,78 @@ class SteppedRestoreEngine
$state['sql_done'] = true;
$session->currentStep++;
$session->phase = 'config';
$session->statusMessage = 'Database import complete: ' . $state['sql_executed'] . ' statements';
$session->log('Database import complete: ' . $state['sql_executed'] . ' statements executed');
$sqlErrors = (int) ($state['sql_errors'] ?? 0);
$doneMsg = 'Database import complete: ' . $state['sql_executed'] . ' statements'
. ($sqlErrors > 0 ? ' (' . $sqlErrors . ' statement(s) FAILED — see log)' : '');
$session->statusMessage = $doneMsg;
$session->log($doneMsg);
}
/**
* Determine whether an accumulated SQL string is a complete statement: it
* ends with ';' that is NOT inside a quoted string literal. Tracks single,
* double and backtick quoting (with backslash escapes; MySQL '' doubling
* falls out of the toggle naturally) so a ';' or newline inside a value does
* not terminate the statement early.
*/
private function isCompleteStatement(string $sql): bool
{
$len = \strlen($sql);
$inSingle = false;
$inDouble = false;
$inBacktick = false;
for ($i = 0; $i < $len; $i++) {
$ch = $sql[$i];
if ($inSingle) {
if ($ch === '\\') { $i++; continue; }
if ($ch === "'") { $inSingle = false; }
continue;
}
if ($inDouble) {
if ($ch === '\\') { $i++; continue; }
if ($ch === '"') { $inDouble = false; }
continue;
}
if ($inBacktick) {
if ($ch === '`') { $inBacktick = false; }
continue;
}
if ($ch === "'") { $inSingle = true; }
elseif ($ch === '"') { $inDouble = true; }
elseif ($ch === '`') { $inBacktick = true; }
}
// If we ended inside a string literal, the statement is not finished yet.
if ($inSingle || $inDouble || $inBacktick) {
return false;
}
return str_ends_with(rtrim($sql), ';');
}
/**
* Record a failed SQL statement in the restore state (count + first few
* messages) so a partial import is surfaced instead of silently passing.
*/
private function recordSqlError(array &$state, string $message): void
{
$state['sql_errors'] = (int) ($state['sql_errors'] ?? 0) + 1;
if (!isset($state['sql_error_list']) || !\is_array($state['sql_error_list'])) {
$state['sql_error_list'] = [];
}
if (\count($state['sql_error_list']) < 5) {
$state['sql_error_list'][] = substr($message, 0, 200);
}
error_log('MokoSuiteBackup SQL import error: ' . $message);
}
/**
@@ -171,6 +171,11 @@ class SteppedSession
/**
* Clean up old session files (older than 24 hours).
*
* Reaps backup + restore session state (mb_*.json covers both the
* `<id>.json` and `<id>.restore.json` files) and, as a backstop for
* restores abandoned without a cancel (e.g. the browser was just closed),
* any orphaned restore staging directories left in the tmp folder.
*/
public static function cleanupOldSessions(): void
{
@@ -182,10 +187,42 @@ class SteppedSession
$cutoff = time() - 86400;
foreach (glob($dir . '/mb_*.json') as $file) {
foreach (glob($dir . '/mb_*.json') ?: [] as $file) {
if (filemtime($file) < $cutoff) {
@unlink($file);
}
}
// Orphaned restore staging directories (mokosuitebackup-restore-<tag>-<id>).
foreach (glob(JPATH_ROOT . '/tmp/mokosuitebackup-restore-*', GLOB_ONLYDIR) ?: [] as $stagingDir) {
if (@filemtime($stagingDir) < $cutoff) {
self::recursiveDelete($stagingDir);
}
}
}
/**
* Recursively delete a directory (used to reap orphaned restore staging dirs).
*/
private static function recursiveDelete(string $dir): void
{
if ($dir === '' || !is_dir($dir)) {
return;
}
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
if ($item->isDir()) {
@rmdir($item->getPathname());
} else {
@unlink($item->getPathname());
}
}
@rmdir($dir);
}
}
@@ -16,13 +16,21 @@ use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
HTMLHelper::_('behavior.multiselect');
// Full-black progress theme for the backup/restore modals (matches view=runbackup).
// Loaded directly rather than via useStyle(), which does not emit a link for this
// component (same pattern as the runbackup screen).
$this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebackup/css/backup-progress.css');
$user = Factory::getApplication()->getIdentity();
$canDownload = $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup');
$ajaxToken = Session::getFormToken();
$ajaxUrl = Route::_('index.php?option=com_mokosuitebackup&format=json', false);
// Template for the "View backup record" button on the progress modal (__MSBID__ swapped in JS).
$recordUrlTpl = Route::_('index.php?option=com_mokosuitebackup&view=backup&id=__MSBID__', false);
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
@@ -135,22 +143,42 @@ $listDirn = $this->escape($this->state->get('list.direction'));
</form>
<!-- Stepped Backup Modal (for shared hosting) -->
<div class="modal fade" id="mokosuitebackup-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal fade msb-progress-modal" id="mokosuitebackup-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mb-modal-title">Backup in Progress</h5>
<h2 class="modal-title" id="mb-modal-title">
<span class="icon-archive" aria-hidden="true" id="mb-modal-icon"></span>
<span id="mb-modal-titletext">Backup in Progress</span>
</h2>
</div>
<div class="modal-body">
<div class="alert alert-warning py-1 px-2 mb-2" style="font-size:0.85rem;">
<p class="msb-pm-warn">
<span class="icon-warning-circle" aria-hidden="true"></span>
<strong>Do not navigate away or close this window</strong> while the backup is running.
Do not navigate away or close this window while the backup is running.
</p>
<div class="msb-rb-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div id="mb-progress-bar" class="msb-rb-bar is-animated" style="width:0%;">0%</div>
</div>
<div class="progress mb-2" style="height:24px;">
<div id="mb-progress-bar" class="progress-bar" role="progressbar" style="width:0%;">0%</div>
<p id="mb-status" class="msb-pm-status">Initializing...</p>
<p id="mb-phase" class="msb-pm-phase">Phase: init</p>
<div class="msb-pm-autocontinue" id="mb-autocontinue-wrap">
<label>
<input type="checkbox" id="mb-autocontinue" checked>
Automatically return to the backups list when the backup finishes
</label>
</div>
<div class="msb-pm-actions d-none" id="mb-actions">
<button type="button" id="mb-retry" class="btn btn-primary d-none">Retry backup</button>
<a href="#" id="mb-record" class="btn btn-primary d-none" target="_blank" rel="noopener">View backup record</a>
<button type="button" id="mb-done" class="btn btn-secondary d-none">Back to backups</button>
</div>
<div class="msb-pm-cancel" id="mb-cancel-wrap">
<button type="button" id="mb-cancel" class="btn btn-outline-danger">Cancel backup</button>
</div>
<p id="mb-status" class="text-muted mb-1" style="font-size:0.9rem;">Initializing...</p>
<p id="mb-phase" class="text-muted mb-0" style="font-size:0.8rem;">Phase: init</p>
</div>
</div>
</div>
@@ -183,12 +211,29 @@ $listDirn = $this->escape($this->state->get('list.direction'));
bootstrap.Modal.getInstance(document.getElementById('mokosuitebackup-modal'))?.hide();
}
// Shared bar-state styling for both the backup and restore progress bars,
// matching the runbackup screen: animated stripes while running, solid green
// on completion, solid red on failure.
function setBarState(bar, phase, progress) {
if (phase === 'failed') {
bar.classList.remove('is-animated', 'is-success');
bar.classList.add('is-danger');
} else if (progress >= 100 || phase === 'complete') {
bar.classList.remove('is-animated', 'is-danger');
bar.classList.add('is-success');
}
if (bar.parentElement) {
bar.parentElement.setAttribute('aria-valuenow', progress);
}
}
function updateProgress(progress, message, phase) {
const bar = document.getElementById('mb-progress-bar');
bar.style.width = progress + '%';
bar.textContent = progress + '%';
document.getElementById('mb-status').textContent = message;
document.getElementById('mb-phase').textContent = 'Phase: ' + phase;
setBarState(bar, phase, progress);
}
async function postAjax(params) {
@@ -205,64 +250,139 @@ $listDirn = $this->escape($this->state->get('list.direction'));
return res.json();
}
// --- Progress-modal controls (auto-continue, cancel, terminal buttons) ---
var RECORD_URL = <?php echo json_encode($recordUrlTpl); ?>;
var mbRecordId = 0;
var mbCancelled = false;
function mbEl(id) { return document.getElementById(id); }
function mbShow(n) { if (n) { n.classList.remove('d-none'); } }
function mbHide(n) { if (n) { n.classList.add('d-none'); } }
// Update the modal title's icon + text without innerHTML (values are static).
function setBackupTitle(iconClass, text) {
var ic = mbEl('mb-modal-icon');
if (ic) { ic.className = iconClass; }
var tx = mbEl('mb-modal-titletext');
if (tx) { tx.textContent = text; }
}
// Reset to the "running" control state (cancel + auto-continue visible, no actions).
function mbRunningControls() {
mbHide(mbEl('mb-actions'));
mbHide(mbEl('mb-retry'));
mbHide(mbEl('mb-record'));
mbHide(mbEl('mb-done'));
mbShow(mbEl('mb-cancel-wrap'));
mbShow(mbEl('mb-autocontinue-wrap'));
if (mbEl('mb-cancel')) { mbEl('mb-cancel').disabled = false; }
}
// Switch out of the "running" control state (hide cancel + auto-continue).
function mbTerminalControls() {
backupRunning = false;
mbHide(mbEl('mb-cancel-wrap'));
mbHide(mbEl('mb-autocontinue-wrap'));
}
function leaveToList() { hideModal(); location.reload(); }
document.addEventListener('DOMContentLoaded', function() {
var doneBtn = mbEl('mb-done');
if (doneBtn) { doneBtn.addEventListener('click', leaveToList); }
// Cancel: double confirmation, then stop the loop and cancel the record.
var cancelBtn = mbEl('mb-cancel');
if (cancelBtn) {
cancelBtn.addEventListener('click', function() {
if (!backupRunning || mbCancelled) { return; }
if (!confirm('Cancel the backup that is in progress?')) { return; }
if (!confirm('Are you sure? The backup will be stopped.')) { return; }
mbCancelled = true;
backupRunning = false;
cancelBtn.disabled = true;
document.getElementById('mb-status').textContent = 'Cancelling…';
if (mbRecordId > 0) {
postAjax({ task: 'ajax.cancelBackup', id: mbRecordId }).then(leaveToList, leaveToList);
} else {
leaveToList();
}
});
}
});
function backupComplete() {
updateProgress(100, 'Backup complete.', 'complete');
setBackupTitle('icon-check', 'Backup Complete');
mbTerminalControls();
// Auto-continue: return to the list automatically unless the user opted out.
if (mbEl('mb-autocontinue') && mbEl('mb-autocontinue').checked) {
setTimeout(leaveToList, 1500);
return;
}
mbShow(mbEl('mb-actions'));
mbShow(mbEl('mb-done'));
if (mbRecordId > 0 && RECORD_URL) {
var rec = mbEl('mb-record');
rec.setAttribute('href', RECORD_URL.replace('__MSBID__', String(mbRecordId)));
mbShow(rec);
}
}
function backupFailed(message) {
updateProgress(0, 'ERROR: ' + (message || 'Backup failed'), 'failed');
setBackupTitle('icon-cancel', 'Backup Failed');
mbTerminalControls();
mbShow(mbEl('mb-actions'));
mbShow(mbEl('mb-done'));
var retry = mbEl('mb-retry');
mbShow(retry);
retry.onclick = function() { startSteppedBackup(); };
}
async function startSteppedBackup() {
const profileSelect = document.getElementById('mb-profile-select');
const profileId = profileSelect ? profileSelect.value : '1';
mbRecordId = 0;
mbCancelled = false;
mbRunningControls();
setBackupTitle('icon-archive', 'Backup in Progress');
showModal();
updateProgress(0, 'Initializing backup...', 'init');
try {
// Init
const initResult = await postAjax({
task: 'ajax.init',
profile_id: profileId
});
const initResult = await postAjax({ task: 'ajax.init', profile_id: profileId });
if (initResult.error) {
updateProgress(0, 'ERROR: ' + initResult.message, 'failed');
setTimeout(hideModal, 5000);
return;
}
if (initResult.error) { backupFailed(initResult.message); return; }
// Show preflight warnings if any
if (initResult.warnings && initResult.warnings.length > 0) {
var warningEl = document.getElementById('mb-phase');
warningEl.textContent = 'Warnings: ' + initResult.warnings.join('; ');
warningEl.style.color = '#856404';
warningEl.style.color = '#ffd23f';
}
const sessionId = initResult.session_id;
if (initResult.record_id) { mbRecordId = initResult.record_id; }
updateProgress(initResult.progress, initResult.message, initResult.phase);
// Run steps until done
// Run steps until done (or cancelled)
let done = false;
while (!done) {
const stepResult = await postAjax({
task: 'ajax.step',
session_id: sessionId
});
if (stepResult.error) {
updateProgress(0, 'ERROR: ' + stepResult.message, 'failed');
setTimeout(hideModal, 5000);
return;
}
while (!done && !mbCancelled) {
const stepResult = await postAjax({ task: 'ajax.step', session_id: sessionId });
if (stepResult.error) { backupFailed(stepResult.message); return; }
if (stepResult.record_id) { mbRecordId = stepResult.record_id; }
updateProgress(stepResult.progress, stepResult.message, stepResult.phase);
done = stepResult.done || false;
}
// Complete
document.getElementById('mb-modal-title').textContent = 'Backup Complete';
setTimeout(function() {
hideModal();
location.reload();
}, 2000);
if (mbCancelled) { return; }
backupComplete();
} catch (err) {
updateProgress(0, 'ERROR: ' + err.message, 'failed');
setTimeout(hideModal, 5000);
if (mbCancelled) { return; }
backupFailed(err.message);
}
}
@@ -292,8 +412,10 @@ $listDirn = $this->escape($this->state->get('list.direction'));
// Close restore modal handled by Bootstrap data-bs-dismiss
// AJAX stepped restore
var restoreRunning = false;
// --- AJAX stepped restore (controls mirror the backup-progress modal) ---
var restoreRunning = false;
var restoreCancelled = false;
var restoreSessionId = '';
function showRestoreProgress() {
restoreRunning = true;
@@ -306,12 +428,38 @@ $listDirn = $this->escape($this->state->get('list.direction'));
bootstrap.Modal.getInstance(document.getElementById('mb-restore-progress-modal'))?.hide();
}
// Update the restore title's icon + text without innerHTML (values are static).
function setRestoreTitle(iconClass, text) {
var ic = mbEl('mb-restore-icon');
if (ic) { ic.className = iconClass; }
var tx = mbEl('mb-restore-titletext');
if (tx) { tx.textContent = text; }
}
function restoreRunningControls() {
mbHide(mbEl('mb-restore-actions'));
mbHide(mbEl('mb-restore-retry'));
mbHide(mbEl('mb-restore-done'));
mbShow(mbEl('mb-restore-cancel-wrap'));
mbShow(mbEl('mb-restore-autocontinue-wrap'));
if (mbEl('mb-restore-cancel')) { mbEl('mb-restore-cancel').disabled = false; }
}
function restoreTerminalControls() {
restoreRunning = false;
mbHide(mbEl('mb-restore-cancel-wrap'));
mbHide(mbEl('mb-restore-autocontinue-wrap'));
}
function leaveRestoreToList() { hideRestoreProgress(); location.reload(); }
function updateRestoreProgress(progress, message, phase) {
var bar = document.getElementById('mb-restore-progress-bar');
bar.style.width = progress + '%';
bar.textContent = progress + '%';
document.getElementById('mb-restore-status').textContent = message;
document.getElementById('mb-restore-phase').textContent = 'Phase: ' + phase;
setBarState(bar, phase, progress);
}
window.addEventListener('beforeunload', function(e) {
@@ -321,15 +469,79 @@ $listDirn = $this->escape($this->state->get('list.direction'));
}
});
async function startSteppedRestore(e) {
e.preventDefault();
document.addEventListener('DOMContentLoaded', function() {
var doneBtn = mbEl('mb-restore-done');
if (doneBtn) { doneBtn.addEventListener('click', leaveRestoreToList); }
// Cancel: double confirmation. A restore cannot be rolled back mid-flight,
// so the second prompt warns that the site may be left partially restored.
var cancelBtn = mbEl('mb-restore-cancel');
if (cancelBtn) {
cancelBtn.addEventListener('click', function() {
if (!restoreRunning || restoreCancelled) { return; }
if (!confirm('Cancel the restore that is in progress?')) { return; }
if (!confirm('Are you sure? Stopping mid-restore may leave the site partially restored.')) { return; }
restoreCancelled = true;
restoreRunning = false;
cancelBtn.disabled = true;
document.getElementById('mb-restore-status').textContent = 'Cancelling…';
// Tell the server to clean up the staging dir + session state, then leave.
if (restoreSessionId) {
postAjax({ task: 'ajax.restoreCancel', session_id: restoreSessionId }).then(leaveRestoreToList, leaveRestoreToList);
} else {
leaveRestoreToList();
}
});
}
});
function restoreComplete(result) {
var sqlErrors = (result && result.sql_errors) ? result.sql_errors : 0;
if (sqlErrors > 0) {
// Partial DB import — surface the warning and do NOT auto-return, so the
// admin actually sees that the restored database may be incomplete.
updateRestoreProgress(100, (result && result.message) || 'Restore finished, but some database statements failed.', 'complete');
setRestoreTitle('icon-warning', 'Restore Complete (with warnings)');
restoreTerminalControls();
mbShow(mbEl('mb-restore-actions'));
mbShow(mbEl('mb-restore-done'));
return;
}
updateRestoreProgress(100, 'Restore complete.', 'complete');
setRestoreTitle('icon-check', 'Restore Complete');
restoreTerminalControls();
if (mbEl('mb-restore-autocontinue') && mbEl('mb-restore-autocontinue').checked) {
setTimeout(leaveRestoreToList, 1500);
return;
}
mbShow(mbEl('mb-restore-actions'));
mbShow(mbEl('mb-restore-done'));
}
function restoreFailed(message) {
updateRestoreProgress(0, 'ERROR: ' + (message || 'Restore failed'), 'failed');
setRestoreTitle('icon-cancel', 'Restore Failed');
restoreTerminalControls();
mbShow(mbEl('mb-restore-actions'));
mbShow(mbEl('mb-restore-done'));
var retry = mbEl('mb-restore-retry');
mbShow(retry);
retry.onclick = function() { runSteppedRestore(); };
}
async function runSteppedRestore() {
var recordId = document.getElementById('mb-restore-record-id').value;
var restoreFiles = document.getElementById('mb-restore-files').checked ? 1 : 0;
var restoreDb = document.getElementById('mb-restore-db').checked ? 1 : 0;
var preserveConfig = document.getElementById('mb-restore-config').checked ? 1 : 0;
var password = document.getElementById('mb-restore-password').value;
restoreCancelled = false;
restoreSessionId = '';
restoreRunningControls();
setRestoreTitle('icon-refresh', 'Restore in Progress');
showRestoreProgress();
updateRestoreProgress(0, 'Initializing restore...', 'init');
@@ -343,47 +555,36 @@ $listDirn = $this->escape($this->state->get('list.direction'));
encryption_password: password
});
if (initResult.error) {
updateRestoreProgress(0, 'ERROR: ' + initResult.message, 'failed');
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
setTimeout(hideRestoreProgress, 5000);
return;
}
if (initResult.error) { restoreFailed(initResult.message); return; }
var sessionId = initResult.session_id;
restoreSessionId = sessionId;
updateRestoreProgress(initResult.progress, initResult.message, initResult.phase);
var done = false;
while (!done) {
var stepResult = await postAjax({
task: 'ajax.restoreStep',
session_id: sessionId
});
if (stepResult.error) {
updateRestoreProgress(0, 'ERROR: ' + stepResult.message, 'failed');
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
setTimeout(hideRestoreProgress, 5000);
return;
}
var lastResult = null;
while (!done && !restoreCancelled) {
var stepResult = await postAjax({ task: 'ajax.restoreStep', session_id: sessionId });
if (stepResult.error) { restoreFailed(stepResult.message); return; }
updateRestoreProgress(stepResult.progress, stepResult.message, stepResult.phase);
lastResult = stepResult;
done = stepResult.done || false;
}
document.getElementById('mb-restore-title').textContent = 'Restore Complete';
setTimeout(function() {
hideRestoreProgress();
location.reload();
}, 2000);
if (restoreCancelled) { return; }
restoreComplete(lastResult);
} catch (err) {
updateRestoreProgress(0, 'ERROR: ' + err.message, 'failed');
document.getElementById('mb-restore-title').textContent = 'Restore Failed';
setTimeout(hideRestoreProgress, 5000);
if (restoreCancelled) { return; }
restoreFailed(err.message);
}
}
function startSteppedRestore(e) {
e.preventDefault();
runSteppedRestore();
}
// Attach the AJAX restore handler to the restore form
document.addEventListener('DOMContentLoaded', function() {
var restoreForm = document.getElementById('mb-restore-form');
@@ -452,18 +653,41 @@ $listDirn = $this->escape($this->state->get('list.direction'));
</div>
<!-- Restore Progress Modal -->
<div class="modal fade" id="mb-restore-progress-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal fade msb-progress-modal" id="mb-restore-progress-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mb-restore-title">Restore in Progress</h5>
<h2 class="modal-title" id="mb-restore-title">
<span class="icon-refresh" aria-hidden="true" id="mb-restore-icon"></span>
<span id="mb-restore-titletext">Restore in Progress</span>
</h2>
</div>
<div class="modal-body">
<div class="progress mb-2" style="height:24px;">
<div id="mb-restore-progress-bar" class="progress-bar bg-danger" role="progressbar" style="width:0%;">0%</div>
<p class="msb-pm-warn">
<span class="icon-warning-circle" aria-hidden="true"></span>
Do not navigate away or close this window while the restore is running.
</p>
<div class="msb-rb-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div id="mb-restore-progress-bar" class="msb-rb-bar is-animated is-danger" style="width:0%;">0%</div>
</div>
<p id="mb-restore-status" class="msb-pm-status">Initializing...</p>
<p id="mb-restore-phase" class="msb-pm-phase">Phase: init</p>
<div class="msb-pm-autocontinue" id="mb-restore-autocontinue-wrap">
<label>
<input type="checkbox" id="mb-restore-autocontinue" checked>
Automatically return to the backups list when the restore finishes
</label>
</div>
<div class="msb-pm-actions d-none" id="mb-restore-actions">
<button type="button" id="mb-restore-retry" class="btn btn-primary d-none">Retry restore</button>
<button type="button" id="mb-restore-done" class="btn btn-secondary d-none">Back to backups</button>
</div>
<div class="msb-pm-cancel" id="mb-restore-cancel-wrap">
<button type="button" id="mb-restore-cancel" class="btn btn-outline-danger">Cancel restore</button>
</div>
<p id="mb-restore-status" class="text-muted mb-1" style="font-size:0.9rem;">Initializing...</p>
<p id="mb-restore-phase" class="text-muted mb-0" style="font-size:0.8rem;">Phase: init</p>
</div>
</div>
</div>
@@ -87,6 +87,10 @@ $config = [
'viewRecord' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_VIEW_RECORD'),
'leaveWarn' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_LEAVE_WARNING'),
'continueUpdate' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CONTINUE_UPDATE'),
'cancelConfirm1' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM1'),
'cancelConfirm2' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL_CONFIRM2'),
'cancelling' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCELLING'),
'failedNoUpdate' => Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_FAILED_NO_UPDATE'),
],
];
@@ -120,6 +124,11 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
<div id="msb-rb-phase" class="fw-bold mb-1"></div>
<div id="msb-rb-status" class="mb-0"><?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_STARTING'), ENT_QUOTES, 'UTF-8'); ?></div>
<div id="msb-rb-warn" class="alert alert-warning fw-bold mt-3 mb-0" role="alert">
<span class="icon-warning" aria-hidden="true"></span>
<?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_DONT_CLOSE'), ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php if ($safeReturnUrl !== '') : ?>
<!-- Pre-update/uninstall flow: opt in to auto-continue, or get a manual "Continue" button on completion. -->
<div class="form-check mt-3" id="msb-rb-autocontinue-wrap">
@@ -132,10 +141,17 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
<div id="msb-rb-actions" class="d-none mt-3">
<button type="button" id="msb-rb-retry" class="btn btn-primary d-none"></button>
<a href="#" id="msb-rb-record" class="btn btn-primary d-none"></a>
<a href="#" id="msb-rb-record" class="btn btn-primary d-none" target="_blank" rel="noopener"></a>
<a href="#" id="msb-rb-continue" class="btn btn-warning d-none"></a>
<a href="<?php echo $dashboardUrl; ?>" id="msb-rb-dashboard" class="btn btn-secondary d-none"></a>
</div>
<!-- Cancel is available while the backup is running; requires a double confirmation. -->
<div id="msb-rb-cancel-wrap" class="mt-3">
<button type="button" id="msb-rb-cancel" class="btn btn-outline-danger">
<?php echo htmlspecialchars(Text::_('COM_MOKOJOOMBACKUP_RUNBACKUP_CANCEL'), ENT_QUOTES, 'UTF-8'); ?>
</button>
</div>
</div>
<!-- Full-bleed progress bar, edge-to-edge along the bottom of the card. -->
@@ -152,6 +168,7 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
var CFG = <?php echo json_encode($config); ?>;
var L = CFG.labels || {};
var running = false;
var cancelled = false;
var lastRecordId = 0;
var el = {
@@ -159,6 +176,7 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
track: document.querySelector('.msb-rb-progress'),
phase: document.getElementById('msb-rb-phase'),
status: document.getElementById('msb-rb-status'),
warn: document.getElementById('msb-rb-warn'),
title: document.getElementById('msb-rb-title'),
actions: document.getElementById('msb-rb-actions'),
retry: document.getElementById('msb-rb-retry'),
@@ -166,9 +184,38 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
continue: document.getElementById('msb-rb-continue'),
dashboard: document.getElementById('msb-rb-dashboard'),
autocontinue: document.getElementById('msb-rb-autocontinue'),
autocontinueWrap: document.getElementById('msb-rb-autocontinue-wrap')
autocontinueWrap: document.getElementById('msb-rb-autocontinue-wrap'),
cancel: document.getElementById('msb-rb-cancel'),
cancelWrap: document.getElementById('msb-rb-cancel-wrap')
};
function hideCancel() { if (el.cancelWrap) { el.cancelWrap.classList.add('d-none'); } }
/* Cancel the running backup requires DOUBLE confirmation. Stops the step
loop, tells the server to cancel the record, and leaves to the dashboard
WITHOUT continuing any pending update. */
if (el.cancel) {
el.cancel.addEventListener('click', function () {
if (!running || cancelled) { return; }
if (!window.confirm(L.cancelConfirm1 || 'Cancel the backup that is in progress?')) { return; }
if (!window.confirm(L.cancelConfirm2 || 'Are you sure? The backup will be stopped and the update will NOT continue.')) { return; }
cancelled = true;
running = false; /* also disarms the leave-warning */
el.cancel.disabled = true;
if (el.warn) { el.warn.classList.add('d-none'); }
setStatus(L.cancelling || 'Cancelling…');
var go = function () { window.location.href = CFG.dashboardUrl; };
/* Best-effort server-side cancel of the record, then leave. */
if (lastRecordId > 0) {
post({ task: 'ajax.cancelBackup', id: lastRecordId }).then(go, go);
} else {
go();
}
});
}
function setBar(pct, striped) {
pct = Math.max(0, Math.min(100, parseInt(pct, 10) || 0));
el.bar.style.width = pct + '%';
@@ -204,6 +251,8 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
el.bar.classList.add('is-success');
setBar(100, false);
el.title.textContent = L.complete || 'Backup complete';
if (el.warn) { el.warn.classList.add('d-none'); }
hideCancel();
/* Set the one-shot skip flag for THIS action so the following
server-side onExtensionBefore(Update|Uninstall) backup is skipped once
@@ -254,30 +303,34 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
running = false;
el.bar.classList.remove('is-animated');
el.bar.classList.add('is-danger');
if (el.warn) { el.warn.classList.add('d-none'); }
el.title.textContent = L.failed || 'Backup failed';
setStatus(message || 'Backup failed');
/* Pre-update/uninstall flow: the backup failed, so the update must NOT
run. We deliberately do NOT offer a "continue without backup" option
here the only ways forward are to retry the backup or go back. */
setStatus(CFG.returnUrl ? (L.failedNoUpdate || 'Backup failed — the update was not started. Retry the backup, or go back without updating.') : (message || 'Backup failed'));
hideCancel();
showActions();
el.retry.textContent = L.retry || 'Retry';
el.retry.classList.remove('d-none');
el.retry.onclick = function () {
el.retry.classList.add('d-none');
el.continue.classList.add('d-none');
el.bar.classList.remove('is-danger');
el.bar.classList.add('is-animated');
run();
};
if (CFG.returnUrl) {
showBtn(el.continue, L.continue || 'Continue without backup', CFG.returnUrl);
}
showBtn(el.dashboard, L.dashboard || 'Back to dashboard', CFG.dashboardUrl);
}
async function run() {
if (running) { return; }
running = true;
cancelled = false;
el.actions.classList.add('d-none');
if (el.cancelWrap) { el.cancelWrap.classList.remove('d-none'); }
if (el.cancel) { el.cancel.disabled = false; }
setBar(0, true);
setPhase('');
setStatus(L.starting || 'Starting backup…');
@@ -295,8 +348,12 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
if (init.record_id) { lastRecordId = init.record_id; }
var done = false;
while (!done) {
var step = await post({ task: 'ajax.step', session_id: init.session_id });
while (!done && !cancelled) {
/* Pass the pre-action so the SERVER can arm the one-shot skip flag the
moment this backup completes (see AjaxController::step) the black
"update is running" overlay then never fronts a second backup even
if the completion ack POST below fails to land. */
var step = await post({ task: 'ajax.step', session_id: init.session_id, action: CFG.action || '' });
if (!step || step.error) {
throw new Error((step && step.message) || 'Backup step failed');
@@ -309,16 +366,27 @@ $this->getDocument()->addStyleSheet(Uri::root(true) . '/media/com_mokosuitebacku
done = step.done || false;
}
/* User cancelled mid-flight the click handler already stops the
loop and navigates away, so do not fall through to completion. */
if (cancelled) { return; }
running = false;
await onComplete();
} catch (err) {
if (cancelled) { return; }
onError(err && err.message ? err.message : String(err));
}
}
/* Warn before leaving while a backup is mid-flight. */
/* Warn before closing/leaving the window while a backup is mid-flight.
Return the message too some browsers need it, others show a generic
prompt, but any non-empty value triggers the confirmation dialog. */
window.addEventListener('beforeunload', function (e) {
if (running) { e.preventDefault(); e.returnValue = L.leaveWarn || ''; }
if (!running) { return undefined; }
var msg = L.leaveWarn || 'A backup is in progress. Leaving now will cancel it.';
e.preventDefault();
e.returnValue = msg;
return msg;
});
if (CFG.autostart) {
@@ -447,7 +447,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
var label = document.createElement('label');
label.className = 'form-check-label';
label.setAttribute('for', 'mb-rtype-' + type);
label.textContent = typeLabels[TYPE] || type;
label.textContent = typeLabels[type] || type;
div.appendChild(input);
div.appendChild(label);
@@ -8,7 +8,7 @@
-->
<extension type="module" client="administrator" method="upgrade">
<name>Module - MokoSuiteBackup - cPanel</name>
<version>02.63.00</version>
<version>02.67.00</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name>
<version>02.63.00</version>
<version>02.67.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name>
<version>02.63.00</version>
<version>02.67.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name>
<version>02.63.00</version>
<version>02.67.00</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name>
<version>02.63.00</version>
<version>02.67.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -39,6 +39,34 @@ final class MokoSuiteBackupQuickicon extends CMSPlugin implements SubscriberInte
return;
}
// A dashboard quick-icon must NEVER be able to crash the whole admin page.
// Build the icon defensively; on any failure, contribute nothing and log.
try {
$icon = $this->buildStatusIcon();
} catch (\Throwable $e) {
error_log('MokoSuiteBackup quickicon widget failed: ' . $e->getMessage());
return;
}
// The module (mod_quickicon) expects each plugin to append a LIST of icons
// to the 'result' argument; it then iterates that list and array_merge()s a
// defaults array into each icon. Appending a bare icon dict instead of a
// [ list ] makes it iterate the dict's string values and crash in
// array_merge — so the wrapping [$icon] here is load-bearing.
$result = $event->getArgument('result', []);
$result[] = [$icon];
$event->setArgument('result', $result);
}
/**
* Build the single status quick-icon (backup freshness / failures).
*
* @return array A mod_quickicon icon definition.
*/
private function buildStatusIcon(): array
{
$db = Factory::getDbo();
// Get last completed backup
@@ -91,20 +119,15 @@ final class MokoSuiteBackupQuickicon extends CMSPlugin implements SubscriberInte
$subtitle = $ago . ' | ' . ($stats->total ?? 0) . ' backups | ' . $sizeTotal . ' MB';
}
$result = $event->getArgument('result', []);
$result[] = [
[
'link' => 'index.php?option=com_mokosuitebackup&view=backups',
'image' => $warning ? 'icon-warning' : 'icon-database',
'icon' => $warning ? 'icon-warning' : 'icon-database',
'text' => Text::_($text),
'linkadd' => $subtitle ? '<br><small>' . htmlspecialchars($subtitle) . '</small>' : '',
'id' => 'plg_quickicon_mokosuitebackup',
'group' => 'MOD_QUICKICON_MAINTENANCE',
],
return [
'link' => 'index.php?option=com_mokosuitebackup&view=backups',
'image' => $warning ? 'icon-warning' : 'icon-database',
'icon' => $warning ? 'icon-warning' : 'icon-database',
'text' => Text::_($text),
'linkadd' => $subtitle ? '<br><small>' . htmlspecialchars($subtitle) . '</small>' : '',
'id' => 'plg_quickicon_mokosuitebackup',
'group' => 'MOD_QUICKICON_MAINTENANCE',
];
$event->setArgument('result', $result);
}
private function timeAgo(string $datetime): string
@@ -14,3 +14,6 @@ PLG_SYSTEM_MOKOJOOMBACKUP_BACKING_UP="Backing up…"
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_TITLE="Backup in progress"
PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_MESSAGE="A MokoSuiteBackup is currently running in another window. The administrator area is locked until it finishes — this page will refresh automatically. Please do not make changes while the backup runs."
@@ -14,3 +14,6 @@ PLG_SYSTEM_MOKOJOOMBACKUP_BACKING_UP="Backing up…"
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup complete — safe to update."
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_FAILED="Backup failed"
PLG_SYSTEM_MOKOJOOMBACKUP_BACKUP_STARTING="Starting backup…"
PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING="Backup complete. The update is now running — please wait and do not close this window…"
PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_TITLE="Backup in progress"
PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_MESSAGE="A MokoSuiteBackup is currently running in another window. The administrator area is locked until it finishes — this page will refresh automatically. Please do not make changes while the backup runs."
@@ -101,6 +101,31 @@
window.location.href = target;
}, true);
/* Full-screen "update is running" overlay shown while the re-fired update
POST processes, so the user knows it is working and not to close/switch. */
function showUpdateOverlay() {
if (document.getElementById('msb-update-overlay')) { return; }
var o = document.createElement('div');
o.id = 'msb-update-overlay';
o.setAttribute('role', 'status');
o.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:#000;color:#fff;'
+ 'display:flex;flex-direction:column;align-items:center;justify-content:center;'
+ 'text-align:center;padding:2rem;font-size:1.15rem;font-weight:600;';
var spin = document.createElement('span');
spin.className = 'spinner-border mb-3';
spin.setAttribute('aria-hidden', 'true');
var msg = document.createElement('div');
msg.textContent = (cfg && cfg.updateRunningText)
|| 'Backup complete. The update is now running — please wait and do not close this window…';
o.appendChild(spin);
o.appendChild(msg);
document.body.appendChild(o);
}
/* On return from the backup screen: restore the selection and re-fire. */
function resume() {
var params = new URLSearchParams(window.location.search);
@@ -131,20 +156,40 @@
if (box) { box.checked = true; }
});
/* Joomla's list-check reads boxchecked — set it so the re-fire passes. */
/* Restore boxchecked AND fire its `change` event. Joomla's toolbar web
component (joomla-toolbar-button) keeps a list-selection button
DISABLED until boxchecked changes via a change event setting the
value alone leaves the button disabled, so executeTask() would no-op
and the update never submits (it just returns to the list). */
var bc = form.querySelector('input[name="boxchecked"]');
if (bc) { bc.value = (data.cids || []).length; }
if (bc) {
bc.value = (data.cids || []).length;
bc.dispatchEvent(new Event('change', { bubbles: true }));
}
}
window.__msbResuming = true;
var btn = document.querySelector('[task="' + data.task + '"]');
/* Show a full-screen "update is running" overlay so the user gets clear
feedback that the update is proceeding while the re-fired POST runs.
It persists until the page navigates away (the update finishes and
redirects), matching the black full-screen backup screen. */
showUpdateOverlay();
if (btn) {
window.setTimeout(function () { btn.click(); }, 300);
} else if (window.Joomla && typeof Joomla.submitbutton === 'function') {
Joomla.submitbutton(data.task);
}
/* Re-fire the toolbar action. The `task` attribute is on the OUTER
<joomla-toolbar-button>, but its click handler is bound to the INNER
<button>/<a>, so click that inner element (clicking the wrapper does
nothing). Fall back to Joomla.submitbutton if the button isn't found. */
window.setTimeout(function () {
var wc = document.querySelector('[task="' + data.task + '"]');
var inner = wc ? (wc.querySelector('button, a') || wc) : null;
if (inner) {
inner.click();
} else if (window.Joomla && typeof Joomla.submitbutton === 'function') {
Joomla.submitbutton(data.task);
}
}, 300);
}
if (document.readyState === 'loading') {
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name>
<version>02.63.00</version>
<version>02.67.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -15,6 +15,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Document\HtmlDocument;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
@@ -34,6 +35,17 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
/** @var array<string,bool> Per-request cache of the one-shot "client already backed up" skip flag. */
private array $preactionSkip = [];
/**
* Max age (seconds) of the client "already backed up" skip flag that this
* plugin will honour. The flag stores the unix time it was armed (by the
* full-screen backup's ajax.step completion / ajax.preupdateAck); the re-fired
* update/uninstall follows within a second or two, so a small window is ample.
* Bounding it means a backup that was run then ABANDONED (browser closed before
* the update re-fired) cannot silently suppress a genuine, unrelated pre-action
* backup minutes or hours later in the same session.
*/
private const PREACTION_SKIP_TTL = 300;
public static function getSubscribedEvents(): array
{
return [
@@ -132,6 +144,10 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
return;
}
// While a backup is actively running, lock the admin UI so no other
// window/page can navigate or change anything mid-backup. May close().
$this->maybeLockAdminDuringBackup();
// Pre-update: send Joomla core-update installs through the full-screen
// backup page BEFORE the update runs (may not return from here).
$this->maybeRedirectForUpdate();
@@ -154,6 +170,97 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
$this->cleanupOldSnapshots();
}
/**
* Detect whether a stepped backup is actively running right now.
*
* The stepped-backup engine rewrites its session JSON file
* (JPATH_ROOT/tmp/mokosuitebackup-sessions/mb_*.json) on every step and
* deletes it on completion, so a file whose mtime is within the last ~45s
* is a reliable, cross-session heartbeat. A crashed/abandoned backup goes
* stale and stops locking on its own the admin is never permanently locked.
*/
private function backupIsActive(): bool
{
$dir = JPATH_ROOT . '/tmp/mokosuitebackup-sessions';
if (!is_dir($dir)) {
return false;
}
$cutoff = time() - 45;
foreach (glob($dir . '/mb_*.json') ?: [] as $file) {
if (@filemtime($file) >= $cutoff) {
return true;
}
}
return false;
}
/**
* While a backup is actively running, lock the admin UI: every admin page
* load (except the full-screen backup screen and the login/logout page)
* gets a "backup in progress" interstitial that auto-refreshes, so no other
* window can navigate or change anything mid-backup. Gated by the
* lock_admin_during_backup option; auto-released when the backup ends.
*/
private function maybeLockAdminDuringBackup(): void
{
if (!(int) ComponentHelper::getParams('com_mokosuitebackup')->get('lock_admin_during_backup', 1)) {
return;
}
$app = $this->getApplication();
$input = $app->getInput();
$option = $input->getCmd('option', '');
$view = $input->getCmd('view', '');
// Never lock the full-screen backup screen (it IS the backup) nor login/logout.
if (($option === 'com_mokosuitebackup' && $view === 'runbackup') || $option === 'com_login') {
return;
}
if (!$this->backupIsActive()) {
return;
}
$this->renderBackupLockPage();
$app->close();
}
/**
* Emit a self-contained "backup in progress" interstitial that refreshes
* itself (meta refresh) until the backup finishes and the lock releases.
*/
private function renderBackupLockPage(): void
{
$app = $this->getApplication();
$title = Text::_('PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_TITLE');
$message = Text::_('PLG_SYSTEM_MOKOSUITEBACKUP_LOCK_MESSAGE');
$t = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
$m = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
$html = '<!doctype html><html lang="en"><head><meta charset="utf-8">'
. '<meta name="viewport" content="width=device-width, initial-scale=1">'
. '<meta http-equiv="refresh" content="8">'
. '<title>' . $t . '</title><style>'
. 'html,body{height:100%;margin:0}'
. 'body{background:#000;color:#fff;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;'
. 'display:flex;align-items:center;justify-content:center;text-align:center}'
. '.w{max-width:34rem;padding:2rem}'
. '.s{width:3rem;height:3rem;margin:0 auto 1.5rem;border:.35rem solid rgba(255,255,255,.2);'
. 'border-top-color:#fff;border-radius:50%;animation:sp 1s linear infinite}'
. 'h1{font-size:1.5rem;margin:.5rem 0}p{opacity:.85;line-height:1.55}'
. '@keyframes sp{to{transform:rotate(360deg)}}'
. '</style></head><body><div class="w"><div class="s"></div>'
. '<h1>' . $t . '</h1><p>' . $m . '</p></div></body></html>';
$app->setHeader('Content-Type', 'text/html; charset=utf-8', true);
$app->sendHeaders();
echo $html;
}
/**
* Inject the client-side interceptor on the Extensions Update and
* Extensions Manage pages, so an EXTENSION update / uninstall also runs
@@ -243,6 +350,7 @@ final class MokoSuiteBackup extends CMSPlugin implements SubscriberInterface
'backupBeforeUpdate' => $view === 'update' && (bool) $beforeUpdate,
'backupBeforeUninstall' => $view === 'manage' && (bool) $beforeUninst,
'recentBackup' => false,
'updateRunningText' => Text::_('PLG_SYSTEM_MOKOSUITEBACKUP_UPDATE_RUNNING'),
]);
$doc->addScript(Uri::root(true) . '/media/plg_system_mokosuitebackup/js/installer-backup.js', [], ['defer' => true]);
@@ -551,12 +659,16 @@ JS;
$skipKey = 'mokosuitebackup.skip_preaction_' . $paramName;
if (!array_key_exists($paramName, $this->preactionSkip)) {
$this->preactionSkip[$paramName] = (bool) $session->get($skipKey, false);
$session->set($skipKey, false);
/* The flag stores the unix time it was armed. Consume it one-shot (reset
to 0) and honour the skip ONLY if it was armed within the TTL a
stale flag from an abandoned backup must not suppress a real one. */
$armedAt = (int) $session->get($skipKey, 0);
$session->set($skipKey, 0);
$this->preactionSkip[$paramName] = ($armedAt > 0 && (time() - $armedAt) <= self::PREACTION_SKIP_TTL);
}
if ($this->preactionSkip[$paramName]) {
// Client already backed up; skip this whole re-fired (possibly batch) request.
// Client already backed up (recently); skip this re-fired (possibly batch) request.
$this->preactionRan[$paramName] = true;
return;
@@ -5,7 +5,7 @@
* "MokoSuiteBackup: Run Backup Profile" task.
-->
<form>
<fieldset name="run_profile">
<fieldset name="run_profile" label="PLG_TASK_MOKOJOOMBACKUP_FIELDSET_RUN_PROFILE">
<field
name="profile_id"
type="sql"

Some files were not shown because too many files have changed in this diff Show More