Compare commits

..

210 Commits

Author SHA1 Message Date
Jonathan Miller 384b8824c6 refactor: remove tpl_mokoonyx submodule
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Template is managed independently; submodule reference no longer needed.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 22:18:47 -05:00
Jonathan Miller e01791ae68 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
2026-06-04 22:13:50 -05:00
Jonathan Miller e42d6e7596 fix: hardcode branding values in overrides, adjust menu indent, add reset download keys
Hardcode {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders
to literal values in all language override .ini files. Adjust admin menu
indent (2rem parent, 2.5rem child). Add one-shot reset download keys
toggle to DevTools plugin.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 22:10:28 -05:00
gitea-actions[bot] 36658fa8ca chore(version): pre-release bump to 02.34.08-dev [skip ci] 2026-06-05 01:42:18 +00:00
Jonathan Miller 5645516845 fix: custom CSS classes for admin menu indent control
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
- .sidebar-wrapper .item-level-1 > a: padding-inline-start 1.5rem
- .mokowaas-menu-item > a: 1rem (replaces item-level-2)
- .mokowaas-menu-child > a: 1.5rem (replaces item-level-3)
Decouples from Atum's deep indentation cascade.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 20:40:32 -05:00
Jonathan Miller 4ce8c6b4ea fix: reduce indent on admin menu module level 2 and 3 items
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Override Atum's deep padding-left with tighter padding-inline-start
(0.5rem for level 2, 0.75rem for level 3 sub-components).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 20:38:51 -05:00
Jonathan Miller 01056afe74 fix: menu icons use FA6 for unmapped classes, query all MokoWaaS submenu items
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
- Helpdesk: fa-solid fa-handshake-angle (was icon-headphones, unmapped)
- .htaccess: fa-solid fa-file-code (was icon-file-code, unmapped)
- Query now finds ALL submenu items under the MokoWaaS parent menu,
  including those linking to com_plugins, com_installer, com_checkin, com_cache

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 20:33:56 -05:00
Jonathan Miller 3cc39cfa8f Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
2026-06-04 20:32:52 -05:00
Jonathan Miller 0956757445 refactor: move security hardening to firewall plugin (#155)
Move protectPlugin(), ensureProtectedFlag(), isOurExtension() from
core to firewall. Core no longer handles extension protection —
the firewall's onAfterRoute does it. Uses MokoWaaSHelper::isMasterUser().

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 20:32:40 -05:00
gitea-actions[bot] 9c75d0254e chore(version): pre-release bump to 02.34.07-dev [skip ci] 2026-06-05 01:32:22 +00:00
Jonathan Miller c847b4a274 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
2026-06-04 20:31:38 -05:00
Jonathan Miller c93ae27b64 fix: revert /opt/moko-platform check — DinD inner containers can't see host volumes
The act_runner DinD architecture means the job container (inner Docker)
doesn't share the outer container's volume mounts. Always clone fresh.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 20:31:28 -05:00
gitea-actions[bot] 0e28958ede chore(version): pre-release bump to 02.34.06-dev [skip ci] 2026-06-05 01:27:55 +00:00
Jonathan Miller 46bb7c31c2 refactor: rename core plugin, remove branding from support files, rename Heartbeat Token
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
- Plugin name: System - MokoWaaS Core (was System - MokoWaaS)
- Description updated to reflect admin tools suite coordinator role
- Removed updateAtumBranding() from plugin install script
- Removed brand_name placeholder from language override deployment
- Renamed "Health API Token" label to "Heartbeat Token" in language files
- Updated all .ini and .sys.ini descriptions (en-GB + en-US)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 20:22:11 -05:00
Jonathan Miller 04af4a93a8 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
2026-06-04 20:15:43 -05:00
Jonathan Miller 3b99c5b6bc fix: offline bypass redirect loop, add default bypassed pages
- Move from onAfterRoute to onAfterInitialise (earlier in lifecycle)
- Remove forced tmpl=component which broke rendering
- Default bypassed pages: terms-of-service, privacy-policy,
  community-guidelines, support, tickets, submit-a-ticket

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 20:15:12 -05:00
gitea-actions[bot] 1b47876a6c chore(version): pre-release bump to 02.34.05-dev [skip ci] 2026-06-05 01:09:11 +00:00
gitea-actions[bot] 48ff2b2109 chore(version): pre-release bump to 02.34.04-dev [skip ci] 2026-06-05 01:01:18 +00:00
Jonathan Miller 0c4857d6e0 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
2026-06-04 19:57:33 -05:00
Jonathan Miller 9f3e4b9d31 fix: cpanel collapse toggle as caret button on far left
Replaced clickable header with a dedicated caret button (fa-caret-right/down)
on the far left that toggles the collapse. Dashboard button pushed to far right.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 19:57:22 -05:00
gitea-actions[bot] 3834ba4c1c chore(version): pre-release bump to 02.34.03-dev [skip ci] 2026-06-05 00:56:40 +00:00
Jonathan Miller a8a41e9bad fix: replace smart quotes with ASCII in workflow files
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Unicode left/right quotes caused git clone to fail with
"protocol 'https' is not supported" inside Docker runner.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 19:56:20 -05:00
Jonathan Miller 8c927b0a1b fix: remove stray chevron icon from cpanel module header
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
The icon-chevron-down appeared as random noise when the card was
collapsed. The entire header row is already clickable as a collapse
toggle — no separate indicator needed.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 19:05:49 -05:00
Jonathan Miller 21e57eaadc fix: override admin Help sidebar link to mokoconsulting.tech/support
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
Targets a[href*="dashboard=help"] in addition to existing
help.joomla.org/docs.joomla.org overrides. Opens in new window.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:51:42 -05:00
Jonathan Miller fadd3a01cd fix: load component sys.ini language files for admin menu translation
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Text::_() returned raw keys (COM_MOKOBACKUP, COM_MOKOJOOMCOMMUNITY)
because Joomla hadn't loaded those components' language files yet.
Now loads both .sys.ini and .ini for each discovered component.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:48:00 -05:00
Jonathan Miller 95097c4d3f ci: use pre-installed /opt/moko-platform on runner, fallback to clone
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
All workflows check for /opt/moko-platform first (updated by cron
every 6h). Falls back to fresh clone if not available. Eliminates
composer install timeouts that were causing build failures.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 18:42:01 -05:00
Jonathan Miller e71b075d94 ci: remove updates.xml steps from pre-release (MokoGitea handles it)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
MokoGitea license server generates updates.xml dynamically.
Removed: updates_xml_build.php call, updates.xml commit/push,
updates.xml branch sync. These are no longer needed.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:56:07 -05:00
Jonathan Miller 1ecc8be8d1 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
2026-06-04 17:51:06 -05:00
Jonathan Miller 361a58f8cd fix: self-heal orphaned update records (extension_id=0) in postflight
Joomla's update finder sometimes fails to link #__updates records
to the installed extension. fixUpdateRecords() joins on element+type
to set the correct extension_id.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:50:52 -05:00
gitea-actions[bot] 51ac178281 chore: update development channel 02.34.02-dev [skip ci] 2026-06-04 22:35:57 +00:00
gitea-actions[bot] b46da78e6c chore(version): pre-release bump to 02.34.02-dev [skip ci] 2026-06-04 22:35:54 +00:00
Jonathan Miller 57a54e8959 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
2026-06-04 17:34:59 -05:00
Jonathan Miller 1c8625f828 feat: admin menu auto-discovers Moko component submenus from #__menu
Queries #__menu for all com_moko* components (except com_mokowaas).
Renders each as a collapsible parent with its submenu items nested
at level 3. Icons and titles loaded from the DB menu records.
Uses Text::_() for language key translation.

MokoJoomBackup, MokoJoomCommunity, MokoJoomCross, etc. automatically
appear with their full submenu when installed — zero config needed.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:34:39 -05:00
gitea-actions[bot] 66b19f184c chore: update development channel 02.34.01-dev [skip ci] 2026-06-04 22:31:43 +00:00
gitea-actions[bot] 4694e67e1c chore(version): pre-release bump to 02.34.01-dev [skip ci] 2026-06-04 22:31:41 +00:00
Jonathan Miller e2e2ac8b56 chore: remove temp files [skip ci] 2026-06-04 17:31:22 -05:00
Jonathan Miller 415eeaac56 chore: minor version bump to 02.34.00 [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:31:12 -05:00
gitea-actions[bot] 4c8bb93952 chore: update development channel 02.33.03-dev [skip ci] 2026-06-04 22:24:56 +00:00
gitea-actions[bot] 561fdcd881 chore(version): pre-release bump to 02.33.03-dev [skip ci] 2026-06-04 22:24:54 +00:00
Jonathan Miller 0d096acfa8 fix: use Joomla native CacheControllerFactory for cache clearing
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
Replace manual file deletion with Joomla's built-in cache API
(CacheControllerFactoryInterface::createCacheController->clean).
Same approach as com_cache uses. Cleans both site and admin cache.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:22:28 -05:00
Jonathan Miller 3db14d29ef Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
2026-06-04 17:18:26 -05:00
Jonathan Miller dfff3c327f fix: remove Factory::getCache()->gc() from clearCache (crashes in Joomla 6)
File-based cache cleanup already handles purging. The gc() calls
used a Joomla 5 API that may not work in Joomla 6, causing the
AJAX response to fail and show an error icon.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:18:13 -05:00
gitea-actions[bot] 4ab3b163f6 chore: update development channel 02.33.02-dev [skip ci] 2026-06-04 22:06:19 +00:00
gitea-actions[bot] 60910c2b8b chore(version): pre-release bump to 02.33.02-dev [skip ci] 2026-06-04 22:06:18 +00:00
Jonathan Miller 4e0151be1b feat: add dlid (license key) and blockChildUninstall to package manifest
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 35s
- dlid prefix="dlid=" tells Joomla to send license key with update requests
- blockChildUninstall prevents removing sub-extensions without full package uninstall

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 17:02:19 -05:00
Jonathan Miller 36d958f31f refactor: pre-release uses CLI version_bump.php instead of inline shell math
Delegates version bumping to moko-platform CLI tools:
- version_bump.php (patch default, --minor for RC)
- version_set_platform.php (stability suffix)
- version_check.php (consistency)

This keeps the workflow thin and the logic in the shared CLI.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 16:45:41 -05:00
Jonathan Miller e482a293c9 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
2026-06-04 16:43:42 -05:00
Jonathan Miller 04a4bf8aba fix: pre-release bumps patch for dev/alpha/beta so each build gets unique version
Without this, repeated dev releases produce the same version and
Joomla won't offer the update (version not higher than installed).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 16:43:41 -05:00
gitea-actions[bot] b3082f27e3 chore: update development channel 02.33.01-dev [skip ci] 2026-06-04 21:32:23 +00:00
Jonathan Miller f30d7dd7af Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-04 16:31:42 -05:00
Jonathan Miller ecd5b6c786 feat: create privacy data requests from backend with auto-process option
- Add collapsible "New Request" form to privacy view (user select, type, auto-process)
- Controller handles 'create' action (pending) and 'approve' without request_id (auto-process: create + immediate approve)
- User dropdown populated from #__users

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 16:31:32 -05:00
gitea-actions[bot] 1f7419f33d chore: update development channel 02.33.01-dev [skip ci] 2026-06-04 19:29:29 +00:00
Jonathan Miller 171f489e3d feat: add Open button to extensions manager for installed components
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Shows an "Open" link to the component dashboard for installed
components and packages that have a com_ admin directory.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 14:28:48 -05:00
Jonathan Miller e808a168cb Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
2026-06-04 14:26:54 -05:00
Jonathan Miller 1f89a323d5 feat: auto-discover installed Moko extensions in admin sidebar menu
Static MokoWaaS views listed first, then installed Moko components
auto-discovered from #__extensions. Supported: MokoJoomBackup,
MokoJoomCommunity, MokoJoomCalendar, MokoJoomGallery, MokoJoomCross,
Akeeba Backup, Akeeba Ticket System. New extensions appear automatically.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 14:26:47 -05:00
gitea-actions[bot] 329eca3db6 chore: update development channel 02.33.01-dev [skip ci] 2026-06-04 19:09:51 +00:00
Jonathan Miller 42b47be564 Merge remote-tracking branch 'origin/main' into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 10s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 27s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
# Conflicts:
#	.mokogitea/workflows/pre-release.yml
#	updates.xml
2026-06-04 14:06:33 -05:00
Jonathan Miller 79ac068bc4 feat: MokoWaaS guided tours — hijack Joomla's system, rebrand to MokoWaaS
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Successful in 12s
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 14s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 32s
- Unpublish all default Joomla guided tours (joomla-* UIDs)
- Create 4 MokoWaaS tours: Welcome, Firewall Setup, Helpdesk, Extensions
- Re-enable mod_guidedtours with title "MokoWaaS Tours"
- Add language override MOD_GUIDEDTOURS → "MokoWaaS Tours"
- Re-enable guided tours plugin if disabled

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 14:04:02 -05:00
Jonathan Miller ee7260b435 fix: change /kb/ URLs to /support/products/ across all references
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:53:39 -05:00
Jonathan Miller 9498a56f98 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
2026-06-04 13:50:22 -05:00
Jonathan Miller 8ea6df020b refactor: remove branding/identity from core plugin (#154)
Remove 8 branding methods and 4 call sites (-428 lines):
- enforceAtumBranding(), hexToHsl(), injectFavicon()
- enforceLoginSupportUrls(), loadLanguageOverrides()
- getPlaceholders(), parseLanguageFile(), setTemplateParam()

MokoWaaS is an admin tools suite, not a branding layer.
Core plugin docblock updated to reflect new purpose.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:50:12 -05:00
gitea-actions[bot] b304d6c9a2 chore: update development channel 02.33.01-dev [skip ci] 2026-06-04 18:24:08 +00:00
gitea-actions[bot] 557c15cbe0 chore(version): pre-release bump to 02.33.01-dev [skip ci] 2026-06-04 18:24:07 +00:00
Jonathan Miller 524523b8c6 fix: Chart.js CDN with SRI hash, rename Community Builder to MokoJoomCommunity
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
Chart.js not bundled with Joomla 6 — use jsdelivr CDN with integrity check.
Dashboard user manager button shows MokoJoomCommunity (not Community Builder).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:23:18 -05:00
Jonathan Miller e858130375 fix: hide cpanel module on MokoWaaS dashboard (redundant info)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
The dashboard already shows site info, plugins, WAF blocks, logins,
and updates. The cpanel module duplicates this. Now it auto-hides
when option=com_mokowaas and view is dashboard or empty.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:20:15 -05:00
Jonathan Miller f0e2228700 feat: dashboard charts, visual separator, remove small class
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
- Add WAF activity bar chart and login activity line chart (Chart.js, 14 days)
- Add border-left separator between plugin grid and info panel
- Remove all 'small' CSS class usage for better readability
- Add getWafBlocksByDay() and getLoginsByDay() to DashboardModel

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:14:09 -05:00
jmiller c5aef3c939 chore: sync updates.xml SHA256 from dev [skip ci] 2026-06-04 17:57:03 +00:00
Jonathan Miller f401a76227 chore: changelog [Unreleased] workflow, CI gate, contributing docs
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
- CHANGELOG.md now uses [Unreleased] section as staging area
- Only minor version headings (no dev patch numbers)
- pr-check.yml blocks PRs to main if [Unreleased] is empty
- pre-release.yml and auto-release.yml extract from [Unreleased] for release notes
- CONTRIBUTING.md documents the changelog workflow
- RC pre-release bumps minor version consolidating dev patches

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:53:04 -05:00
Jonathan Miller 71133cdc24 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
2026-06-04 12:33:03 -05:00
Jonathan Miller 7bd9213ec5 ci: RC pre-release bumps minor version, consolidating dev patches
Dev patches (02.33.01, 02.33.02, etc.) exist for the update system.
RC consolidates to next minor: 02.33.xx → 02.34.00-rc.
auto-release already uses --bump minor for stable.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:33:01 -05:00
gitea-actions[bot] 5ca1eb98a8 chore: update development channel 02.33.01-dev [skip ci] 2026-06-04 17:31:12 +00:00
Jonathan Miller 65a6cdf505 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 20s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-04 12:28:56 -05:00
Jonathan Miller 5ab21a0fac ci: extract changelog into release notes for both pre-release and auto-release
Adds a step after release creation that reads CHANGELOG.md, extracts
the latest version section, and PATCHes it onto the Gitea release body.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:28:41 -05:00
gitea-actions[bot] 0a5d43e12b chore: update development channel 02.33.01-dev [skip ci] 2026-06-04 17:24:49 +00:00
Jonathan Miller 92b32dd924 fix: remove tpl_mokoonyx from package manifest
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
MokoOnyx is a separate repo with its own release pipeline.
Bundling it as a submodule causes "Install path does not exist"
because the build doesn't create the ZIP correctly from submodule content.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:23:37 -05:00
jmiller 0d96174f75 chore: shorten update server name [skip ci] 2026-06-04 17:20:12 +00:00
Jonathan Miller 27959a0afe chore: shorten update server name to Package - MokoWaaS [skip ci] 2026-06-04 12:19:26 -05:00
jmiller 6acae6d20f chore: update server pretty name [skip ci] 2026-06-04 17:15:56 +00:00
Jonathan Miller 9a8b3b53fc Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
# Conflicts:
#	updates.xml
2026-06-04 12:15:35 -05:00
Jonathan Miller eae734afca chore: update server pretty name - MokoWaaS - Moko Consulting Admin Tools Suite
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:15:05 -05:00
gitea-actions[bot] 1a42a71852 chore: update development channel 02.33.01-dev [skip ci] 2026-06-04 17:05:14 +00:00
Jonathan Miller 3976ce78c3 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
2026-06-04 12:04:52 -05:00
Jonathan Miller 9c4d9f060e chore: remove dev-release.yml, pre-release.yml handles dev builds [skip ci] 2026-06-04 12:04:39 -05:00
gitea-actions[bot] dfa38b6e0e chore(release): build 02.33.01-dev [skip ci] 2026-06-04 17:04:29 +00:00
Jonathan Miller c862a01a0f fix: dev-release stability value is 'dev' not 'development' [skip ci] 2026-06-04 12:04:10 -05:00
jmiller 9dacc01a67 chore: recreate updates.xml [skip ci] 2026-06-04 16:59:51 +00:00
Jonathan Miller e76248a1c9 chore: recreate updates.xml with stable and development entries
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 11:59:42 -05:00
Jonathan Miller d30f8eb0db Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
2026-06-04 11:30:53 -05:00
Jonathan Miller ef654ad3fc ci: add dev-release workflow for pre-release builds from dev
Manual dispatch workflow that builds package from dev branch and
uploads to "development" release tag. No static updates.xml —
MokoGitea generates the update feed dynamically.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 11:30:42 -05:00
gitea-actions[bot] c7d914f786 fix: ensure all pre-releases marked prerelease=true [skip ci] 2026-06-04 16:10:48 +00:00
jmiller 729aa3850d chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:57:59 +00:00
jmiller 6b9a0867ac chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:56:13 +00:00
jmiller f6c73c4f82 chore: standardize updateservers URL [skip ci] 2026-06-04 15:48:59 +00:00
Jonathan Miller b24e4e097b Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 9s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 24s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 11s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
# Conflicts:
#	.mokogitea/manifest.xml
#	.mokogitea/workflows/issue-branch.yml
#	CHANGELOG.md
#	CODE_OF_CONDUCT.md
#	GOVERNANCE.md
#	LICENSE.md
#	README.md
#	SECURITY.md
#	docs/guides/build-guide.md
#	docs/guides/configuration-guide.md
#	docs/guides/installation-guide.md
#	docs/guides/operations-guide.md
#	docs/guides/rollback-and-recovery-guide.md
#	docs/guides/testing-guide.md
#	docs/guides/troubleshooting-guide.md
#	docs/guides/upgrade-and-versioning-guide.md
#	docs/index.md
#	docs/plugin-basic.md
#	docs/update-server.md
#	src/packages/com_mokowaas/mokowaas.xml
#	src/packages/mod_mokowaas_cpanel/mod_mokowaas_cpanel.xml
#	src/packages/plg_system_mokowaas/Extension/MokoWaaS.php
#	src/packages/plg_system_mokowaas/Field/AllowedIpsField.php
#	src/packages/plg_system_mokowaas/Field/CopyableTokenField.php
#	src/packages/plg_system_mokowaas/Field/CurrentIpField.php
#	src/packages/plg_system_mokowaas/Field/DemoTaskInfoField.php
#	src/packages/plg_system_mokowaas/Field/NextResetField.php
#	src/packages/plg_system_mokowaas/Field/SnapshotTablesField.php
#	src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
#	src/packages/plg_system_mokowaas/Service/ContentSyncService.php
#	src/packages/plg_system_mokowaas/Service/DemoResetService.php
#	src/packages/plg_system_mokowaas/mokowaas.xml
#	src/packages/plg_system_mokowaas/script.php
#	src/packages/plg_system_mokowaas/services/provider.php
#	src/packages/plg_system_mokowaas_devtools/mokowaas_devtools.xml
#	src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml
#	src/packages/plg_system_mokowaas_monitor/mokowaas_monitor.xml
#	src/packages/plg_system_mokowaas_tenant/mokowaas_tenant.xml
#	src/packages/plg_task_mokowaasdemo/mokowaasdemo.xml
#	src/packages/plg_task_mokowaassync/mokowaassync.xml
#	src/packages/plg_webservices_mokowaas/mokowaas.xml
#	src/packages/plg_webservices_perfectpublisher/perfectpublisher.xml
#	src/packages/plg_webservices_perfectpublisher/services/provider.php
#	src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
#	src/pkg_mokowaas.xml
2026-06-04 10:43:55 -05:00
Jonathan Miller 6fa3f4fa82 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 27s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 28s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
2026-06-04 10:41:30 -05:00
Jonathan Miller e01167f679 fix: preflight ALTER for #__extensions.element default (strict mode)
Joomla 6 + MySQL STRICT_TRANS_TABLES: the element column is NOT NULL
with no default. Package installer creates placeholder rows before
processing sub-extension manifests, causing INSERT failures. preflight()
adds DEFAULT '' so the INSERT succeeds.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:41:26 -05:00
jmiller d4176836a5 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:40:27 +00:00
jmiller 375d11c199 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:38:22 +00:00
jmiller ef9d98ea04 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:31:48 +00:00
Jonathan Miller 6d29e9a853 fix: migrate MokoJoomTOS settings before retiring the plugin
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Swap order: migrateStandalonePlugins() now runs before
removeRetiredExtensions() so TOS params are copied to
mokowaas_offline before the old plugin is deleted.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:31:43 -05:00
Jonathan Miller fea6ae9f0a Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 10:30:19 -05:00
Jonathan Miller 3f69fe6fc1 feat: retire MokoJoomTOS, MokoATS-Automation, MokoDPCalendarAPI, MokoGalleryCalendar
Add to removeRetiredExtensions(): uninstalls plugins, removes update sites,
and deletes files on MokoWaaS install/update. These features are now built
into MokoWaaS directly.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:30:09 -05:00
jmiller 5d303287c0 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:28:49 +00:00
jmiller ffecdc4796 chore: remove updates.xml [skip ci] 2026-06-04 15:27:15 +00:00
Jonathan Miller b32a7c12e7 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
# Conflicts:
#	updates.xml
2026-06-04 10:21:17 -05:00
Jonathan Miller 64eade2589 chore: remove update-server workflow and static updates.xml, update changelog
- Remove update-server.yml — MokoGitea generates update feed dynamically
- Remove static updates.xml — no longer needed
- Fix cache cleaner CSRF: send token as POST FormData
- Update CHANGELOG.md with all 02.33.00 changes

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:20:54 -05:00
jmiller f1dbc10e4d chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:18:39 +00:00
jmiller 1289ef81b2 chore: sync updates.xml from development [skip ci] 2026-06-04 15:15:36 +00:00
gitea-actions[bot] 81781e393d chore: update development channel 02.32.52 [skip ci] 2026-06-04 15:15:35 +00:00
gitea-actions[bot] bd403e4617 chore(version): auto-bump 02.32.52 [skip ci] 2026-06-04 15:15:34 +00:00
Jonathan Miller 7c6d8a1b65 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 33s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 10:15:17 -05:00
Jonathan Miller 31e1843fe1 feat: migrate all Moko update server URLs, fix menu icons
- Add migrateUpdateServerUrls() to postflight: rewrites /raw/branch/main/updates.xml to /updates.xml for all Moko extensions
- Fix Helpdesk icon to fa-handshake-angle, .htaccess to fa-solid fa-file-code (unmapped in joomla-fontawesome.css)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:15:07 -05:00
jmiller 1ab8230191 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:13:19 +00:00
jmiller 070df8982b chore: sync updates.xml from development [skip ci] 2026-06-04 14:53:06 +00:00
gitea-actions[bot] 07fd04d27e chore: update development channel 02.32.51 [skip ci] 2026-06-04 14:53:06 +00:00
gitea-actions[bot] 8d4a302730 chore(version): auto-bump 02.32.51 [skip ci] 2026-06-04 14:53:04 +00:00
Jonathan Miller 2fbaf09e88 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 09:52:49 -05:00
Jonathan Miller 36082bd2e3 fix: cache button native Atum markup, tickets unassigned HTML encoding
- Cache cleaner button uses native header-item-content/header-item-icon markup
- Fix <em>Unassigned</em> showing raw HTML in tickets list (escape only non-null values)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:52:41 -05:00
gitea-actions[bot] 99179ad245 chore: sync pre-release workflow — auto RC on PR to main [skip ci] 2026-06-04 14:45:41 +00:00
jmiller 0fccd3f1a4 chore: sync updates.xml from development [skip ci] 2026-06-04 14:39:03 +00:00
gitea-actions[bot] 3bc1e66acf chore: update development channel 02.32.50 [skip ci] 2026-06-04 14:39:02 +00:00
gitea-actions[bot] dcf115e572 chore(version): auto-bump 02.32.50 [skip ci] 2026-06-04 14:39:00 +00:00
Jonathan Miller 75f73b0dff Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 09:38:31 -05:00
Jonathan Miller 30a6f6607a fix: add fixMenuIcons to postflight for submenu icon params
Joomla only renders img column icons for level-1 menu items. Level 2+
need menu_icon in the params JSON. This runs on every install/update.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:38:05 -05:00
jmiller ef873bda3b chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:23:05 +00:00
gitea-actions[bot] a2006c2287 chore: update development channel 02.32.49 [skip ci] 2026-06-04 14:21:50 +00:00
jmiller 3243ecba4a chore: sync updates.xml from development [skip ci] 2026-06-04 14:21:50 +00:00
gitea-actions[bot] 0552c0a0b0 chore(version): auto-bump 02.32.49 [skip ci] 2026-06-04 14:21:48 +00:00
Jonathan Miller 8de7b473a8 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 09:21:32 -05:00
Jonathan Miller 130aa26f27 fix: admin menu native classes, sys.ini to global language dir for menu icons
- Rewrite mod_mokowaas_menu template to use native MetisMenu classes (item, has-arrow, mm-collapse, mm-active, sidebar-item-title) — no custom CSS/JS
- Add <languages> element to component manifest so sys.ini deploys to administrator/language/en-GB/ for menu title translation

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:21:14 -05:00
jmiller 3f6a7af83e chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:20:17 +00:00
jmiller b8083203e9 chore: sync updates.xml from development [skip ci] 2026-06-04 14:10:24 +00:00
gitea-actions[bot] 290fc0fb99 chore: update development channel 02.32.48 [skip ci] 2026-06-04 14:10:24 +00:00
gitea-actions[bot] de7a945470 chore(version): auto-bump 02.32.48 [skip ci] 2026-06-04 14:10:22 +00:00
Jonathan Miller 7d9dbe702b Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 09:09:56 -05:00
Jonathan Miller 1819fa276c feat: collapsible admin menu, cache cleaner status module, update server URL
- Admin menu module now collapses by default, auto-expands on MokoWaaS pages (CB-style)
- Add mod_mokowaas_cache: one-click cache cleaner button in admin status bar (replaces Regular Labs Cache Cleaner)
- clearCache now purges all cache files (not just expired), matching RL behavior
- Update server URL changed to https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml
- Add setupCacheModule() to package install script

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:09:38 -05:00
jmiller 31a4d12ceb chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-04 13:46:52 +00:00
jmiller 9fedffe570 chore: sync updates.xml from development [skip ci] 2026-06-04 12:55:58 +00:00
gitea-actions[bot] 8903af5d7f chore: update development channel 02.32.47 [skip ci] 2026-06-04 12:55:57 +00:00
gitea-actions[bot] 1c7738e276 chore(version): auto-bump 02.32.47 [skip ci] 2026-06-04 12:55:56 +00:00
Jonathan Miller 234c6037c0 feat: database tools, cache cleanup, admin menu update, license key warning moved to postflight
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 13s
Update Server / Update Server (push) Successful in 25s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m13s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
- Add database tools view (view=database) with table status, optimize, repair, session purge (#127)
- Add cache cleanup view (view=cleanup) with directory size reporting and one-click cleanup (#128)
- Add Database Tools and Cache Cleanup links to admin sidebar menu module
- Add MokoWaaS-specific update badge (blue) separate from other updates in cpanel module
- Add SSL certificate expiry monitoring to cpanel module (#148)
- Move license key warning from every-page onAfterRoute to package postflight (install/update only)
- Add privacy and waflog menu entries to component manifest submenu

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:55:16 -05:00
Jonathan Miller 055562b06a fix: merge + stash pop [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:38:21 -05:00
Jonathan Miller f057f0ba86 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev 2026-06-04 07:38:18 -05:00
Jonathan Miller 500644bc8d fix: license key warning shows on every admin page load, non-dismissable
Restored warnMissingLicenseKey() in onAfterRoute. No session caching —
the warning fires on every admin page load if no download key (dlid)
is configured in the MokoWaaS update site. Only shown to master users.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:36:57 -05:00
jmiller 47e3802293 chore: sync updates.xml from development [skip ci] 2026-06-04 12:24:44 +00:00
gitea-actions[bot] a30db55024 chore: update development channel 02.32.46 [skip ci] 2026-06-04 12:24:44 +00:00
gitea-actions[bot] 53dec689b3 chore(version): auto-bump 02.32.46 [skip ci] 2026-06-04 12:24:42 +00:00
Jonathan Miller 861086bf33 fix: code review — 10 security and quality fixes
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 57s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
#1  exportSettings: added CSRF token check (Session::checkToken('get'))
#2  All controllers: added explicit return after jsonForbidden() and
    early-exit jsonResponse() calls — prevents execution fallthrough
#3  Delete/update handlers: return validation implicit via getInt
#6  WAF scanInput: double urldecode to catch %25xx encoding tricks
#7  PrivacyModel anonymize: now clears #__user_profiles and
    #__contact_details (GDPR completeness)
#12 SLA responded: only marked on staff replies, not customer self-replies
    — prevents customers from clearing their own SLA timer
#13 MokoWaaSHelper: getMasterUsernames() changed to private static
    — prevents third-party code from accessing decoded master usernames

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:24:33 -05:00
Jonathan Miller ca2160d42f chore: remove duplicate <version> tags from 3 manifests (#120) [skip ci]
CI auto-bump was inserting duplicate version lines. Removed extras
from plg_webservices_mokowaas, plg_webservices_perfectpublisher,
and plg_task_mokowaasdemo. All 14 extensions now at 02.32.45.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:15:11 -05:00
Jonathan Miller d193d0992e Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m6s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 07:09:09 -05:00
Jonathan Miller 0620ffd735 feat: expanded automation — Joomla event triggers, create_ticket action, behavior options (#151)
New trigger events hooked into core plugin:
- user_login — fires on successful Joomla login
- user_register — fires on new user creation
- user_login_failed — fires on failed login attempt

New action type: create_ticket with behavior options:
- append: add reply to existing open ticket (same user+category)
- always_new: always create a new ticket
- skip_if_open: do nothing if open ticket exists

New method: runSystemEventAutomation() for non-ticket events
that builds a virtual context object from event data.

Automation rules can now create tickets from any system event,
with intelligent deduplication to avoid ticket spam.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:08:59 -05:00
jmiller 76fe9ba311 chore: sync updates.xml from development [skip ci] 2026-06-04 12:05:57 +00:00
gitea-actions[bot] 0b49a959f4 chore: update development channel 02.32.45 [skip ci] 2026-06-04 12:05:56 +00:00
gitea-actions[bot] 72e5e31a31 chore(version): auto-bump 02.32.45 [skip ci] 2026-06-04 12:05:53 +00:00
Jonathan Miller 1389c26895 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 8s
Update Server / Update Server (push) Successful in 21s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 40s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 07:05:33 -05:00
Jonathan Miller 69776d9b77 feat: MokoWaaS admin sidebar menu module (like CB's mod_cbadmin)
New module mod_mokowaas_menu renders a dedicated MokoWaaS section
in the admin sidebar at position=menu, ordering=0 (before CB at 1
and Joomla's mod_menu at 2).

Menu items: Dashboard, Helpdesk, Extensions, .htaccess Maker,
Privacy Guard, WAF Log, Feature Plugins

Active state highlighting on current view.
Auto-created on package install with access=Special.
Added to package manifest.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:05:27 -05:00
jmiller 806a798b87 chore: sync updates.xml from development [skip ci] 2026-06-04 12:02:13 +00:00
gitea-actions[bot] 6f7495703c chore: update development channel 02.32.44 [skip ci] 2026-06-04 12:02:12 +00:00
gitea-actions[bot] 9cb49ec4b9 chore(version): auto-bump 02.32.44 [skip ci] 2026-06-04 12:02:11 +00:00
Jonathan Miller ade768b94c Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m9s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 07:01:55 -05:00
Jonathan Miller d3561dd5c9 feat: Part 1 batch 2 — helpdesk admin UIs (#137, #138, #139)
Ticket Categories admin (view=categories):
- Inline editable table with SLA times, auto-assign user
- Add/save/delete with AJAX
- SLA response + resolution minutes per category

Canned Responses admin (view=canned):
- Card list with title + preview
- Modal form for new responses with category filter
- Delete with AJAX

Automation Rules admin (view=automation):
- Card list with conditions + actions displayed inline
- Enable/disable toggle per rule
- Modal form with trigger, conditions JSON, actions JSON
- Delete with AJAX

Controller tasks: saveCategory, deleteCategory, saveCanned,
deleteCanned, saveAutomation, deleteAutomation, toggleAutomation

All ACL-protected (tickets ACL for categories/canned, core.admin
for automation rules).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:01:46 -05:00
jmiller d899bf945e chore: sync updates.xml from development [skip ci] 2026-06-04 11:44:54 +00:00
gitea-actions[bot] 6ca195fd9f chore: update development channel 02.32.43 [skip ci] 2026-06-04 11:44:54 +00:00
gitea-actions[bot] abd7a4a35e chore(version): auto-bump 02.32.43 [skip ci] 2026-06-04 11:44:52 +00:00
Jonathan Miller 788c516fd6 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 32s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-04 06:44:37 -05:00
Jonathan Miller 2919722dab feat: Part 1 batch — security headers, auto-ban, SSL monitor, IP display, settings export
#118 Display current IP on dashboard info bar
#124 HTTP security headers at runtime (X-Frame, X-Content-Type, X-XSS,
     Referrer-Policy, HSTS, CSP, Permissions-Policy) — new firewall
     fieldset with per-header toggles
#143 WAF auto-ban: threshold + window params, auto-adds to IP blocklist
     after N blocks in M minutes
#148 SSL certificate expiry monitoring in cpanel module (green/yellow/red
     badge with days remaining)
#132 Settings import/export — export all plugin + component params as
     JSON, import on another site

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 06:44:14 -05:00
jmiller 6892b6ac44 chore: sync updates.xml from development [skip ci] 2026-06-04 04:42:50 +00:00
gitea-actions[bot] 46a9701b62 chore: update development channel 02.32.42 [skip ci] 2026-06-04 04:42:49 +00:00
gitea-actions[bot] 4b4d5c714b chore(version): auto-bump 02.32.42 [skip ci] 2026-06-04 04:42:48 +00:00
Jonathan Miller 645fbc66c6 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-03 23:42:29 -05:00
Jonathan Miller 8f936fc92c feat: WAF log viewer with filters, one-click ban, and purge (#144)
Admin view at MokoWaaS > WAF Log:

- Rule distribution cards showing block counts per shield
- Filterable log table: rule, IP, search (URI/detail/UA), date range
- Pagination (50 per page)
- One-click IP ban from any log entry or top IPs sidebar
- Top 10 blocked IPs sidebar with ban buttons
- Purge old logs (configurable days)
- Color-coded rule badges (sqli=red, xss=red, mua=yellow, etc.)

WaflogModel:
- getLogs with filters + pagination
- getTotal for page count
- getRuleCounts for distribution cards
- getTopIps for sidebar
- getRuleNames for filter dropdown
- purgeLogs(days) with affected count
- banIp adds to firewall plugin IP blocklist params

ACL: core.admin (Super Users only)
Submenu: MokoWaaS > WAF Log

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-03 23:42:18 -05:00
jmiller 3a1fc7e4ac chore: sync updates.xml from development [skip ci] 2026-06-03 16:54:20 +00:00
gitea-actions[bot] d22d470aa2 chore: update development channel 02.32.41 [skip ci] 2026-06-03 16:54:19 +00:00
gitea-actions[bot] 8cd80ae7d2 chore(version): auto-bump 02.32.41 [skip ci] 2026-06-03 16:54:18 +00:00
Jonathan Miller 6a4f81dd32 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-03 11:54:02 -05:00
Jonathan Miller dd20e42cb2 feat: Privacy Guard - data compliance, consent, retention (#150)
Admin Privacy Dashboard (view=privacy):
- Data subject requests list with approve/deny actions
- Retention policies table with active status
- Summary cards (pending, total, consent entries, policies)
- Export user data as JSON download
- ACL: core.admin only

PrivacyModel:
- createRequest/processRequest for export/delete/anonymize
- exportUserData: profile, articles, action logs, tickets, replies,
  consent history, Community Builder profile
- anonymizeUserData: replace PII, block account, clear logs
- deleteUserData: full hard delete (anonymize first, then remove)
- logConsent/getUserConsent: consent tracking
- enforceRetentionPolicies: action_logs, waf_logs, sessions,
  inactive_users, closed_tickets (scheduled task ready)
- getDashboardSummary

Frontend Self-Service (/index.php?option=com_mokowaas&view=privacy):
- Download My Data, Anonymize, Delete Account buttons
- Request history table
- Consent history table
- Login required

Database tables:
- #__mokowaas_consent_log
- #__mokowaas_data_requests
- #__mokowaas_retention_policies (5 defaults)

Submenu: MokoWaaS > Privacy Guard

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-03 11:53:54 -05:00
jmiller 63fb1339b8 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 09:36:51 +00:00
jmiller c2a90265d2 chore: sync updates.xml from development [skip ci] 2026-06-03 03:52:43 +00:00
gitea-actions[bot] 53fe8c08a9 chore: update development channel 02.32.40 [skip ci] 2026-06-03 03:52:42 +00:00
gitea-actions[bot] 5a274f844c chore(version): auto-bump 02.32.40 [skip ci] 2026-06-03 03:52:41 +00:00
Jonathan Miller 4c728ef7b6 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 22:52:28 -05:00
Jonathan Miller 79bc17912a feat: helpdesk email notifications (#135)
NotificationService with dual recipient support:
- Joomla user IDs (looks up email from #__users)
- Raw email addresses (comma-separated)
- Admin emails + admin user IDs from component params
- Per-event smart routing (creator, assignee, admins)

Events:
- ticket_created → admin emails + assigned user
- ticket_replied → creator + assigned user (skip internal notes)
- status_changed → creator (includes old/new status)
- ticket_assigned → newly assigned user

Email format: plain text with ticket details + view link

Automation engine: added send_email action type for rules
to send emails to specific addresses.

Config stored in com_mokowaas params.notifications:
  admin_emails: comma-separated email addresses
  admin_user_ids: comma-separated Joomla user IDs

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 22:52:21 -05:00
jmiller 236a148d42 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 03:10:33 +00:00
jmiller c9889d4abe chore: sync updates.xml from development [skip ci] 2026-06-03 03:09:34 +00:00
gitea-actions[bot] bc22f33a0c chore: update development channel 02.32.39 [skip ci] 2026-06-03 03:09:34 +00:00
gitea-actions[bot] 755954425e chore(version): auto-bump 02.32.39 [skip ci] 2026-06-03 03:09:32 +00:00
Jonathan Miller 92cbcfeefd Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 22:09:13 -05:00
Jonathan Miller aab196c26b fix: hide import buttons after successful import + fix Factory class error
Import markers stored in com_mokowaas component params (imported_admintools,
imported_ats). checkAdminToolsAvailable() and checkAtsAvailable() return null
if already imported, hiding the dashboard banner and ticket list button.

Also fixed missing 'use Joomla\CMS\Factory' in admin tickets template.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 22:03:54 -05:00
jmiller d306b01260 chore: sync updates.xml from development [skip ci] 2026-06-03 00:29:37 +00:00
jmiller 7e2476b250 chore: sync updates.xml from development [skip ci] 2026-06-02 23:48:59 +00:00
jmiller 6b195d0514 chore: sync updates.xml from development [skip ci] 2026-06-02 23:46:32 +00:00
Moko Consulting fc1f3dd903 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:33:03 +00:00
Moko Consulting 4f1b9ac3f2 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:33:02 +00:00
Moko Consulting 188defdf1b chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 32s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:33:00 +00:00
jmiller 3b972efcdc chore: sync updates.xml from development [skip ci] 2026-06-02 20:51:48 +00:00
jmiller 2fd3f04f79 chore: sync updates.xml from development [skip ci] 2026-06-02 20:47:40 +00:00
jmiller 2b1bbb9c94 chore: sync updates.xml from development [skip ci] 2026-06-02 20:33:50 +00:00
jmiller 34cf1235c2 chore: sync updates.xml from development [skip ci] 2026-06-02 20:31:16 +00:00
jmiller a8341d456d chore: sync updates.xml from development [skip ci] 2026-06-02 20:25:39 +00:00
jmiller d49fdd24fc chore: sync updates.xml from development [skip ci] 2026-06-02 19:59:15 +00:00
jmiller 47db66b70b chore: sync updates.xml from development [skip ci] 2026-06-02 19:52:40 +00:00
jmiller ca9ef82caf chore: sync updates.xml from development [skip ci] 2026-06-02 19:34:30 +00:00
jmiller b7057745a3 chore: sync updates.xml from development [skip ci] 2026-06-02 19:31:59 +00:00
jmiller 6d3eaa4471 chore: sync updates.xml from development [skip ci] 2026-06-02 19:14:31 +00:00
jmiller 4237740d32 chore: sync updates.xml from development [skip ci] 2026-06-02 19:09:25 +00:00
jmiller f79dc2a26e chore: sync updates.xml from development [skip ci] 2026-06-02 19:06:01 +00:00
jmiller 903999a262 chore: sync updates.xml from development [skip ci] 2026-06-02 18:54:55 +00:00
gitea-actions[bot] d4514aa37d chore: update channels for 02.33.00 [skip ci] 2026-06-02 18:53:36 +00:00
gitea-actions[bot] 723f25bb59 chore(release): build 02.33.00 [skip ci] 2026-06-02 18:53:35 +00:00
jmiller 1522416287 Merge pull request 'chore(release): stable release' (#134) from dev into main
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
chore(release): stable release
2026-06-02 18:53:23 +00:00
121 changed files with 6474 additions and 1801 deletions
-4
View File
@@ -1,4 +0,0 @@
[submodule "src/packages/tpl_mokoonyx"]
path = src/packages/tpl_mokoonyx
url = https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
branch = main
+1 -1
View File
@@ -9,7 +9,7 @@
<display-name>Package - MokoWaaS</display-name>
<org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.32.38</version>
<version>02.34.08</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+6 -9
View File
@@ -48,15 +48,12 @@ jobs:
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
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Bump version
run: |
+316 -283
View File
@@ -1,283 +1,316 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
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 }}
permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
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
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: Rename branch to rc
run: |
php /tmp/moko-platform-api/cli/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_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 "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
# Ensure PHP + Composer are available
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
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
- name: "Publish stable release"
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
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="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_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
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
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](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +========================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, closed]
branches:
- main
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
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 }}
permissions:
contents: write
jobs:
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
promote-rc:
name: Promote to RC
runs-on: release
if: >-
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
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
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Rename branch to rc
run: |
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_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 "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_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 }}" \
--skip-update-stream
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found - aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
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
fi
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: "Publish stable release"
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Update release notes from CHANGELOG.md
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Stable release"
else
NOTES="Stable release"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
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="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_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
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral - created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released - ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
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](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 02.32.38
# VERSION: 02.34.08
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+255
View File
@@ -147,6 +147,98 @@ jobs:
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Joomla JEXEC guard check
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# 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
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "JEXEC guard: OK"
- name: Joomla directory listing protection
if: steps.platform.outputs.platform == 'joomla'
run: |
MISSING=0
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && exit 0
while IFS= read -r dir; do
if [ ! -f "${dir}/index.html" ]; then
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
MISSING=$((MISSING + 1))
fi
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
if [ "$MISSING" -gt 0 ]; then
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
fi
echo "Directory protection: ${MISSING} missing (advisory)"
- name: Joomla script file and asset checks
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && exit 0
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check scriptfile exists if declared
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
if [ -n "$SCRIPTFILE" ]; then
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
ERRORS=$((ERRORS + 1))
else
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
fi
fi
# Require joomla.asset.json and validate it
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ASSET_JSON" ]; then
echo "::error::joomla.asset.json not found — Joomla asset system is required"
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
echo "::error::joomla.asset.json is not valid JSON"
ERRORS=$((ERRORS + 1))
}
fi
echo "joomla.asset.json: valid"
fi
# Validate all XML files in src/ are well-formed
XML_ERRORS=0
if command -v php &> /dev/null; then
while IFS= read -r -d '' xmlfile; do
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
XML_ERRORS=$((XML_ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
fi
if [ "$XML_ERRORS" -gt 0 ]; then
echo "::error::${XML_ERRORS} XML file(s) are malformed"
ERRORS=$((ERRORS + 1))
else
echo "XML well-formedness: OK"
fi
[ "$ERRORS" -gt 0 ] && exit 1
echo "Joomla asset checks: OK"
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
@@ -164,6 +256,13 @@ 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)
if [ -n "$RAW_URLS" ]; then
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS"
exit 1
fi
echo "Joomla manifest valid"
;;
dolibarr)
@@ -196,6 +295,162 @@ jobs:
;;
esac
- name: Check changelog has unreleased entries (PRs to main)
if: github.base_ref == 'main'
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::error::CHANGELOG.md not found — required for releases"
exit 1
fi
# Extract content between [Unreleased] and next ## heading
ENTRIES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found && /^- /{count++} END{print count+0}' CHANGELOG.md)
if [ "$ENTRIES" -eq 0 ]; then
echo "::error::CHANGELOG.md has no entries under [Unreleased]. Add changelog entries before releasing."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "No entries found under \`[Unreleased]\` in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
echo "Add entries describing what changed before merging to main." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${ENTRIES} unreleased entries found"
echo "## Changelog Check: Passed" >> $GITHUB_STEP_SUMMARY
echo "${ENTRIES} entries under [Unreleased]" >> $GITHUB_STEP_SUMMARY
- name: Validate Joomla language files
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
WARNINGS=0
# Require both en-GB and en-US language directories
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$LANG_ROOT" ]; then
echo "No language/ directory found — skipping"
exit 0
fi
if [ ! -d "$LANG_ROOT/en-GB" ]; then
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
ERRORS=$((ERRORS + 1))
fi
if [ ! -d "$LANG_ROOT/en-US" ]; then
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
ERRORS=$((ERRORS + 1))
fi
# Check that en-GB and en-US have matching .ini files
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
[ ! -f "$GB_INI" ] && continue
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
if [ ! -f "$US_INI" ]; then
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
ERRORS=$((ERRORS + 1))
fi
done
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
[ ! -f "$US_INI" ] && continue
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
if [ ! -f "$GB_INI" ]; then
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
ERRORS=$((ERRORS + 1))
fi
done
fi
# Find all .ini language files
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
if [ -z "$INI_FILES" ]; then
echo "No .ini language files found"
[ "$ERRORS" -gt 0 ] && exit 1
exit 0
fi
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
for FILE in $INI_FILES; do
FNAME=$(basename "$FILE")
LINENUM=0
SEEN_KEYS=""
while IFS= read -r line || [ -n "$line" ]; do
LINENUM=$((LINENUM + 1))
# Skip empty lines and comments
[ -z "$line" ] && continue
echo "$line" | grep -qE '^\s*;' && continue
echo "$line" | grep -qE '^\s*$' && continue
# Must match KEY="VALUE" format
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
ERRORS=$((ERRORS + 1))
continue
fi
# Extract key and check for duplicates
KEY=$(echo "$line" | sed 's/=.*//')
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
ERRORS=$((ERRORS + 1))
fi
SEEN_KEYS="${SEEN_KEYS}
${KEY}"
done < "$FILE"
echo " ${FILE}: checked ${LINENUM} lines"
done
# Cross-check en-GB vs en-US key consistency
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
for GB_FILE in "$GB_DIR"/*.ini; do
[ ! -f "$GB_FILE" ] && continue
FNAME=$(basename "$GB_FILE")
US_FILE="$US_DIR/$FNAME"
[ ! -f "$US_FILE" ] && continue
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
# Keys in en-GB but not en-US
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_US" ]; then
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
# Keys in en-US but not en-GB
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_GB" ]; then
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
done
fi
{
echo "### Language File Validation"
echo "| Metric | Count |"
echo "|---|---|"
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
echo "| Errors | ${ERRORS} |"
echo "| Warnings | ${WARNINGS} |"
} >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "::error::Language validation failed with ${ERRORS} error(s)"
exit 1
fi
echo "Language files: OK (${WARNINGS} warning(s))"
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
+74 -65
View File
@@ -7,7 +7,7 @@
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 09.23.00
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
@@ -17,6 +17,10 @@ on:
types: [closed]
branches:
- dev
pull_request_target:
types: [synchronize, opened, reopened]
branches:
- main
workflow_dispatch:
inputs:
stability:
@@ -43,7 +47,8 @@ jobs:
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
steps:
- name: Checkout
@@ -51,7 +56,7 @@ jobs:
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
submodules: recursive
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
- name: Setup moko-platform tools
env:
@@ -61,7 +66,6 @@ jobs:
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
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
@@ -77,24 +81,40 @@ jobs:
- name: Resolve metadata and bump version
id: meta
run: |
STABILITY="${{ inputs.stability || 'development' }}"
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
STABILITY="release-candidate"
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) TAG="development" ;;
alpha) TAG="alpha" ;;
beta) TAG="beta" ;;
release-candidate) TAG="release-candidate" ;;
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Set stability suffix, bump preserves it, fix consistency
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
--branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Read final version (includes suffix, e.g. 01.02.15-dev)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
@@ -119,11 +139,12 @@ jobs:
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
@@ -136,6 +157,41 @@ jobs:
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Update release notes from CHANGELOG.md
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_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
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
run: |
@@ -147,55 +203,8 @@ jobs:
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
# updates.xml is generated dynamically by MokoGitea license server
# No need to build, commit, or sync updates.xml from workflows
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
+9 -115
View File
@@ -11,7 +11,7 @@
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: "Generic: Repo Health"
@@ -24,13 +24,12 @@ on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, release, scripts, or repo'
description: 'Validation profile: all, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- release
- scripts
- repo
pull_request:
@@ -40,10 +39,6 @@ permissions:
contents: read
env:
# Release policy - Repository Variables Only
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
# Scripts governance policy
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
@@ -138,101 +133,6 @@ jobs:
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
release_config:
name: Release configuration
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Guardrails release vars
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes release validation'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
missing=()
missing_optional=()
for k in "${required[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing+=("${k}")
done
for k in "${optional[@]}"; do
v="${!k:-}"
[ -z "${v}" ] && missing_optional+=("${k}")
done
{
printf '%s\n' '### Release configuration (Repository Variables)'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Variable | Status |'
printf '%s\n' '|---|---|'
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repository variables'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#missing[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repository variables'
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
{
printf '%s\n' '### Repository variables validation result'
printf '%s\n' 'Status: OK'
printf '%s\n' 'All required repository variables present.'
printf '%s\n' ''
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
scripts_governance:
name: Scripts governance
needs: access_check
@@ -256,14 +156,14 @@ jobs:
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
if [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
@@ -370,14 +270,14 @@ jobs:
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|release|scripts|repo) ;;
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
if [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
@@ -704,7 +604,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 variables | OK | Repository variables validation |'
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
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 |'
@@ -773,11 +673,10 @@ jobs:
report-issues:
name: "Report Issues"
runs-on: ubuntu-latest
needs: [access_check, release_config, scripts_governance, repo_health]
needs: [access_check, scripts_governance, repo_health]
if: >-
always() &&
(needs.release_config.result == 'failure' ||
needs.scripts_governance.result == 'failure' ||
(needs.scripts_governance.result == 'failure' ||
needs.repo_health.result == 'failure')
steps:
@@ -803,10 +702,6 @@ jobs:
fi
}
report_gate "Release Configuration" \
"${{ needs.release_config.result }}" \
"Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings."
report_gate "Scripts Governance" \
"${{ needs.scripts_governance.result }}" \
"Scripts directory policy violations detected. Review required and allowed directories."
@@ -814,4 +709,3 @@ jobs:
report_gate "Repository Health" \
"${{ needs.repo_health.result }}" \
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
-302
View File
@@ -1,302 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/update-server.yml
# VERSION: 09.23.00
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
#
# Thin wrapper around moko-platform CLI tools.
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
#
# Joomla filters update entries by the user's "Minimum Stability" setting.
name: "Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
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 }}
permissions:
contents: write
jobs:
update-xml:
name: Update Server
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
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
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: |
BRANCH="${{ github.ref_name }}"
# Configure git for bot pushes
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
else
STABILITY="development"
fi
# Gitea release tag per stability
case "$STABILITY" in
development) TAG="development" ;;
alpha) TAG="alpha" ;;
beta) TAG="beta" ;;
rc) TAG="release-candidate" ;;
*) TAG="stable" ;;
esac
# Bump patch, set platform suffix, fix consistency — version_bump preserves suffix
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
--branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Read final version (includes suffix, e.g. 01.02.15-dev)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
# Commit version bump if changed
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
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
# Build package and upload
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
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
python3 -c "
import base64, json, urllib.request, sys
with open('updates.xml', 'rb') as f:
content = base64.b64encode(f.read()).decode()
payload = json.dumps({
'content': content,
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main'
}).encode()
req = urllib.request.Request(
'${API_BASE}/contents/updates.xml',
data=payload, method='PUT',
headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# Permission check: admin or maintain role required
ACTOR="${{ github.actor }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${VERSION}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
+53 -18
View File
@@ -14,12 +14,58 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md
VERSION: 02.32.38
VERSION: 02.34.08
BRIEF: Version history using `Keep a Changelog`
-->
# Changelog
## [02.32.00] - 2026-06-02
## [Unreleased]
### Added
- Database Tools view — table status, optimize, repair, session purge (#127)
- Cache Cleanup view — directory size reporting and one-click cleanup (#128)
- mod_mokowaas_cache — one-click cache cleaner button in admin status bar (replaces Regular Labs Cache Cleaner)
- mod_mokowaas_menu — collapsible admin sidebar menu using native MetisMenu classes (like Community Builder)
- SSL certificate expiry monitoring in cpanel module (#148)
- MokoWaaS-specific update badge (blue) separate from other updates in cpanel module
- migrateUpdateServerUrls() — rewrites all Moko extension update server URLs to clean /updates.xml on install/update
- fixMenuIcons() — sets menu_icon params on submenu items (Joomla only renders img on level 1)
- setupCacheModule() — registers cache cleaner module in status bar position on install
- Component config.xml for Joomla Options modal (#149)
- preflight() ALTER for #__extensions.element default (MySQL strict mode fix)
- Retire MokoJoomTOS, MokoATS-Automation, MokoDPCalendarAPI, MokoGalleryCalendar on install
- MokoJoomTOS settings auto-migrate to mokowaas_offline before removal
- dev-release and pre-release workflows with changelog extraction into release notes
- RC pre-release consolidates dev patches into clean minor version bump
### Changed
- Move security hardening methods (protectPlugin, ensureProtectedFlag, isOurExtension) from core plugin to firewall plugin (#155)
- Admin menu module uses native Joomla MetisMenu CSS classes
- Helpdesk icon changed to fa-handshake-angle, .htaccess to fa-solid fa-file-code
- clearCache purges all cache files recursively (replaces Regular Labs Cache Cleaner behavior)
- License key warning moved from every-page onAfterRoute to package postflight only
- Update server URL changed to dynamic MokoGitea feed
- Component manifest adds `<languages>` for global language dir deployment
- Privacy and WAF Log added to component manifest submenu
- MokoOnyx template removed from package manifest (separate repo/release)
### Removed
- Static updates.xml — MokoGitea generates update feed dynamically from releases
- update-server.yml workflow — replaced by pre-release.yml
### Fixed
- Tickets list showing raw `<em>Unassigned</em>` HTML instead of italic text
- Cache cleaner CSRF failure — token now sent as POST FormData
- Admin menu icons missing for Helpdesk and .htaccess Maker
- Firewall install error "Field 'element' doesn't have a default value" (MySQL strict mode)
## [02.32] - 2026-06-02
### Added
- Admin control panel dashboard in com_mokowaas with site info bar, feature plugin grid, and quick actions
- Feature plugin architecture — MokoWaaS features split into toggleable plugins managed from the dashboard
@@ -43,7 +89,8 @@
- License key validation (licensing system not ready — will return in future release)
- Dynamic MokoGitea update feed dependency (replaced with static updates.xml)
## [02.31.00] - 2026-06-01
## [02.31] - 2026-06-01
### Added
- License key support via Joomla's native Update Sites download key system (dlid)
- Update server URL migrated from static XML to MokoGitea's dynamic update feed endpoint
@@ -76,7 +123,8 @@
- Site Aliases config tab (hardcoded to dev.{primary_domain})
- File sync (images/, files/, media/) — sync is API/DB content only
## [02.29.03] - 2026-05-31
## [02.29] - 2026-05-31
### Added
- `allow_extension_updates` param — separate update rights from installer restrictions; tenants can update extensions by default even when the installer is restricted
- Hardcoded master usernames — multiple privileged users supported with identical access
@@ -90,7 +138,6 @@
- Demo Mode with configurable warning banner on frontend when enabled
### Fixed
- Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours
- `DemoResetService` — baseline snapshot and restore for DB tables + media files
- API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string)
@@ -105,16 +152,4 @@
- Package installer: clean up legacy `mokowaasbrand` extension entries and files on install/update
- API endpoint `GET /?mokowaas=extensions` and `GET /api/v1/mokowaas/extensions` — list installed extensions with version, status, and update server info
## [02.20.00] --- 2026-05-28
## [02.20.00] --- 2026-05-28
## [02.19.00] --- 2026-05-28
## [02.18.00] --- 2026-05-28
All notable changes to the MokoWaaS plugin will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [02.20] --- 2026-05-28
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+24
View File
@@ -127,6 +127,30 @@ The version tools update all files containing version stamps:
Files synced from other repos (with a `# REPO:` header) are not touched.
## Changelog
We use [Keep a Changelog](https://keepachangelog.com/) with an `[Unreleased]` staging section.
### Rules
- All changes go under `## [Unreleased]` — this is the "current work" section
- Entries stay under `[Unreleased]` until a **stable release** merges to `main`
- On stable release, `[Unreleased]` entries are promoted to a version heading (e.g., `## [02.34] - 2026-06-10`)
- Only **minor versions** get changelog headings — patch numbers from dev are never shown
- Dev/alpha/beta/RC pre-release descriptions pull from `[Unreleased]` automatically
- **CI will block PRs to main** if `[Unreleased]` has no entries
### Categories
Use these headings under each version:
- `### Added` — new features
- `### Changed` — changes to existing functionality
- `### Deprecated` — features that will be removed
- `### Removed` — features that were removed
- `### Fixed` — bug fixes
- `### Security` — vulnerability fixes
## Code Standards
- **PHP**: PSR-12, tabs for indentation
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
-->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./LICENSE.md
VERSION: 02.32.38
VERSION: 02.34.08
BRIEF: Project license (GPL-3.0-or-later)
-->
GNU GENERAL PUBLIC LICENSE
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 02.32.38
VERSION: 02.34.08
BRIEF: Security vulnerability reporting and handling policy
-->
+2 -2
View File
@@ -11,13 +11,13 @@
INGROUP: MokoWaaS.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas
FILE: build-guide.md
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
-->
# MokoWaaS Build Guide (VERSION: 02.32.38)
# MokoWaaS Build Guide (VERSION: 02.34.08)
## 1. Purpose
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoWaaS system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
-->
# MokoWaaS Configuration Guide (VERSION: 02.32.38)
# MokoWaaS Configuration Guide (VERSION: 02.34.08)
## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoWaaS system plugin
NOTE: First document in the guide set
-->
# MokoWaaS Installation Guide (VERSION: 02.32.38)
# MokoWaaS Installation Guide (VERSION: 02.34.08)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors
-->
# MokoWaaS Operations Guide (VERSION: 02.32.38)
# MokoWaaS Operations Guide (VERSION: 02.34.08)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for WaaS plugin governance
-->
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.32.38)
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.08)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
-->
# MokoWaaS Testing Guide (VERSION: 02.32.38)
# MokoWaaS Testing Guide (VERSION: 02.34.08)
## 1. Prerequisites
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
NOTE: Designed for administrators and WaaS operations teams
-->
# MokoWaaS Troubleshooting Guide (VERSION: 02.32.38)
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.08)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
NOTE: Defines release flow, version rules, and upgrade validation
-->
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.32.38)
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.08)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.38
VERSION: 02.34.08
PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoWaaS plugin
NOTE: Automatically maintained index for all guide canvases
-->
# MokoWaaS Documentation Index (VERSION: 02.32.38)
# MokoWaaS Documentation Index (VERSION: 02.34.08)
## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: /docs/plugin-basic.md
VERSION: 02.32.38
VERSION: 02.34.08
BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoWaaS Plugin Overview (VERSION: 02.32.38)
# MokoWaaS Plugin Overview (VERSION: 02.34.08)
## Introduction
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
PATH: /docs/update-server.md
VERSION: 02.32.38
VERSION: 02.34.08
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<fieldset name="notifications" label="Email Notifications" description="Configure email recipients for ticket and security notifications.">
<field name="admin_emails" type="text" default=""
label="Admin Email Addresses"
description="Comma-separated email addresses to receive all notifications."
hint="admin@example.com, support@example.com" />
<field name="admin_user_ids" type="text" default=""
label="Admin User IDs"
description="Comma-separated Joomla user IDs to receive notifications."
hint="320, 321" />
<field name="security_alerts" type="radio" default="1"
label="Security Alerts"
description="Send email alerts for WAF blocks and admin logins."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="helpdesk" label="Helpdesk Settings" description="Default helpdesk behavior.">
<field name="default_category" type="sql" default=""
label="Default Ticket Category"
description="Category assigned to tickets without a selection."
query="SELECT id AS value, title AS text FROM #__mokowaas_ticket_categories WHERE published = 1 ORDER BY ordering" />
<field name="autoclose_days" type="number" default="7"
label="Auto-Close After (days)"
description="Resolved tickets are auto-closed after this many days. 0 = disabled." />
<field name="kb_search_enabled" type="radio" default="1"
label="KB Search on Ticket Forms"
description="Show knowledge base search before ticket submission."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="permissions" label="COM_MOKOWAAS_ACL_TITLE"
description="COM_MOKOWAAS_ACL_DESC">
<field name="rules" type="rules"
label="COM_MOKOWAAS_ACL_TITLE"
validate="rules"
filter="rules"
component="com_mokowaas"
section="component" />
</fieldset>
</config>
@@ -12,4 +12,8 @@ COM_MOKOWAAS_MENU_UPDATES="Joomla Updates"
COM_MOKOWAAS_MENU_CHECKIN="Global Check-in"
COM_MOKOWAAS_MENU_TICKETS="Helpdesk"
COM_MOKOWAAS_MENU_HTACCESS=".htaccess Maker"
COM_MOKOWAAS_MENU_PRIVACY="Privacy Guard"
COM_MOKOWAAS_MENU_WAFLOG="WAF Log"
COM_MOKOWAAS_MENU_DATABASE="Database Tools"
COM_MOKOWAAS_MENU_CLEANUP="Cache Cleanup"
COM_MOKOWAAS_MENU_CACHE="Cache Management"
@@ -85,3 +85,51 @@ INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `des
(3, 'Feature Request', 'feature-request', 'Request a new feature or enhancement', 1440, 10080, 3),
(4, 'Billing', 'billing', 'Billing, invoicing, and payment questions', 240, 1440, 4),
(5, 'Urgent / Outage', 'urgent-outage', 'Site down or critical issue', 60, 240, 5);
--
-- Privacy Guard Tables
--
CREATE TABLE IF NOT EXISTS `#__mokowaas_consent_log` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`category` VARCHAR(50) NOT NULL,
`action` ENUM('granted','revoked') NOT NULL,
`ip_address` VARCHAR(45) NOT NULL DEFAULT '',
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_data_requests` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`type` ENUM('export','delete','anonymize') NOT NULL,
`status` ENUM('pending','processing','completed','denied') NOT NULL DEFAULT 'pending',
`notes` TEXT,
`processed_by` INT DEFAULT NULL,
`created` DATETIME NOT NULL,
`processed` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_retention_policies` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`content_type` VARCHAR(100) NOT NULL,
`retention_days` INT UNSIGNED NOT NULL DEFAULT 365,
`action` ENUM('anonymize','delete','archive') NOT NULL DEFAULT 'anonymize',
`enabled` TINYINT NOT NULL DEFAULT 1,
`description` VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default retention policies
INSERT IGNORE INTO `#__mokowaas_retention_policies` (`id`, `content_type`, `retention_days`, `action`, `enabled`, `description`) VALUES
(1, 'action_logs', 90, 'delete', 1, 'Delete action log entries older than 90 days'),
(2, 'waf_logs', 30, 'delete', 1, 'Delete WAF block logs older than 30 days'),
(3, 'sessions', 7, 'delete', 1, 'Purge expired sessions older than 7 days'),
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
@@ -29,6 +29,13 @@ class DisplayController extends BaseController
'htaccess' => 'mokowaas.htaccess',
'tickets' => 'mokowaas.tickets',
'ticket' => 'mokowaas.tickets',
'privacy' => 'core.admin',
'waflog' => 'core.admin',
'categories' => 'mokowaas.tickets',
'canned' => 'mokowaas.tickets',
'automation' => 'core.admin',
'database' => 'core.admin',
'cleanup' => 'mokowaas.cache',
];
public function display($cachable = false, $urlparams = [])
@@ -58,6 +65,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.plugins.toggle'))
{
$this->jsonForbidden();
return;
}
$app = Factory::getApplication();
@@ -82,6 +90,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.cache'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Dashboard')->clearCache());
@@ -98,6 +107,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.extensions'))
{
$this->jsonForbidden();
return;
}
$downloadUrl = Factory::getApplication()->getInput()->getString('download_url', '');
@@ -105,6 +115,7 @@ class DisplayController extends BaseController
if (empty($downloadUrl))
{
$this->jsonResponse(['success' => false, 'message' => 'Missing download URL.']);
return;
}
$this->jsonResponse($this->getModel('Extensions')->installFromUrl($downloadUrl));
@@ -121,6 +132,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.htaccess'))
{
$this->jsonForbidden();
return;
}
$app = Factory::getApplication();
@@ -152,6 +164,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.htaccess'))
{
$this->jsonForbidden();
return;
}
$model = $this->getModel('Htaccess');
@@ -179,6 +192,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.tickets.create'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
@@ -198,6 +212,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.tickets'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
@@ -216,6 +231,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.tickets'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
@@ -268,6 +284,356 @@ class DisplayController extends BaseController
}
}
// ==================================================================
// Maintenance (#127, #128)
// ==================================================================
public function optimizeDb()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->optimizeTables());
}
public function repairDb()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->repairTables());
}
public function purgeSessions()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->purgeSessions());
}
public function cleanDirectory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.cache')) { $this->jsonForbidden(); return; }
$dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->cleanDirectory($dirKey));
}
// ==================================================================
// Helpdesk CRUD (#137, #138, #139)
// ==================================================================
public function saveCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$id = $input->getInt('id', 0);
$data = (object) [
'title' => $input->getString('title', ''),
'alias' => \Joomla\CMS\Filter\OutputFilter::stringURLSafe($input->getString('title', '')),
'sla_response_minutes' => $input->getInt('sla_response_minutes', 480),
'sla_resolution_minutes' => $input->getInt('sla_resolution_minutes', 2880),
'auto_assign_user' => $input->getInt('auto_assign_user', 0) ?: null,
'published' => $input->getInt('published', 1),
];
if ($id) {
$data->id = $id;
$db->updateObject('#__mokowaas_ticket_categories', $data, 'id');
} else {
$data->ordering = 0;
$db->insertObject('#__mokowaas_ticket_categories', $data, 'id');
}
$this->jsonResponse(['success' => true, 'message' => 'Category saved.', 'id' => (int) $data->id]);
}
public function deleteCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
}
public function saveCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$data = (object) [
'title' => $input->getString('title', ''),
'body' => $input->getRaw('body', ''),
'category_id' => $input->getInt('category_id', 0) ?: null,
'ordering' => 0,
];
$id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_canned', $data, 'id'); }
else { $db->insertObject('#__mokowaas_ticket_canned', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Canned response saved.', 'id' => (int) $data->id]);
}
public function deleteCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
}
public function saveAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$data = (object) [
'title' => $input->getString('title', ''),
'trigger_event' => $input->getString('trigger_event', 'ticket_created'),
'conditions' => $input->getRaw('conditions', '[]'),
'actions' => $input->getRaw('actions', '[]'),
'enabled' => 1,
'ordering' => 0,
];
$id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_automation', $data, 'id'); }
else { $db->insertObject('#__mokowaas_ticket_automation', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Rule saved.', 'id' => (int) $data->id]);
}
public function deleteAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']);
}
public function toggleAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->update('#__mokowaas_ticket_automation')
->set('enabled = ' . $input->getInt('enabled', 0))
->where('id = ' . $input->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule updated.']);
}
// ==================================================================
// Settings Import/Export (#132)
// ==================================================================
public function exportSettings()
{
Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$db = Factory::getDbo();
$settings = [];
// Export all MokoWaaS plugin params
$plugins = ['mokowaas', 'mokowaas_firewall', 'mokowaas_tenant', 'mokowaas_devtools', 'mokowaas_offline'];
foreach ($plugins as $element)
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
);
$settings['plugins'][$element] = json_decode($db->loadResult() ?? '{}', true);
}
// Export component params
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$settings['component'] = json_decode($db->loadResult() ?? '{}', true);
$settings['exported'] = gmdate('Y-m-d\TH:i:s\Z');
$settings['site'] = Factory::getConfig()->get('sitename', '');
$this->jsonResponse(['success' => true, 'settings' => $settings]);
}
public function importSettings()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$json = Factory::getApplication()->getInput()->getRaw('settings_json', '');
$data = json_decode($json, true);
if (empty($data) || empty($data['plugins']))
{
$this->jsonResponse(['success' => false, 'message' => 'Invalid settings JSON.']);
return;
}
$db = Factory::getDbo();
$count = 0;
foreach ($data['plugins'] ?? [] as $element => $params)
{
if (!is_array($params))
{
continue;
}
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute();
$count++;
}
if (!empty($data['component']) && is_array($data['component']))
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component'])))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute();
$count++;
}
$this->jsonResponse(['success' => true, 'message' => "Imported settings for {$count} extensions."]);
}
// ==================================================================
// WAF Log
// ==================================================================
public function purgeWafLog()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$days = Factory::getApplication()->getInput()->getInt('days', 30);
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
$this->jsonResponse($model->purgeLogs($days));
}
public function banIpFromLog()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$ip = Factory::getApplication()->getInput()->getString('ip', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
$this->jsonResponse($model->banIp($ip));
}
// ==================================================================
// Privacy Guard
// ==================================================================
public function processDataRequest()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$action = $input->getString('action', 'deny');
if ($action === 'create')
{
$result = $model->createRequest(
$input->getInt('user_id', 0),
$input->getString('type', 'export')
);
$this->jsonResponse($result);
return;
}
if ($action === 'approve' && !$input->getInt('request_id', 0) && $input->getInt('user_id', 0))
{
// Auto-process: create then immediately approve
$result = $model->createRequest(
$input->getInt('user_id', 0),
$input->getString('type', 'export')
);
if ($result['success'] && !empty($result['id']))
{
$result = $model->processRequest((int) $result['id'], 'approve');
}
$this->jsonResponse($result);
return;
}
$this->jsonResponse($model->processRequest(
$input->getInt('request_id', 0),
$action
));
}
public function exportUserData()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$this->jsonResponse($model->exportUserData(
Factory::getApplication()->getInput()->getInt('user_id', 0)
));
}
// ==================================================================
// Importers
// ==================================================================
@@ -279,6 +645,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('mokowaas.tickets'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Import')->importAts());
@@ -291,6 +658,7 @@ class DisplayController extends BaseController
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Import')->importAdminTools());
@@ -333,5 +701,6 @@ class DisplayController extends BaseController
private function jsonForbidden(): void
{
$this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
return;
}
}
@@ -267,13 +267,18 @@ class DashboardModel extends BaseDatabaseModel
{
try
{
$app = Factory::getApplication();
$app->get('cache_handler', 'file');
// Clear site and admin caches
// Use Joomla's native cache API — same as com_cache
$cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class);
Factory::getCache('', '')->gc();
Factory::getCache('', '', 'administrator')->gc();
$cache->createCacheController('', ['defaultgroup' => ''])->cache->clean('');
// Also clean admin cache
$conf = Factory::getApplication()->get('cache_handler', 'file');
$options = [
'defaultgroup' => '',
'cachebase' => JPATH_ADMINISTRATOR . '/cache',
'storage' => $conf,
];
$cache->createCacheController('', $options)->cache->clean('');
// Clear opcache if available
if (\function_exists('opcache_reset'))
@@ -281,7 +286,7 @@ class DashboardModel extends BaseDatabaseModel
\opcache_reset();
}
return ['success' => true, 'message' => 'Cache cleared successfully.'];
return ['success' => true, 'message' => 'All cache cleared successfully.'];
}
catch (\Throwable $e)
{
@@ -447,4 +452,84 @@ class DashboardModel extends BaseDatabaseModel
return [];
}
}
/**
* WAF blocks per day for the last 14 days.
*/
public function getWafBlocksByDay(int $days = 14): array
{
try
{
$db = $this->getDatabase();
$db->setQuery(
"SELECT DATE(" . $db->quoteName('created') . ") AS day, COUNT(*) AS total"
. " FROM " . $db->quoteName('#__mokowaas_waf_log')
. " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
. " GROUP BY day ORDER BY day"
);
$rows = $db->loadObjectList() ?: [];
// Fill in missing days with zero
$result = [];
$date = new \DateTime("-{$days} days");
$now = new \DateTime('now');
$map = [];
foreach ($rows as $r)
{
$map[$r->day] = (int) $r->total;
}
while ($date <= $now)
{
$key = $date->format('Y-m-d');
$result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0];
$date->modify('+1 day');
}
return $result;
}
catch (\Throwable $e)
{
return [];
}
}
/**
* Admin logins per day for the last 14 days.
*/
public function getLoginsByDay(int $days = 14): array
{
try
{
$db = $this->getDatabase();
$db->setQuery(
"SELECT DATE(" . $db->quoteName('log_date') . ") AS day, COUNT(*) AS total"
. " FROM " . $db->quoteName('#__action_logs')
. " WHERE " . $db->quoteName('message_language_key') . " = 'PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN'"
. " AND " . $db->quoteName('log_date') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
. " GROUP BY day ORDER BY day"
);
$rows = $db->loadObjectList() ?: [];
$result = [];
$date = new \DateTime("-{$days} days");
$now = new \DateTime('now');
$map = [];
foreach ($rows as $r)
{
$map[$r->day] = (int) $r->total;
}
while ($date <= $now)
{
$key = $date->format('Y-m-d');
$result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0];
$date->modify('+1 day');
}
return $result;
}
catch (\Throwable $e)
{
return [];
}
}
}
@@ -33,7 +33,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'package',
'icon' => 'icon-shield-alt',
'category' => 'Platform',
'article' => 'https://mokoconsulting.tech/kb/mokowaas-platform',
'article' => 'https://mokoconsulting.tech/support/products/mokowaas-platform',
'protected' => true,
],
'MokoOnyx' => [
@@ -43,7 +43,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'template',
'icon' => 'icon-paint-brush',
'category' => 'Templates',
'article' => 'https://mokoconsulting.tech/kb/mokoonyx-template',
'article' => 'https://mokoconsulting.tech/support/products/mokoonyx-template',
'protected' => false,
],
'MokoJoomTOS' => [
@@ -53,7 +53,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'component',
'icon' => 'icon-file-contract',
'category' => 'Components',
'article' => 'https://mokoconsulting.tech/kb/mokojoomtos',
'article' => 'https://mokoconsulting.tech/support/products/mokojoomtos',
'protected' => false,
],
'MokoJoomHero' => [
@@ -63,7 +63,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'module',
'icon' => 'icon-image',
'category' => 'Modules',
'article' => 'https://mokoconsulting.tech/kb/mokojoomhero',
'article' => 'https://mokoconsulting.tech/support/products/mokojoomhero',
'protected' => false,
],
'MokoWaaSAnnounce' => [
@@ -73,7 +73,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'module',
'icon' => 'icon-bullhorn',
'category' => 'Modules',
'article' => 'https://mokoconsulting.tech/kb/mokowaas-announce',
'article' => 'https://mokoconsulting.tech/support/products/mokowaas-announce',
'protected' => false,
],
'MokoDPCalendarAPI' => [
@@ -83,7 +83,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'plugin',
'icon' => 'icon-calendar',
'category' => 'Plugins',
'article' => 'https://mokoconsulting.tech/kb/mokodpcalendarapi',
'article' => 'https://mokoconsulting.tech/support/products/mokodpcalendarapi',
'protected' => false,
],
'MokoGalleryCalendar' => [
@@ -93,7 +93,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'plugin',
'icon' => 'icon-images',
'category' => 'Plugins',
'article' => 'https://mokoconsulting.tech/kb/mokogallerycalendar',
'article' => 'https://mokoconsulting.tech/support/products/mokogallerycalendar',
'protected' => false,
],
'MokoJoomOpenGraph' => [
@@ -103,7 +103,7 @@ class ExtensionsModel extends BaseDatabaseModel
'type' => 'package',
'icon' => 'icon-share-alt',
'category' => 'Components',
'article' => 'https://mokoconsulting.tech/kb/mokojoomopengraph',
'article' => 'https://mokoconsulting.tech/support/products/mokojoomopengraph',
'protected' => false,
],
];
@@ -28,9 +28,15 @@ class ImportModel extends BaseDatabaseModel
{
/**
* Check if Admin Tools data is available for import.
* Returns null if already imported or no data found.
*/
public function checkAdminToolsAvailable(): ?object
{
if ($this->wasImported('admintools'))
{
return null;
}
$db = $this->getDatabase();
try
@@ -138,6 +144,8 @@ class ImportModel extends BaseDatabaseModel
$this->disableAdminTools($db);
$results['disabled'] = true;
$this->markImported('admintools');
return [
'success' => true,
'message' => \sprintf(
@@ -542,9 +550,15 @@ class ImportModel extends BaseDatabaseModel
/**
* Check if ATS tables exist.
* Returns null if already imported or no data found.
*/
public function checkAtsAvailable(): ?object
{
if ($this->wasImported('ats'))
{
return null;
}
$db = $this->getDatabase();
try
@@ -612,6 +626,63 @@ class ImportModel extends BaseDatabaseModel
$result['message'] .= ' Warning: could not disable ATS: ' . $e->getMessage();
}
$this->markImported('ats');
return $result;
}
// ==================================================================
// Import markers (stored in component params)
// ==================================================================
private function wasImported(string $key): bool
{
try
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = new Registry($db->loadResult() ?? '{}');
return (bool) $params->get('imported_' . $key, false);
}
catch (\Throwable $e)
{
return false;
}
}
private function markImported(string $key): void
{
try
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = new Registry($db->loadResult() ?? '{}');
$params->set('imported_' . $key, 1);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute();
}
catch (\Throwable $e)
{
Log::add('Import marker error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
@@ -0,0 +1,251 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class MaintenanceModel extends BaseDatabaseModel
{
/**
* Get database table status (size, rows, engine, overhead).
*/
public function getTableStatus(): array
{
$db = $this->getDatabase();
$prefix = $db->getPrefix();
$db->setQuery('SHOW TABLE STATUS');
$tables = $db->loadObjectList() ?: [];
$results = [];
$totalSize = 0;
$totalOverhead = 0;
foreach ($tables as $t)
{
$sizeMb = round(($t->Data_length + $t->Index_length) / 1048576, 2);
$overheadKb = round(($t->Data_free ?? 0) / 1024, 1);
$totalSize += $sizeMb;
$totalOverhead += $overheadKb;
$results[] = (object) [
'name' => $t->Name,
'rows' => (int) $t->Rows,
'engine' => $t->Engine,
'size_mb' => $sizeMb,
'overhead_kb' => $overheadKb,
'is_moko' => str_contains($t->Name, 'mokowaas'),
];
}
usort($results, fn($a, $b) => $b->size_mb <=> $a->size_mb);
return ['tables' => $results, 'total_size_mb' => round($totalSize, 2), 'total_overhead_kb' => round($totalOverhead, 1), 'count' => \count($results)];
}
/**
* Optimize all tables or specific ones.
*/
public function optimizeTables(array $tableNames = []): array
{
$db = $this->getDatabase();
$count = 0;
try
{
if (empty($tableNames))
{
$db->setQuery('SHOW TABLE STATUS WHERE Data_free > 0');
$tables = $db->loadObjectList() ?: [];
$tableNames = array_column($tables, 'Name');
}
foreach ($tableNames as $name)
{
$db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($name));
$db->execute();
$count++;
}
return ['success' => true, 'message' => "Optimized {$count} tables."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Optimize failed: ' . $e->getMessage()];
}
}
/**
* Repair all tables.
*/
public function repairTables(): array
{
$db = $this->getDatabase();
try
{
$db->setQuery('SHOW TABLE STATUS');
$tables = $db->loadObjectList() ?: [];
$count = 0;
foreach ($tables as $t)
{
if ($t->Engine === 'InnoDB' || $t->Engine === 'MyISAM')
{
$db->setQuery('REPAIR TABLE ' . $db->quoteName($t->Name));
$db->execute();
$count++;
}
}
return ['success' => true, 'message' => "Repaired {$count} tables."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Repair failed: ' . $e->getMessage()];
}
}
/**
* Purge expired sessions.
*/
public function purgeSessions(): array
{
try
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__session'))
->where($db->quoteName('time') . ' < ' . (time() - 86400))
)->execute();
return ['success' => true, 'message' => 'Expired sessions purged. ' . $db->getAffectedRows() . ' removed.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => $e->getMessage()];
}
}
// ==================================================================
// Temp/Cache Cleanup (#128)
// ==================================================================
/**
* Get directory sizes for cleanup.
*/
public function getCleanupInfo(): array
{
$dirs = [
['path' => JPATH_ROOT . '/cache', 'label' => 'Site Cache'],
['path' => JPATH_ADMINISTRATOR . '/cache', 'label' => 'Admin Cache'],
['path' => JPATH_ROOT . '/tmp', 'label' => 'Temp Directory'],
['path' => JPATH_ADMINISTRATOR . '/logs', 'label' => 'Log Files'],
];
$results = [];
foreach ($dirs as $dir)
{
$size = 0;
$files = 0;
if (is_dir($dir['path']))
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir['path'], \RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file)
{
if ($file->isFile())
{
$size += $file->getSize();
$files++;
}
}
}
$results[] = (object) [
'label' => $dir['label'],
'path' => $dir['path'],
'size_mb' => round($size / 1048576, 2),
'files' => $files,
'writable' => is_writable($dir['path']),
];
}
return $results;
}
/**
* Clean a specific directory.
*/
public function cleanDirectory(string $dirKey): array
{
$allowed = [
'site_cache' => JPATH_ROOT . '/cache',
'admin_cache' => JPATH_ADMINISTRATOR . '/cache',
'tmp' => JPATH_ROOT . '/tmp',
'logs' => JPATH_ADMINISTRATOR . '/logs',
];
if (!isset($allowed[$dirKey]))
{
return ['success' => false, 'message' => 'Invalid directory.'];
}
$dir = $allowed[$dirKey];
if (!is_dir($dir))
{
return ['success' => false, 'message' => 'Directory not found.'];
}
$count = 0;
try
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $item)
{
// Keep index.html and .htaccess files
$name = $item->getFilename();
if ($name === 'index.html' || $name === '.htaccess')
{
continue;
}
if ($item->isDir())
{
@rmdir($item->getPathname());
}
else
{
@unlink($item->getPathname());
$count++;
}
}
// Also clear opcache
if (\function_exists('opcache_reset'))
{
\opcache_reset();
}
return ['success' => true, 'message' => "Cleaned {$count} files from {$dirKey}."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Cleanup failed: ' . $e->getMessage()];
}
}
}
@@ -0,0 +1,612 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class PrivacyModel extends BaseDatabaseModel
{
/**
* Get all pending data requests.
*/
public function getDataRequests(string $filterStatus = ''): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
$db->quoteName('r') . '.*',
$db->quoteName('u.name', 'user_name'),
$db->quoteName('u.email', 'user_email'),
$db->quoteName('u.username'),
$db->quoteName('p.name', 'processed_by_name'),
])
->from($db->quoteName('#__mokowaas_data_requests', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->leftJoin($db->quoteName('#__users', 'p') . ' ON p.id = r.processed_by');
if ($filterStatus)
{
$query->where($db->quoteName('r.status') . ' = ' . $db->quote($filterStatus));
}
$query->order($db->quoteName('r.created') . ' DESC')->setLimit(50);
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
/**
* Create a data request (from admin or user self-service).
*/
public function createRequest(int $userId, string $type, string $notes = ''): array
{
$validTypes = ['export', 'delete', 'anonymize'];
if (!\in_array($type, $validTypes, true))
{
return ['success' => false, 'message' => 'Invalid request type.'];
}
try
{
$db = $this->getDatabase();
$row = (object) [
'user_id' => $userId,
'type' => $type,
'status' => 'pending',
'notes' => $notes,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokowaas_data_requests', $row, 'id');
return ['success' => true, 'message' => ucfirst($type) . ' request #' . $row->id . ' created.', 'id' => (int) $row->id];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
}
}
/**
* Process a data request (approve and execute).
*/
public function processRequest(int $requestId, string $action): array
{
$db = $this->getDatabase();
try
{
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_data_requests'))
->where($db->quoteName('id') . ' = ' . $requestId)
);
$request = $db->loadObject();
if (!$request)
{
return ['success' => false, 'message' => 'Request not found.'];
}
if ($action === 'deny')
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('denied'))
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
->where($db->quoteName('id') . ' = ' . $requestId)
)->execute();
return ['success' => true, 'message' => 'Request denied.'];
}
// Mark as processing
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('processing'))
->where($db->quoteName('id') . ' = ' . $requestId)
)->execute();
// Execute the request
$result = null;
switch ($request->type)
{
case 'export':
$result = $this->exportUserData((int) $request->user_id);
break;
case 'delete':
$result = $this->deleteUserData((int) $request->user_id);
break;
case 'anonymize':
$result = $this->anonymizeUserData((int) $request->user_id);
break;
}
// Mark completed
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('completed'))
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
->where($db->quoteName('id') . ' = ' . $requestId)
)->execute();
return $result ?? ['success' => true, 'message' => 'Request processed.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Processing failed: ' . $e->getMessage()];
}
}
/**
* Export all data for a user as a structured array.
*/
public function exportUserData(int $userId): array
{
$db = $this->getDatabase();
$data = ['user_id' => $userId, 'exported' => gmdate('Y-m-d\TH:i:s\Z')];
try
{
// User profile
$db->setQuery(
$db->getQuery(true)
->select(['id', 'name', 'username', 'email', 'registerDate', 'lastvisitDate', 'params'])
->from($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . $userId)
);
$data['profile'] = $db->loadObject();
// Content (articles)
$db->setQuery(
$db->getQuery(true)
->select(['id', 'title', 'alias', 'created', 'modified', 'hits'])
->from($db->quoteName('#__content'))
->where($db->quoteName('created_by') . ' = ' . $userId)
);
$data['articles'] = $db->loadObjectList() ?: [];
// Action logs
$db->setQuery(
$db->getQuery(true)
->select(['message', 'log_date', 'ip_address'])
->from($db->quoteName('#__action_logs'))
->where($db->quoteName('user_id') . ' = ' . $userId)
->order('log_date DESC')
->setLimit(100)
);
$data['action_logs'] = $db->loadObjectList() ?: [];
// Support tickets
$db->setQuery(
$db->getQuery(true)
->select(['id', 'subject', 'body', 'status', 'priority', 'created'])
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
);
$data['tickets'] = $db->loadObjectList() ?: [];
// Ticket replies
$db->setQuery(
$db->getQuery(true)
->select(['r.id', 'r.ticket_id', 'r.body', 'r.created'])
->from($db->quoteName('#__mokowaas_ticket_replies', 'r'))
->where($db->quoteName('r.user_id') . ' = ' . $userId)
);
$data['ticket_replies'] = $db->loadObjectList() ?: [];
// Consent log
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId)
->order('created ASC')
);
$data['consent_history'] = $db->loadObjectList() ?: [];
// Community Builder profile (if table exists)
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%comprofiler%'));
if ($db->loadResult())
{
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__comprofiler'))
->where($db->quoteName('user_id') . ' = ' . $userId)
);
$data['community_builder'] = $db->loadObject();
}
}
catch (\Throwable $e) {}
return ['success' => true, 'message' => 'Data exported.', 'data' => $data];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Export failed: ' . $e->getMessage()];
}
}
/**
* Anonymize a user's data (GDPR right to be forgotten — soft).
*/
public function anonymizeUserData(int $userId): array
{
$db = $this->getDatabase();
$now = Factory::getDate()->toSql();
$anon = 'Anonymous User #' . $userId;
try
{
// Anonymize user record
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__users'))
->set([
$db->quoteName('name') . ' = ' . $db->quote($anon),
$db->quoteName('username') . ' = ' . $db->quote('anon_' . $userId),
$db->quoteName('email') . ' = ' . $db->quote('anon_' . $userId . '@deleted.local'),
$db->quoteName('password') . ' = ' . $db->quote(''),
$db->quoteName('block') . ' = 1',
$db->quoteName('params') . ' = ' . $db->quote('{}'),
])
->where($db->quoteName('id') . ' = ' . $userId)
)->execute();
// Anonymize article authorship
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('created_by_alias') . ' = ' . $db->quote($anon))
->where($db->quoteName('created_by') . ' = ' . $userId)
)->execute();
// Delete action logs
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__action_logs'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
// Anonymize ticket replies
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_ticket_replies'))
->set($db->quoteName('body') . ' = ' . $db->quote('[Content removed per data request]'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
// Community Builder
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%comprofiler%'));
if ($db->loadResult())
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__comprofiler'))
->set([
$db->quoteName('firstname') . ' = ' . $db->quote('Anonymous'),
$db->quoteName('lastname') . ' = ' . $db->quote('User'),
$db->quoteName('middlename') . ' = ' . $db->quote(''),
])
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
}
}
catch (\Throwable $e) {}
// Clear Joomla user profile fields (#7)
try
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__user_profiles'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
}
catch (\Throwable $e) {}
// Clear contact details if linked
try
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__contact_details'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
}
catch (\Throwable $e) {}
// Log the anonymization
$this->logConsent($userId, 'account_anonymized', 'granted');
return ['success' => true, 'message' => 'User #' . $userId . ' data anonymized.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Anonymization failed: ' . $e->getMessage()];
}
}
/**
* Delete a user's data completely (hard delete).
*/
public function deleteUserData(int $userId): array
{
$result = $this->anonymizeUserData($userId);
if (!$result['success'])
{
return $result;
}
$db = $this->getDatabase();
try
{
// Delete tickets and replies
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
);
$ticketIds = $db->loadColumn() ?: [];
if (!empty($ticketIds))
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_ticket_replies'))
->where($db->quoteName('ticket_id') . ' IN (' . implode(',', $ticketIds) . ')')
)->execute();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
)->execute();
}
// Delete consent log
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
// Delete user record
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . $userId)
)->execute();
return ['success' => true, 'message' => 'User #' . $userId . ' data permanently deleted.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Deletion failed: ' . $e->getMessage()];
}
}
// ==================================================================
// Consent Management
// ==================================================================
/**
* Get consent status for a user.
*/
public function getUserConsent(int $userId): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId)
->order($db->quoteName('created') . ' DESC')
);
return $db->loadObjectList() ?: [];
}
/**
* Record a consent action.
*/
public function logConsent(int $userId, string $category, string $action): void
{
$db = $this->getDatabase();
$row = (object) [
'user_id' => $userId,
'category' => $category,
'action' => $action === 'revoked' ? 'revoked' : 'granted',
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokowaas_consent_log', $row, 'id');
}
// ==================================================================
// Retention Policy Enforcement
// ==================================================================
/**
* Get all retention policies.
*/
public function getRetentionPolicies(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_retention_policies'))
->order($db->quoteName('id') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Run retention policy enforcement (called by scheduled task).
*/
public function enforceRetentionPolicies(): array
{
$db = $this->getDatabase();
$results = ['policies_run' => 0, 'items_affected' => 0];
$policies = $this->getRetentionPolicies();
foreach ($policies as $policy)
{
if (!(int) $policy->enabled)
{
continue;
}
$cutoff = Factory::getDate('-' . (int) $policy->retention_days . ' days')->toSql();
$count = 0;
try
{
switch ($policy->content_type)
{
case 'action_logs':
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__action_logs'))
->where($db->quoteName('log_date') . ' < ' . $db->quote($cutoff))
)->execute();
$count = $db->getAffectedRows();
break;
case 'waf_logs':
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_waf_log'))
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
)->execute();
$count = $db->getAffectedRows();
break;
case 'sessions':
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__session'))
->where($db->quoteName('time') . ' < ' . (int) strtotime($cutoff))
)->execute();
$count = $db->getAffectedRows();
break;
case 'closed_tickets':
if ($policy->action === 'anonymize')
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('body') . ' = ' . $db->quote('[Removed per retention policy]'))
->where($db->quoteName('status') . ' = ' . $db->quote('closed'))
->where($db->quoteName('closed') . ' < ' . $db->quote($cutoff))
->where($db->quoteName('body') . ' != ' . $db->quote('[Removed per retention policy]'))
)->execute();
$count = $db->getAffectedRows();
}
break;
case 'inactive_users':
if ($policy->action === 'anonymize')
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__users'))
->where($db->quoteName('lastvisitDate') . ' < ' . $db->quote($cutoff))
->where($db->quoteName('lastvisitDate') . ' != ' . $db->quote('0000-00-00 00:00:00'))
->where($db->quoteName('block') . ' = 0')
->where($db->quoteName('username') . ' NOT LIKE ' . $db->quote('anon_%'))
);
$userIds = $db->loadColumn() ?: [];
foreach ($userIds as $uid)
{
$this->anonymizeUserData((int) $uid);
$count++;
}
}
break;
}
if ($count > 0)
{
$results['policies_run']++;
$results['items_affected'] += $count;
Log::add(\sprintf('Retention: %s — %d items affected', $policy->content_type, $count), Log::INFO, 'mokowaas');
}
}
catch (\Throwable $e)
{
Log::add('Retention policy error (' . $policy->content_type . '): ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
return $results;
}
/**
* Get privacy dashboard summary counts.
*/
public function getDashboardSummary(): object
{
$db = $this->getDatabase();
$summary = (object) [
'pending_requests' => 0,
'total_requests' => 0,
'consent_entries' => 0,
'policies_active' => 0,
];
try
{
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests WHERE status = ' . $db->quote('pending'));
$summary->pending_requests = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests');
$summary->total_requests = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_consent_log');
$summary->consent_entries = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_retention_policies WHERE enabled = 1');
$summary->policies_active = (int) $db->loadResult();
}
catch (\Throwable $e) {}
return $summary;
}
}
@@ -12,6 +12,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Moko\Component\MokoWaaS\Administrator\Service\NotificationService;
class TicketsModel extends BaseDatabaseModel
{
@@ -173,8 +174,9 @@ class TicketsModel extends BaseDatabaseModel
$db->insertObject('#__mokowaas_tickets', $ticket, 'id');
// Run automation
// Run automation + notifications
$this->runAutomation('ticket_created', (int) $ticket->id);
NotificationService::notify('ticket_created', $this->getTicket((int) $ticket->id));
return ['success' => true, 'message' => 'Ticket #' . $ticket->id . ' created.', 'id' => (int) $ticket->id];
}
@@ -205,19 +207,31 @@ class TicketsModel extends BaseDatabaseModel
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
// Mark SLA as responded if first staff reply
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->set($db->quoteName('sla_responded') . ' = 1')
->where($db->quoteName('id') . ' = ' . $ticketId)
->where($db->quoteName('sla_responded') . ' = 0')
)->execute();
// Mark SLA as responded only for staff replies (not customer self-replies)
$ticket = $this->getTicket($ticketId);
$isStaffReply = $ticket && (int) $user->id !== (int) $ticket->created_by;
// Run automation
$updateQuery = $db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $ticketId);
if ($isStaffReply)
{
$updateQuery->set($db->quoteName('sla_responded') . ' = 1')
->where($db->quoteName('sla_responded') . ' = 0');
}
$db->setQuery($updateQuery)->execute();
// Run automation + notifications (skip internal notes)
$this->runAutomation('ticket_replied', $ticketId);
if (!$isInternal)
{
NotificationService::notify('ticket_replied', $this->getTicket($ticketId), ['reply_body' => $body]);
}
return ['success' => true, 'message' => 'Reply added.'];
}
catch (\Throwable $e)
@@ -243,6 +257,15 @@ class TicketsModel extends BaseDatabaseModel
$db = $this->getDatabase();
$now = Factory::getDate()->toSql();
// Capture old status for notification
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('status'))
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('id') . ' = ' . $ticketId)
);
$oldStatus = $db->loadResult() ?? '';
$sets = [
$db->quoteName('status') . ' = ' . $db->quote($status),
$db->quoteName('modified') . ' = ' . $db->quote($now),
@@ -265,8 +288,9 @@ class TicketsModel extends BaseDatabaseModel
->where($db->quoteName('id') . ' = ' . $ticketId)
)->execute();
// Run automation
// Run automation + notifications
$this->runAutomation('status_changed', $ticketId);
NotificationService::notify('status_changed', $this->getTicket($ticketId), ['old_status' => $oldStatus]);
return ['success' => true, 'message' => 'Status updated to ' . $status . '.'];
}
@@ -569,10 +593,138 @@ class TicketsModel extends BaseDatabaseModel
];
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
break;
case 'send_email':
// value = email address or comma-separated list
$emails = array_filter(array_map('trim', explode(',', $value)));
foreach ($emails as $email)
{
try
{
$mailer = Factory::getMailer();
$mailer->addRecipient($email);
$mailer->setSubject('[Ticket #' . $ticketId . '] Automation Alert');
$mailer->setBody('Automation rule triggered for ticket #' . $ticketId . ': ' . ($ticket->subject ?? ''));
$mailer->isHtml(false);
$mailer->Send();
}
catch (\Throwable $e)
{
\Joomla\CMS\Log\Log::add('Automation email failed: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
}
}
break;
case 'create_ticket':
// value = JSON: {"subject":"...","body":"...","category_id":1,"priority":"normal","behavior":"append"}
$ticketData = json_decode($value, true) ?: [];
$behavior = $ticketData['behavior'] ?? 'append';
$userId = (int) ($ticket->created_by ?? 0);
$catId = (int) ($ticketData['category_id'] ?? 0);
if ($behavior === 'append' && $userId > 0)
{
// Check for existing open ticket from this user in this category
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1')
->order($db->quoteName('created') . ' DESC')
->setLimit(1)
);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$this->addReply($existingId, $ticketData['body'] ?? 'Automation event', true);
break;
}
}
elseif ($behavior === 'skip_if_open' && $userId > 0)
{
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
);
if ((int) $db->loadResult() > 0)
{
break;
}
}
// Create new ticket
$this->createTicket([
'subject' => $ticketData['subject'] ?? 'Automation: ' . ($ticket->subject ?? 'System event'),
'body' => $ticketData['body'] ?? '',
'priority' => $ticketData['priority'] ?? 'normal',
'category_id' => $catId,
]);
break;
}
}
}
/**
* Run automation for a system event (not tied to a specific ticket).
* Creates a virtual ticket context from event data.
*/
public function runSystemEventAutomation(string $event, array $eventData = []): void
{
try
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$rules = $db->loadObjectList() ?: [];
if (empty($rules))
{
return;
}
// Build a virtual ticket-like object from event data
$context = (object) array_merge([
'id' => 0,
'subject' => $eventData['subject'] ?? $event,
'body' => $eventData['body'] ?? '',
'status' => 'open',
'priority' => $eventData['priority'] ?? 'normal',
'created_by' => $eventData['user_id'] ?? 0,
'created' => gmdate('Y-m-d H:i:s'),
'age_hours' => 0,
], $eventData);
foreach ($rules as $rule)
{
$conditions = json_decode($rule->conditions, true) ?: [];
$actions = json_decode($rule->actions, true) ?: [];
if (empty($conditions) || $this->evaluateConditions($conditions, $context))
{
$this->executeActions($actions, 0, $context);
}
}
}
catch (\Throwable $e)
{
\Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
}
}
/**
* Get all automation rules.
*/
@@ -0,0 +1,215 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class WaflogModel extends BaseDatabaseModel
{
/**
* Get WAF log entries with filters and pagination.
*/
public function getLogs(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_waf_log'));
if (!empty($filters['rule']))
{
$query->where($db->quoteName('rule') . ' = ' . $db->quote($filters['rule']));
}
if (!empty($filters['ip']))
{
$query->where($db->quoteName('ip') . ' LIKE ' . $db->quote('%' . $db->escape($filters['ip'], true) . '%'));
}
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('uri') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('detail') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('user_agent') . ' LIKE ' . $search . ')');
}
if (!empty($filters['date_from']))
{
$query->where($db->quoteName('created') . ' >= ' . $db->quote($filters['date_from'] . ' 00:00:00'));
}
if (!empty($filters['date_to']))
{
$query->where($db->quoteName('created') . ' <= ' . $db->quote($filters['date_to'] . ' 23:59:59'));
}
$query->order($db->quoteName('created') . ' DESC');
$query->setLimit($limit, $offset);
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
/**
* Get total count for pagination.
*/
public function getTotal(array $filters = []): int
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_waf_log'));
if (!empty($filters['rule']))
{
$query->where($db->quoteName('rule') . ' = ' . $db->quote($filters['rule']));
}
if (!empty($filters['ip']))
{
$query->where($db->quoteName('ip') . ' LIKE ' . $db->quote('%' . $db->escape($filters['ip'], true) . '%'));
}
$db->setQuery($query);
return (int) $db->loadResult();
}
/**
* Get block counts grouped by rule for the summary bar.
*/
public function getRuleCounts(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('rule'), 'COUNT(*) AS ' . $db->quoteName('cnt')])
->from($db->quoteName('#__mokowaas_waf_log'))
->group($db->quoteName('rule'))
->order($db->quoteName('cnt') . ' DESC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get top blocked IPs.
*/
public function getTopIps(int $limit = 10): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('ip'), 'COUNT(*) AS ' . $db->quoteName('cnt'),
'MAX(' . $db->quoteName('created') . ') AS ' . $db->quoteName('last_seen')])
->from($db->quoteName('#__mokowaas_waf_log'))
->group($db->quoteName('ip'))
->order($db->quoteName('cnt') . ' DESC')
->setLimit($limit)
);
return $db->loadObjectList() ?: [];
}
/**
* Get distinct rule names for the filter dropdown.
*/
public function getRuleNames(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('DISTINCT ' . $db->quoteName('rule'))
->from($db->quoteName('#__mokowaas_waf_log'))
->order($db->quoteName('rule') . ' ASC')
);
return $db->loadColumn() ?: [];
}
/**
* Delete logs older than N days.
*/
public function purgeLogs(int $days): array
{
try
{
$db = $this->getDatabase();
$cutoff = Factory::getDate('-' . $days . ' days')->toSql();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_waf_log'))
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
)->execute();
$count = $db->getAffectedRows();
return ['success' => true, 'message' => "Purged {$count} log entries older than {$days} days."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Purge failed: ' . $e->getMessage()];
}
}
/**
* Add an IP to the firewall blocklist.
*/
public function banIp(string $ip, string $reason = 'Banned from WAF log'): array
{
try
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
$db->setQuery($query);
$params = new \Joomla\Registry\Registry($db->loadResult() ?? '{}');
$blocklist = json_decode($params->get('ip_blocklist', '[]'), true) ?: [];
// Check if already blocked
foreach ($blocklist as $entry)
{
if (($entry['ip'] ?? '') === $ip)
{
return ['success' => false, 'message' => $ip . ' is already blocked.'];
}
}
$blocklist[] = ['ip' => $ip, 'enabled' => '1', 'label' => $reason];
$params->set('ip_blocklist', json_encode($blocklist));
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute();
return ['success' => true, 'message' => $ip . ' has been added to the IP blocklist.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Ban failed: ' . $e->getMessage()];
}
}
}
@@ -0,0 +1,416 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
/**
* Helpdesk email notification service.
*
* Sends emails for ticket events to Joomla users (by ID) and/or
* raw email addresses. Uses Joomla's configured mailer.
*
* @since 02.32.00
*/
class NotificationService
{
/**
* Send a ticket notification email.
*
* @param string $event Event name (ticket_created, ticket_replied, status_changed, ticket_assigned)
* @param object $ticket Ticket object with id, subject, status, priority, created_by, assigned_to
* @param array $extra Extra context (reply body, old status, etc.)
*/
public static function notify(string $event, object $ticket, array $extra = []): void
{
try
{
$recipients = self::getRecipients($event, $ticket);
if (empty($recipients))
{
return;
}
$subject = self::buildSubject($event, $ticket);
$body = self::buildBody($event, $ticket, $extra);
$mailer = Factory::getMailer();
$mailer->isHtml(false);
$mailer->setSubject($subject);
$mailer->setBody($body);
foreach ($recipients as $email)
{
$email = trim($email);
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL))
{
continue;
}
try
{
$mailer->clearAddresses();
$mailer->addRecipient($email);
$mailer->Send();
}
catch (\Throwable $e)
{
Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
catch (\Throwable $e)
{
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
/**
* Determine recipients based on event type and ticket data.
*/
private static function getRecipients(string $event, object $ticket): array
{
$emails = [];
// Get notification config from component params
$config = self::getNotificationConfig();
// Always notify configured admin emails
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
$emails = array_merge($emails, $adminEmails);
// Always notify configured admin user IDs
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
foreach ($adminUserIds as $uid)
{
$email = self::getUserEmail($uid);
if ($email)
{
$emails[] = $email;
}
}
switch ($event)
{
case 'ticket_created':
// Notify assigned user if any
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'ticket_replied':
// Notify ticket creator (customer gets notified of staff reply)
if (!empty($ticket->created_by))
{
$email = self::getUserEmail((int) $ticket->created_by);
if ($email)
{
$emails[] = $email;
}
}
// Notify assigned user
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'status_changed':
// Notify ticket creator
if (!empty($ticket->created_by))
{
$email = self::getUserEmail((int) $ticket->created_by);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'ticket_assigned':
// Notify newly assigned user
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
}
return array_unique($emails);
}
/**
* Build email subject line.
*/
private static function buildSubject(string $event, object $ticket): string
{
$siteName = Factory::getConfig()->get('sitename', 'Support');
$prefix = '[' . $siteName . ' #' . $ticket->id . '] ';
switch ($event)
{
case 'ticket_created':
return $prefix . 'New Ticket: ' . ($ticket->subject ?? '');
case 'ticket_replied':
return $prefix . 'Reply: ' . ($ticket->subject ?? '');
case 'status_changed':
return $prefix . 'Status Changed: ' . ($ticket->subject ?? '');
case 'ticket_assigned':
return $prefix . 'Assigned: ' . ($ticket->subject ?? '');
default:
return $prefix . ($ticket->subject ?? '');
}
}
/**
* Build email body.
*/
private static function buildBody(string $event, object $ticket, array $extra): string
{
$siteName = Factory::getConfig()->get('sitename', 'Support');
$siteUrl = rtrim(Uri::root(), '/');
$ticketUrl = $siteUrl . '/index.php?option=com_mokowaas&view=ticket&id=' . $ticket->id;
$lines = [];
$lines[] = $siteName . ' Support';
$lines[] = str_repeat('-', 40);
$lines[] = '';
switch ($event)
{
case 'ticket_created':
$lines[] = 'A new support ticket has been created.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
$lines[] = 'Category: ' . ($ticket->category_title ?? 'General');
$lines[] = '';
if (!empty($ticket->body))
{
$lines[] = 'Description:';
$lines[] = strip_tags($ticket->body);
$lines[] = '';
}
break;
case 'ticket_replied':
$lines[] = 'A new reply has been added to your ticket.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
$lines[] = '';
if (!empty($extra['reply_body']))
{
$lines[] = 'Reply:';
$lines[] = strip_tags($extra['reply_body']);
$lines[] = '';
}
break;
case 'status_changed':
$lines[] = 'Your ticket status has been updated.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'New Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
if (!empty($extra['old_status']))
{
$lines[] = 'Old Status: ' . ucwords(str_replace('_', ' ', $extra['old_status']));
}
$lines[] = '';
break;
case 'ticket_assigned':
$lines[] = 'A ticket has been assigned to you.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
$lines[] = '';
break;
}
$lines[] = 'View ticket: ' . $ticketUrl;
$lines[] = '';
$lines[] = '-- ';
$lines[] = $siteName . ' | Powered by MokoWaaS';
return implode("\n", $lines);
}
/**
* Get email address for a Joomla user ID.
*/
private static function getUserEmail(int $userId): ?string
{
if ($userId <= 0)
{
return null;
}
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('email'))
->from($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . $userId)
);
return $db->loadResult() ?: null;
}
catch (\Throwable $e)
{
return null;
}
}
/**
* Get notification configuration from component params.
*/
private static function getNotificationConfig(): array
{
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = json_decode($db->loadResult() ?? '{}', true);
return $params['notifications'] ?? [];
}
catch (\Throwable $e)
{
return [];
}
}
// ==================================================================
// Security Event Notifications (#131)
// ==================================================================
/**
* Send a security alert to admin emails.
*/
public static function securityAlert(string $event, string $subject, string $body): void
{
try
{
$config = self::getNotificationConfig();
$enabled = $config['security_alerts'] ?? '1';
if (!$enabled)
{
return;
}
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
$recipients = $adminEmails;
foreach ($adminUserIds as $uid)
{
$email = self::getUserEmail($uid);
if ($email)
{
$recipients[] = $email;
}
}
$recipients = array_unique($recipients);
if (empty($recipients))
{
return;
}
$siteName = Factory::getConfig()->get('sitename', 'Site');
$fullSubject = '[' . $siteName . ' Security] ' . $subject;
$lines = [
$siteName . ' Security Alert',
str_repeat('-', 40),
'',
'Event: ' . $event,
'Time: ' . gmdate('Y-m-d H:i:s') . ' UTC',
'',
$body,
'',
'-- ',
$siteName . ' | MokoWaaS Security',
];
$mailer = Factory::getMailer();
$mailer->isHtml(false);
$mailer->setSubject($fullSubject);
$mailer->setBody(implode("\n", $lines));
foreach ($recipients as $email)
{
try
{
$mailer->clearAddresses();
$mailer->addRecipient(trim($email));
$mailer->Send();
}
catch (\Throwable $e)
{
Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
catch (\Throwable $e)
{
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
@@ -0,0 +1,27 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Automation;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $rules = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\TicketsModel();
$this->rules = $model->getAutomationRules();
ToolbarHelper::title('Automation Rules', 'cogs');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Canned;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $responses = [];
protected $categories = [];
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT * FROM #__mokowaas_ticket_canned ORDER BY ordering ASC');
$this->responses = $db->loadObjectList() ?: [];
$db->setQuery('SELECT id, title FROM #__mokowaas_ticket_categories WHERE published = 1 ORDER BY ordering');
$this->categories = $db->loadObjectList() ?: [];
ToolbarHelper::title('Canned Responses', 'comment');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,41 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Categories;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $categories = [];
protected $users = [];
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT * FROM #__mokowaas_ticket_categories ORDER BY ordering ASC');
$this->categories = $db->loadObjectList() ?: [];
// Get admin users for auto-assign dropdown
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('name')])
->from($db->quoteName('#__users'))
->where($db->quoteName('block') . ' = 0')
->order($db->quoteName('name') . ' ASC')
->setLimit(100)
);
$this->users = $db->loadObjectList() ?: [];
ToolbarHelper::title('Ticket Categories', 'folder');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Cleanup;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $dirs = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->dirs = $model->getCleanupInfo();
ToolbarHelper::title('Cache &amp; Temp Cleanup', 'trash');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -23,6 +23,8 @@ class HtmlView extends BaseHtmlView
protected $pendingUpdates = [];
protected $checkedOutItems = [];
protected $wafBlocks = [];
protected $wafChartData = [];
protected $loginChartData = [];
public function display($tpl = null)
{
@@ -34,6 +36,8 @@ class HtmlView extends BaseHtmlView
$this->pendingUpdates = $model->getPendingUpdates();
$this->checkedOutItems = $model->getCheckedOutItems();
$this->wafBlocks = $model->getRecentWafBlocks(5);
$this->wafChartData = $model->getWafBlocksByDay(14);
$this->loginChartData = $model->getLoginsByDay(14);
// Check for importable Akeeba data
try
@@ -0,0 +1,27 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Database;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $tableData = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->tableData = $model->getTableStatus();
ToolbarHelper::title('Database Tools', 'database');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,39 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Privacy;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $requests = [];
protected $policies = [];
protected $summary;
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$filterStatus = Factory::getApplication()->getInput()->getString('filter_status', '');
$this->requests = $model->getDataRequests($filterStatus);
$this->policies = $model->getRetentionPolicies();
$this->summary = $model->getDashboardSummary();
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Privacy Guard', 'lock');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
}
}
@@ -0,0 +1,55 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Waflog;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $logs = [];
protected $ruleCounts = [];
protected $topIps = [];
protected $ruleNames = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'rule' => $input->getString('filter_rule', ''),
'ip' => $input->getString('filter_ip', ''),
'search' => $input->getString('filter_search', ''),
'date_from' => $input->getString('filter_date_from', ''),
'date_to' => $input->getString('filter_date_to', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->logs = $model->getLogs($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->ruleCounts = $model->getRuleCounts();
$this->topIps = $model->getTopIps(10);
$this->ruleNames = $model->getRuleNames();
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('WAF Log Viewer', 'shield-alt');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
}
}
@@ -0,0 +1,141 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$rules = $this->rules;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveAutomation&format=json');
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteAutomation&format=json');
$toggleUrl = Route::_('index.php?option=com_mokowaas&task=display.toggleAutomation&format=json');
$triggerLabels = ['ticket_created' => 'On Ticket Created', 'ticket_replied' => 'On Reply', 'status_changed' => 'On Status Change', 'scheduled' => 'Scheduled (Cron)'];
?>
<div id="mokowaas-automation">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($rules); ?> Automation Rules</h4>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newRuleModal">
<span class="icon-plus"></span> Add Rule
</button>
</div>
<?php foreach ($rules as $r): ?>
<?php $conditions = json_decode($r->conditions, true) ?: []; $actions = json_decode($r->actions, true) ?: []; ?>
<div class="card mb-2 <?php echo !$r->enabled ? 'opacity-50' : ''; ?>" data-id="<?php echo $r->id; ?>">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="d-flex align-items-center gap-2">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input rule-toggle" data-id="<?php echo $r->id; ?>" <?php echo $r->enabled ? 'checked' : ''; ?>>
</div>
<strong><?php echo htmlspecialchars($r->title); ?></strong>
<span class="badge bg-secondary"><?php echo $triggerLabels[$r->trigger_event] ?? $r->trigger_event; ?></span>
</div>
<div class="small text-muted mt-1">
<span class="text-primary">IF</span>
<?php foreach ($conditions as $i => $c): ?>
<?php echo $i > 0 ? ' AND ' : ''; ?><?php echo htmlspecialchars($c['field'] ?? ''); ?> <?php echo htmlspecialchars($c['op'] ?? ''); ?> <?php echo htmlspecialchars($c['value'] ?? ''); ?>
<?php endforeach; ?>
<span class="text-success ms-2">THEN</span>
<?php foreach ($actions as $a): ?>
<?php echo htmlspecialchars($a['type'] ?? ''); ?>=<?php echo htmlspecialchars(mb_substr($a['value'] ?? '', 0, 30)); ?>
<?php endforeach; ?>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-rule" data-id="<?php echo $r->id; ?>">
<span class="icon-trash"></span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($rules)): ?>
<div class="alert alert-info">No automation rules. Click "Add Rule" to create one.</div>
<?php endif; ?>
</div>
<!-- New Rule Modal -->
<div class="modal fade" id="newRuleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"><h5>Add Automation Rule</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" id="rule-title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Trigger</label>
<select id="rule-trigger" class="form-select">
<?php foreach ($triggerLabels as $k => $v): ?><option value="<?php echo $k; ?>"><?php echo $v; ?></option><?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Conditions (JSON)</label>
<textarea id="rule-conditions" class="form-control font-monospace" rows="3" placeholder='[{"field":"status","op":"eq","value":"resolved"}]'></textarea>
<small class="text-muted">Fields: status, priority, category_id, assigned_to, sla_responded, age_hours. Ops: eq, neq, gt, lt, in, not_in</small>
</div>
<div class="mb-3">
<label class="form-label">Actions (JSON)</label>
<textarea id="rule-actions" class="form-control font-monospace" rows="3" placeholder='[{"type":"set_status","value":"closed"}]'></textarea>
<small class="text-muted">Types: set_status, set_priority, assign, add_note, send_email</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-save-rule"><span class="icon-save"></span> Save Rule</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
// Save new rule
document.getElementById('btn-save-rule').addEventListener('click', function() {
var fd = new FormData();
fd.append('id', '0');
fd.append('title', document.getElementById('rule-title').value);
fd.append('trigger_event', document.getElementById('rule-trigger').value);
fd.append('conditions', document.getElementById('rule-conditions').value || '[]');
fd.append('actions', document.getElementById('rule-actions').value || '[]');
fd.append(token, '1');
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (d.success) location.reload(); else Joomla.renderMessages({error:[d.message]}); });
});
// Toggle rule
document.querySelectorAll('.rule-toggle').forEach(function(cb) {
cb.addEventListener('change', function() {
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append('enabled', this.checked ? '1' : '0');
fd.append(token, '1');
fetch('<?php echo $toggleUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (!d.success) Joomla.renderMessages({error:[d.message]}); });
});
});
// Delete rule
document.querySelectorAll('.btn-delete-rule').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this rule?')) return;
var card = this.closest('.card');
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append(token, '1');
fetch('<?php echo $deleteUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (d.success) card.remove(); else Joomla.renderMessages({error:[d.message]}); });
});
});
});
</script>
@@ -0,0 +1,107 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$responses = $this->responses;
$categories = $this->categories;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveCanned&format=json');
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCanned&format=json');
?>
<div id="mokowaas-canned">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($responses); ?> Canned Responses</h4>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newCannedModal">
<span class="icon-plus"></span> Add Response
</button>
</div>
<?php foreach ($responses as $r): ?>
<div class="card mb-2" data-id="<?php echo $r->id; ?>">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong><?php echo htmlspecialchars($r->title); ?></strong>
<p class="text-muted small mb-0 mt-1"><?php echo htmlspecialchars(mb_substr($r->body, 0, 150)); ?></p>
</div>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-canned" data-id="<?php echo $r->id; ?>">
<span class="icon-trash"></span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($responses)): ?>
<div class="alert alert-info">No canned responses yet. Click "Add Response" to create one.</div>
<?php endif; ?>
</div>
<!-- New Canned Modal -->
<div class="modal fade" id="newCannedModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"><h5>Add Canned Response</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" id="canned-title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Category (optional)</label>
<select id="canned-category" class="form-select">
<option value="">All categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo htmlspecialchars($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Response Text</label>
<textarea id="canned-body" class="form-control" rows="6" required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-save-canned"><span class="icon-save"></span> Save</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
document.getElementById('btn-save-canned').addEventListener('click', function() {
var fd = new FormData();
fd.append('id', '0');
fd.append('title', document.getElementById('canned-title').value);
fd.append('body', document.getElementById('canned-body').value);
fd.append('category_id', document.getElementById('canned-category').value);
fd.append(token, '1');
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) location.reload();
else Joomla.renderMessages({error:[d.message]});
});
});
document.querySelectorAll('.btn-delete-canned').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this canned response?')) return;
var card = this.closest('.card');
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append(token, '1');
fetch('<?php echo $deleteUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (d.success) card.remove(); else Joomla.renderMessages({error:[d.message]}); });
});
});
});
</script>
@@ -0,0 +1,126 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$categories = $this->categories;
$users = $this->users;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveCategory&format=json');
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCategory&format=json');
?>
<div id="mokowaas-categories">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($categories); ?> Categories</h4>
<button type="button" class="btn btn-primary btn-sm" id="btn-add-cat">
<span class="icon-plus"></span> Add Category
</button>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-striped mb-0" id="cat-table">
<thead><tr><th>Title</th><th>SLA Response</th><th>SLA Resolution</th><th>Auto-Assign</th><th>Active</th><th></th></tr></thead>
<tbody>
<?php foreach ($categories as $c): ?>
<tr data-id="<?php echo $c->id; ?>">
<td><input type="text" class="form-control form-control-sm cat-field" data-field="title" value="<?php echo htmlspecialchars($c->title); ?>"></td>
<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_response_minutes" value="<?php echo $c->sla_response_minutes; ?>" style="width:80px"> min</td>
<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_resolution_minutes" value="<?php echo $c->sla_resolution_minutes; ?>" style="width:80px"> min</td>
<td>
<select class="form-select form-select-sm cat-field" data-field="auto_assign_user">
<option value="">None</option>
<?php foreach ($users as $u): ?>
<option value="<?php echo $u->id; ?>" <?php echo (int)$c->auto_assign_user === (int)$u->id ? 'selected' : ''; ?>><?php echo htmlspecialchars($u->name); ?></option>
<?php endforeach; ?>
</select>
</td>
<td>
<input type="checkbox" class="form-check-input cat-field" data-field="published" <?php echo $c->published ? 'checked' : ''; ?>>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-success btn-save-cat" title="Save"><span class="icon-save"></span></button>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-cat" title="Delete"><span class="icon-trash"></span></button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
// Save category
document.querySelectorAll('.btn-save-cat').forEach(function(btn) {
btn.addEventListener('click', function() {
var row = this.closest('tr');
var id = row.dataset.id || '0';
var fd = new FormData();
fd.append('id', id);
fd.append(token, '1');
row.querySelectorAll('.cat-field').forEach(function(f) {
fd.append(f.dataset.field, f.type === 'checkbox' ? (f.checked ? '1' : '0') : f.value);
});
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); if (d.id && id === '0') row.dataset.id = d.id; }
else Joomla.renderMessages({error:[d.message]});
});
});
});
// Delete category
document.querySelectorAll('.btn-delete-cat').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this category?')) return;
var row = this.closest('tr');
var fd = new FormData();
fd.append('id', row.dataset.id);
fd.append(token, '1');
fetch('<?php echo $deleteUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) row.remove();
else Joomla.renderMessages({error:[d.message]});
});
});
});
// Add new row
document.getElementById('btn-add-cat').addEventListener('click', function() {
var tbody = document.querySelector('#cat-table tbody');
var tr = document.createElement('tr');
tr.dataset.id = '0';
tr.innerHTML = '<td><input type="text" class="form-control form-control-sm cat-field" data-field="title" value=""></td>'
+ '<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_response_minutes" value="480" style="width:80px"> min</td>'
+ '<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_resolution_minutes" value="2880" style="width:80px"> min</td>'
+ '<td><select class="form-select form-select-sm cat-field" data-field="auto_assign_user"><option value="">None</option><?php foreach ($users as $u): ?><option value="<?php echo $u->id; ?>"><?php echo htmlspecialchars($u->name); ?></option><?php endforeach; ?></select></td>'
+ '<td><input type="checkbox" class="form-check-input cat-field" data-field="published" checked></td>'
+ '<td><button type="button" class="btn btn-sm btn-outline-success btn-save-cat"><span class="icon-save"></span></button></td>';
tbody.appendChild(tr);
tr.querySelector('.btn-save-cat').addEventListener('click', function() {
var row = this.closest('tr');
var fd = new FormData();
fd.append('id', '0');
fd.append(token, '1');
row.querySelectorAll('.cat-field').forEach(function(f) {
fd.append(f.dataset.field, f.type === 'checkbox' ? (f.checked ? '1' : '0') : f.value);
});
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else Joomla.renderMessages({error:[d.message]});
});
});
tr.querySelector('input').focus();
});
});
</script>
@@ -0,0 +1,63 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$dirs = $this->dirs;
$token = Session::getFormToken();
$cleanUrl = Route::_('index.php?option=com_mokowaas&task=display.cleanDirectory&format=json');
$dirKeys = ['site_cache', 'admin_cache', 'tmp', 'logs'];
$totalMb = 0;
$totalFiles = 0;
foreach ($dirs as $d) { $totalMb += $d->size_mb; $totalFiles += $d->files; }
?>
<div id="mokowaas-cleanup">
<div class="row g-3 mb-4">
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalMb, 1); ?> MB</span><small class="text-muted">Total Size</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalFiles); ?></span><small class="text-muted">Total Files</small></div></div>
</div>
<div class="row g-3">
<?php foreach ($dirs as $i => $d): ?>
<div class="col-12 col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-body text-center">
<h5><?php echo htmlspecialchars($d->label); ?></h5>
<p class="fs-3 fw-bold mb-1 <?php echo $d->size_mb > 50 ? 'text-warning' : ''; ?>"><?php echo number_format($d->size_mb, 1); ?> MB</p>
<p class="text-muted small"><?php echo number_format($d->files); ?> files</p>
<?php if (!$d->writable): ?>
<span class="badge bg-danger">Not writable</span>
<?php else: ?>
<button type="button" class="btn btn-outline-danger btn-clean" data-key="<?php echo $dirKeys[$i] ?? ''; ?>" data-label="<?php echo htmlspecialchars($d->label); ?>">
<span class="icon-trash"></span> Clean
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<script>
document.querySelectorAll('.btn-clean').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Clean all files in ' + this.dataset.label + '?')) return;
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append('dir_key', el.dataset.key);
fd.append('<?php echo $token; ?>', '1');
fetch('<?php echo $cleanUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); setTimeout(function(){location.reload()},1500); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
})
.catch(function(){ el.disabled = false; });
});
});
</script>
@@ -65,6 +65,10 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<?php if ($siteInfo->offline): ?>
<span class="badge bg-danger"><?php echo Text::_('COM_MOKOWAAS_OFFLINE'); ?></span>
<?php endif; ?>
<div class="mokowaas-info-item ms-auto">
<span class="icon-globe" aria-hidden="true"></span>
<code><?php echo $this->escape($_SERVER['REMOTE_ADDR'] ?? ''); ?></code>
</div>
</div>
</div>
@@ -132,12 +136,12 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
</div>
<div class="col-6 col-md-4 col-xl-3">
<?php
// Use Community Builder if available, otherwise Joomla user manager
// Use MokoJoomCommunity if available, otherwise Joomla user manager
$useCB = file_exists(JPATH_ADMINISTRATOR . '/components/com_comprofiler/comprofiler.php');
$userUrl = $useCB
? Route::_('index.php?option=com_comprofiler&task=showusers')
: Route::_('index.php?option=com_users');
$userLabel = $useCB ? 'Community Builder' : 'User Manager';
$userLabel = $useCB ? 'MokoJoomCommunity' : 'User Manager';
?>
<a href="<?php echo $userUrl; ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-users d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
@@ -180,7 +184,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<span class="badge bg-light text-dark"><?php echo $this->escape($plugin->version); ?></span>
<?php endif; ?>
</div>
<p class="card-text text-muted small flex-grow-1"><?php echo $this->escape($plugin->description); ?></p>
<p class="card-text text-muted text-muted flex-grow-1"><?php echo $this->escape($plugin->description); ?></p>
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
<?php if ($plugin->protected): ?>
<span class="badge bg-dark"><?php echo Text::_('COM_MOKOWAAS_PROTECTED'); ?></span>
@@ -196,7 +200,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.togglePlugin&format=json'); ?>"
data-token="<?php echo $token; ?>"
<?php echo $plugin->enabled ? 'checked' : ''; ?>>
<label class="form-check-label small" for="toggle-<?php echo $plugin->extension_id; ?>">
<label class="form-check-label" for="toggle-<?php echo $plugin->extension_id; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOWAAS_ENABLED') : Text::_('COM_MOKOWAAS_DISABLED'); ?>
</label>
</div>
@@ -215,8 +219,28 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<?php endforeach; ?>
</div>
<!-- Right: Information Tables (4 cols) -->
<div class="col-12 col-xl-4">
<!-- Right: Charts & Information (4 cols) -->
<div class="col-12 col-xl-4" style="border-left:1px solid var(--gray-300, #dee2e6);padding-left:1.5rem;">
<!-- WAF Activity Chart -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-shield-alt" aria-hidden="true"></span> WAF Activity (14 days)</strong>
</div>
<div class="card-body py-2">
<canvas id="mokowaas-chart-waf" height="140"></canvas>
</div>
</div>
<!-- Login Activity Chart -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-user" aria-hidden="true"></span> Login Activity (14 days)</strong>
</div>
<div class="card-body py-2">
<canvas id="mokowaas-chart-logins" height="140"></canvas>
</div>
</div>
<!-- Pending Updates -->
<div class="card mb-3">
@@ -231,16 +255,16 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody>
<?php foreach ($pendingUpdates as $upd): ?>
<tr>
<td class="small"><?php echo $this->escape($upd->name); ?></td>
<td class="small text-muted"><?php echo $this->escape($upd->current_version); ?></td>
<td class="small text-success"><?php echo $this->escape($upd->version); ?></td>
<td class="text-muted"><?php echo $this->escape($upd->name); ?></td>
<td class="text-muted"><?php echo $this->escape($upd->current_version); ?></td>
<td class="text-success fw-bold"><?php echo $this->escape($upd->version); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">
<div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> All extensions up to date
</div>
<?php endif; ?>
@@ -259,19 +283,19 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody>
<?php foreach ($checkedOut as $item): ?>
<tr>
<td class="small"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td>
<td class="small"><?php echo $this->escape($item->username ?? ''); ?></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, 'M d H:i'); ?></td>
<td class="text-muted"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td>
<td class="text-muted"><?php echo $this->escape($item->username ?? ''); ?></td>
<td class="text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, 'M d H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card-footer text-center py-1">
<a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="small">Global Check-in</a>
<a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="text-muted">Global Check-in</a>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">
<div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> No checked out items
</div>
<?php endif; ?>
@@ -290,16 +314,16 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody>
<?php foreach ($wafBlocks as $block): ?>
<tr>
<td class="small"><code><?php echo $this->escape($block->ip); ?></code></td>
<td class="small"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $block->created, 'M d H:i'); ?></td>
<td class="text-muted"><code><?php echo $this->escape($block->ip); ?></code></td>
<td class="text-muted"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td>
<td class="text-muted"><?php echo HTMLHelper::_('date', $block->created, 'M d H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">
<div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> No recent blocks
</div>
<?php endif; ?>
@@ -317,19 +341,85 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody>
<?php foreach ($recentLogins as $login): ?>
<tr>
<td class="small"><?php echo $this->escape($login->username ?? ''); ?></td>
<td class="small"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $login->log_date, 'M d H:i'); ?></td>
<td class="text-muted"><?php echo $this->escape($login->username ?? ''); ?></td>
<td class="text-muted"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td>
<td class="text-muted"><?php echo HTMLHelper::_('date', $login->log_date, 'M d H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card-body text-center text-muted small py-3">No login activity recorded</div>
<div class="card-body text-center text-muted py-3">No login activity recorded</div>
<?php endif; ?>
</div>
</div><!-- /.col-xl-4 -->
</div><!-- /.row -->
</div>
<?php
// Prepare chart data as JSON for JavaScript
$wafChartData = $this->wafChartData ?? [];
$loginChartData = $this->loginChartData ?? [];
$wafLabels = array_map(fn($d) => $d->day, $wafChartData);
$wafValues = array_map(fn($d) => $d->total, $wafChartData);
$loginLabels = array_map(fn($d) => $d->day, $loginChartData);
$loginValues = array_map(fn($d) => $d->total, $loginChartData);
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var chartDefaults = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { maxRotation: 45, font: { size: 10 } } },
y: { beginAtZero: true, ticks: { stepSize: 1, font: { size: 10 } } }
}
};
// WAF chart
var wafCtx = document.getElementById('mokowaas-chart-waf');
if (wafCtx) {
new Chart(wafCtx, {
type: 'bar',
data: {
labels: <?php echo json_encode($wafLabels); ?>,
datasets: [{
data: <?php echo json_encode($wafValues); ?>,
backgroundColor: 'rgba(197, 40, 39, 0.6)',
borderColor: '#c52827',
borderWidth: 1,
borderRadius: 3
}]
},
options: chartDefaults
});
}
// Login chart
var loginCtx = document.getElementById('mokowaas-chart-logins');
if (loginCtx) {
new Chart(loginCtx, {
type: 'line',
data: {
labels: <?php echo json_encode($loginLabels); ?>,
datasets: [{
data: <?php echo json_encode($loginValues); ?>,
borderColor: '#2a69b8',
backgroundColor: 'rgba(42, 105, 184, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
pointBackgroundColor: '#2a69b8'
}]
},
options: chartDefaults
});
}
});
</script>
@@ -0,0 +1,72 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$data = $this->tableData;
$tables = $data['tables'] ?? [];
$token = Session::getFormToken();
$optimizeUrl = Route::_('index.php?option=com_mokowaas&task=display.optimizeDb&format=json');
$repairUrl = Route::_('index.php?option=com_mokowaas&task=display.repairDb&format=json');
$purgeUrl = Route::_('index.php?option=com_mokowaas&task=display.purgeSessions&format=json');
?>
<div id="mokowaas-database">
<div class="row g-3 mb-4">
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['count']; ?></span><small class="text-muted">Tables</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['total_size_mb']; ?> MB</span><small class="text-muted">Total Size</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3 <?php echo $data['total_overhead_kb'] > 100 ? 'text-warning' : 'text-success'; ?>"><?php echo $data['total_overhead_kb']; ?> KB</span><small class="text-muted">Overhead</small></div></div>
<div class="col-6 col-md-3">
<div class="card p-3 d-grid gap-2">
<button type="button" class="btn btn-sm btn-primary btn-db-action" data-url="<?php echo $optimizeUrl; ?>" data-token="<?php echo $token; ?>" data-confirm="Optimize all tables with overhead?">
<span class="icon-bolt"></span> Optimize All
</button>
<button type="button" class="btn btn-sm btn-outline-warning btn-db-action" data-url="<?php echo $repairUrl; ?>" data-token="<?php echo $token; ?>" data-confirm="Repair all tables?">
<span class="icon-wrench"></span> Repair All
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-db-action" data-url="<?php echo $purgeUrl; ?>" data-token="<?php echo $token; ?>" data-confirm="Purge expired sessions?">
<span class="icon-trash"></span> Purge Sessions
</button>
</div>
</div>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-striped table-sm mb-0">
<thead><tr><th>Table</th><th>Engine</th><th class="text-end">Rows</th><th class="text-end">Size</th><th class="text-end">Overhead</th></tr></thead>
<tbody>
<?php foreach ($tables as $t): ?>
<tr class="<?php echo $t->overhead_kb > 10 ? 'table-warning' : ''; ?> <?php echo $t->is_moko ? 'fw-bold' : ''; ?>">
<td class="small"><?php echo htmlspecialchars($t->name); ?></td>
<td class="small"><?php echo htmlspecialchars($t->engine); ?></td>
<td class="text-end small"><?php echo number_format($t->rows); ?></td>
<td class="text-end small"><?php echo $t->size_mb; ?> MB</td>
<td class="text-end small <?php echo $t->overhead_kb > 10 ? 'text-warning fw-bold' : ''; ?>"><?php echo $t->overhead_kb > 0 ? $t->overhead_kb . ' KB' : '—'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.querySelectorAll('.btn-db-action').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm(this.dataset.confirm)) return;
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); setTimeout(function(){location.reload()},1500); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
})
.catch(function(){ el.disabled = false; });
});
});
</script>
@@ -83,6 +83,26 @@ $statusBadge = [
Install
</button>
<?php elseif ($pkg->status === 'installed'): ?>
<?php
$dashLink = '';
if ($pkg->type === 'component')
{
$dashLink = 'index.php?option=' . $pkg->element;
}
elseif ($pkg->type === 'package' && strpos($pkg->element, 'pkg_') === 0)
{
$comElement = 'com_' . substr($pkg->element, 4);
if (is_dir(JPATH_ADMINISTRATOR . '/components/' . $comElement))
{
$dashLink = 'index.php?option=' . $comElement;
}
}
?>
<?php if ($dashLink): ?>
<a href="<?php echo Route::_($dashLink); ?>" class="btn btn-sm btn-outline-primary" title="Open">
<span class="icon-arrow-right" aria-hidden="true"></span> Open
</a>
<?php endif; ?>
<span class="btn btn-sm btn-outline-success disabled">
<span class="icon-check" aria-hidden="true"></span> Installed
</span>
@@ -0,0 +1,267 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$requests = $this->requests;
$policies = $this->policies;
$summary = $this->summary;
$token = Session::getFormToken();
$statusBadge = [
'pending' => 'bg-warning text-dark',
'processing' => 'bg-info',
'completed' => 'bg-success',
'denied' => 'bg-secondary',
];
$typeBadge = [
'export' => 'bg-primary',
'delete' => 'bg-danger',
'anonymize' => 'bg-warning text-dark',
];
?>
<div id="mokowaas-privacy">
<!-- Summary cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3 <?php echo $summary->pending_requests > 0 ? 'text-warning' : 'text-success'; ?>"><?php echo $summary->pending_requests; ?></span>
<small class="text-muted">Pending Requests</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3"><?php echo $summary->total_requests; ?></span>
<small class="text-muted">Total Requests</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3"><?php echo $summary->consent_entries; ?></span>
<small class="text-muted">Consent Entries</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3"><?php echo $summary->policies_active; ?></span>
<small class="text-muted">Active Policies</small>
</div>
</div>
</div>
<!-- New Request Form -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-plus"></span> Create Data Request</strong>
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newRequestForm" aria-expanded="false">
<span class="icon-plus"></span> New Request
</button>
</div>
<div class="collapse" id="newRequestForm">
<div class="card-body">
<form id="formNewRequest" class="row g-3">
<div class="col-12 col-md-5">
<label for="req_user_id" class="form-label">User</label>
<select id="req_user_id" class="form-select" required>
<option value="">Select a user...</option>
<?php
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('name'), $db->quoteName('email')])
->from($db->quoteName('#__users'))
->where($db->quoteName('block') . ' = 0')
->order($db->quoteName('name'))
);
foreach ($db->loadObjectList() as $u):
?>
<option value="<?php echo (int) $u->id; ?>"><?php echo $this->escape($u->name); ?> (<?php echo $this->escape($u->email); ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-3">
<label for="req_type" class="form-label">Request Type</label>
<select id="req_type" class="form-select" required>
<option value="export">Export Data</option>
<option value="delete">Delete Data</option>
<option value="anonymize">Anonymize Data</option>
</select>
</div>
<div class="col-12 col-md-2">
<label for="req_auto" class="form-label">Auto-process</label>
<select id="req_auto" class="form-select">
<option value="0">No (pending)</option>
<option value="1">Yes (immediate)</option>
</select>
</div>
<div class="col-12 col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100" id="btnCreateRequest"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-check"></span> Submit
</button>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<!-- Data Requests -->
<div class="col-12 col-xl-8">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-user-shield"></span> Data Subject Requests</strong>
<form method="get" class="d-inline">
<input type="hidden" name="option" value="com_mokowaas">
<input type="hidden" name="view" value="privacy">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All</option>
<?php foreach (['pending','processing','completed','denied'] as $s): ?>
<option value="<?php echo $s; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_status') === $s ? 'selected' : ''; ?>><?php echo ucfirst($s); ?></option>
<?php endforeach; ?>
</select>
</form>
</div>
<?php if (empty($requests)): ?>
<div class="card-body text-center text-muted py-4">No data requests found.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead><tr><th>#</th><th>User</th><th>Type</th><th>Status</th><th>Created</th><th>Processed</th><th>Actions</th></tr></thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td><?php echo $r->id; ?></td>
<td><?php echo $this->escape($r->user_name ?? ''); ?><br><small class="text-muted"><?php echo $this->escape($r->user_email ?? ''); ?></small></td>
<td><span class="badge <?php echo $typeBadge[$r->type] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->type); ?></span></td>
<td><span class="badge <?php echo $statusBadge[$r->status] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->status); ?></span></td>
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $r->created, 'M d, Y H:i'); ?></td>
<td class="text-nowrap small"><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, 'M d, Y H:i') : '—'; ?></td>
<td>
<?php if ($r->status === 'pending'): ?>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-success btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="approve"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">Approve</button>
<button type="button" class="btn btn-outline-danger btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="deny"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">Deny</button>
</div>
<?php elseif ($r->status === 'completed' && $r->type === 'export'): ?>
<button type="button" class="btn btn-sm btn-outline-primary btn-export-download" data-user="<?php echo $r->user_id; ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.exportUserData&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-download"></span> Download
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Retention Policies -->
<div class="col-12 col-xl-4">
<div class="card mb-4">
<div class="card-header"><strong><span class="icon-clock"></span> Retention Policies</strong></div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>Type</th><th>Days</th><th>Action</th><th>Active</th></tr></thead>
<tbody>
<?php foreach ($policies as $p): ?>
<tr>
<td class="small"><?php echo $this->escape($p->content_type); ?></td>
<td><?php echo $p->retention_days; ?></td>
<td><span class="badge bg-secondary"><?php echo $p->action; ?></span></td>
<td><?php echo (int) $p->enabled ? '<span class="text-success">Yes</span>' : '<span class="text-muted">No</span>'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Process request buttons
document.querySelectorAll('.btn-privacy-action').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
var action = el.dataset.action;
if (!confirm(action === 'approve' ? 'Approve and process this data request?' : 'Deny this request?')) return;
el.disabled = true;
var fd = new FormData();
fd.append('request_id', el.dataset.id);
fd.append('action', action);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
});
});
});
// Create new request
var form = document.getElementById('formNewRequest');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
var btn = document.getElementById('btnCreateRequest');
var userId = document.getElementById('req_user_id').value;
var type = document.getElementById('req_type').value;
var auto = document.getElementById('req_auto').value;
if (!userId) { Joomla.renderMessages({warning:['Please select a user.']}); return; }
btn.disabled = true;
var fd = new FormData();
fd.append('user_id', userId);
fd.append('type', type);
fd.append('action', auto === '1' ? 'approve' : 'create');
fd.append(btn.dataset.token, '1');
fetch(btn.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message || 'Request created.']}); location.reload(); }
else { Joomla.renderMessages({error:[d.message || 'Failed.']}); btn.disabled = false; }
})
.catch(function(){ btn.disabled = false; });
});
}
// Export download
document.querySelectorAll('.btn-export-download').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
var fd = new FormData();
fd.append('user_id', el.dataset.user);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success && d.data) {
var blob = new Blob([JSON.stringify(d.data, null, 2)], {type:'application/json'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'user-data-export-' + el.dataset.user + '.json';
a.click();
} else {
Joomla.renderMessages({error:[d.message || 'Export failed']});
}
});
});
});
});
</script>
@@ -1,6 +1,7 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
@@ -112,7 +113,7 @@ $priorityBadge = [
<td><span class="badge <?php echo $priorityBadge[$t->priority] ?? 'bg-secondary'; ?>"><?php echo ucfirst($t->priority); ?></span></td>
<td><?php echo $this->escape($t->category_title ?? '—'); ?></td>
<td><?php echo $this->escape($t->created_by_name ?? ''); ?></td>
<td><?php echo $this->escape($t->assigned_to_name ?? '<em>Unassigned</em>'); ?></td>
<td><?php echo $t->assigned_to_name ? $this->escape($t->assigned_to_name) : '<em>Unassigned</em>'; ?></td>
<td class="small"><?php echo HTMLHelper::_('date', $t->created, 'M d H:i'); ?></td>
<td class="small">
<?php if ($t->sla_response_due && !$t->sla_responded): ?>
@@ -0,0 +1,212 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$logs = $this->logs;
$ruleCounts = $this->ruleCounts;
$topIps = $this->topIps;
$ruleNames = $this->ruleNames;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$totalPages = max(1, ceil($total / 50));
$ruleBadge = [
'sqli' => 'bg-danger', 'xss' => 'bg-danger', 'mua' => 'bg-warning text-dark',
'rfi' => 'bg-danger', 'dfi' => 'bg-danger', 'blocked_file' => 'bg-info',
'blocked_php' => 'bg-info', 'tmpl_switch' => 'bg-secondary',
'ip_blocklist' => 'bg-dark', 'admin_secret' => 'bg-dark',
];
?>
<div id="mokowaas-waflog">
<!-- Rule distribution cards -->
<div class="d-flex flex-wrap gap-2 mb-4">
<?php foreach ($ruleCounts as $rc): ?>
<div class="card p-2 text-center" style="min-width:100px">
<span class="badge <?php echo $ruleBadge[$rc->rule] ?? 'bg-secondary'; ?> mb-1"><?php echo htmlspecialchars($rc->rule); ?></span>
<span class="fw-bold"><?php echo number_format($rc->cnt); ?></span>
</div>
<?php endforeach; ?>
<div class="card p-2 text-center" style="min-width:100px">
<span class="badge bg-primary mb-1">Total</span>
<span class="fw-bold"><?php echo number_format($total); ?></span>
</div>
</div>
<div class="row">
<!-- Main: Log table -->
<div class="col-12 col-xl-9">
<!-- Filters -->
<form method="get" class="card mb-3">
<div class="card-body">
<input type="hidden" name="option" value="com_mokowaas">
<input type="hidden" name="view" value="waflog">
<div class="row g-2">
<div class="col-md-2">
<select name="filter_rule" class="form-select form-select-sm">
<option value="">All Rules</option>
<?php foreach ($ruleNames as $r): ?>
<option value="<?php echo htmlspecialchars($r); ?>" <?php echo $filters['rule'] === $r ? 'selected' : ''; ?>><?php echo htmlspecialchars($r); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<input type="text" name="filter_ip" class="form-control form-control-sm" placeholder="IP address" value="<?php echo htmlspecialchars($filters['ip']); ?>">
</div>
<div class="col-md-2">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search URI/detail" value="<?php echo htmlspecialchars($filters['search']); ?>">
</div>
<div class="col-md-2">
<input type="date" name="filter_date_from" class="form-control form-control-sm" value="<?php echo htmlspecialchars($filters['date_from']); ?>">
</div>
<div class="col-md-2">
<input type="date" name="filter_date_to" class="form-control form-control-sm" value="<?php echo htmlspecialchars($filters['date_to']); ?>">
</div>
<div class="col-md-2 d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</div>
</form>
<!-- Log table -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><?php echo number_format($total); ?> blocked requests</strong>
<button type="button" class="btn btn-sm btn-outline-danger" id="btn-purge"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.purgeWafLog&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-trash"></span> Purge Old Logs
</button>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover table-sm mb-0">
<thead>
<tr><th>Time</th><th>IP</th><th>Rule</th><th>URI</th><th>Detail</th><th>User Agent</th><th></th></tr>
</thead>
<tbody>
<?php if (empty($logs)): ?>
<tr><td colspan="7" class="text-center text-muted py-4">No blocked requests found.</td></tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr>
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $log->created, 'M d H:i:s'); ?></td>
<td><code><?php echo htmlspecialchars($log->ip); ?></code></td>
<td><span class="badge <?php echo $ruleBadge[$log->rule] ?? 'bg-secondary'; ?>"><?php echo htmlspecialchars($log->rule); ?></span></td>
<td class="small" style="max-width:250px;overflow:hidden;text-overflow:ellipsis" title="<?php echo htmlspecialchars($log->uri); ?>"><?php echo htmlspecialchars(mb_substr($log->uri, 0, 60)); ?></td>
<td class="small" style="max-width:200px;overflow:hidden;text-overflow:ellipsis"><?php echo htmlspecialchars(mb_substr($log->detail, 0, 50)); ?></td>
<td class="small" style="max-width:200px;overflow:hidden;text-overflow:ellipsis"><?php echo htmlspecialchars(mb_substr($log->user_agent, 0, 40)); ?></td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($log->ip); ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.banIpFromLog&format=json'); ?>"
data-token="<?php echo $token; ?>" title="Ban this IP">
<span class="icon-ban"></span>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<div class="card-footer d-flex justify-content-between align-items-center">
<small class="text-muted">Page <?php echo $page; ?> of <?php echo $totalPages; ?></small>
<nav>
<ul class="pagination pagination-sm mb-0">
<?php if ($page > 1): ?>
<li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog&page=' . ($page - 1)); ?>">Prev</a></li>
<?php endif; ?>
<?php if ($page < $totalPages): ?>
<li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog&page=' . ($page + 1)); ?>">Next</a></li>
<?php endif; ?>
</ul>
</nav>
</div>
<?php endif; ?>
</div>
</div>
<!-- Sidebar: Top IPs -->
<div class="col-12 col-xl-3">
<div class="card">
<div class="card-header"><strong>Top Blocked IPs</strong></div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>IP</th><th>Blocks</th><th>Last</th><th></th></tr></thead>
<tbody>
<?php foreach ($topIps as $tip): ?>
<tr>
<td><code class="small"><?php echo htmlspecialchars($tip->ip); ?></code></td>
<td class="fw-bold"><?php echo $tip->cnt; ?></td>
<td class="small text-nowrap"><?php echo HTMLHelper::_('date', $tip->last_seen, 'M d'); ?></td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($tip->ip); ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.banIpFromLog&format=json'); ?>"
data-token="<?php echo $token; ?>" title="Ban">
<span class="icon-ban"></span>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
// Ban IP buttons
document.querySelectorAll('.btn-ban-ip').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
var ip = el.dataset.ip;
if (!confirm('Add ' + ip + ' to the firewall IP blocklist?')) return;
el.disabled = true;
var fd = new FormData();
fd.append('ip', ip);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); el.textContent = 'Banned'; }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
});
});
});
// Purge button
var purgeBtn = document.getElementById('btn-purge');
if (purgeBtn) {
purgeBtn.addEventListener('click', function() {
var days = prompt('Delete WAF logs older than how many days?', '30');
if (!days || isNaN(days)) return;
this.disabled = true;
var fd = new FormData();
fd.append('days', days);
fd.append(this.dataset.token, '1');
fetch(this.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else { Joomla.renderMessages({error:[d.message]}); }
})
.finally(function(){ purgeBtn.disabled = false; });
});
}
});
</script>
+10 -2
View File
@@ -8,7 +8,7 @@
DEFGROUP: Joomla.Component
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.32.04
VERSION: 02.34.00
PATH: /mokowaas.xml
BRIEF: Component manifest for MokoWaaS admin dashboard and REST API
-->
@@ -20,7 +20,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.38</version>
<version>02.34.08-dev</version>
<description>MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoWaaS</namespace>
@@ -32,6 +32,10 @@
<menu link="option=com_mokowaas&amp;view=extensions" img="class:puzzle-piece">COM_MOKOWAAS_MENU_EXTENSIONS</menu>
<menu link="option=com_mokowaas&amp;view=tickets" img="class:headphones">COM_MOKOWAAS_MENU_TICKETS</menu>
<menu link="option=com_mokowaas&amp;view=htaccess" img="class:file-code">COM_MOKOWAAS_MENU_HTACCESS</menu>
<menu link="option=com_mokowaas&amp;view=privacy" img="class:lock">COM_MOKOWAAS_MENU_PRIVACY</menu>
<menu link="option=com_mokowaas&amp;view=waflog" img="class:shield-alt">COM_MOKOWAAS_MENU_WAFLOG</menu>
<menu link="option=com_mokowaas&amp;view=database" img="class:database">COM_MOKOWAAS_MENU_DATABASE</menu>
<menu link="option=com_mokowaas&amp;view=cleanup" img="class:trash">COM_MOKOWAAS_MENU_CLEANUP</menu>
<menu link="option=com_plugins&amp;filter[folder]=system&amp;filter[search]=mokowaas" img="class:power-off">COM_MOKOWAAS_MENU_PLUGINS</menu>
<menu link="option=com_installer&amp;view=update" img="class:refresh">COM_MOKOWAAS_MENU_UPDATES</menu>
<menu link="option=com_checkin" img="class:check-square">COM_MOKOWAAS_MENU_CHECKIN</menu>
@@ -39,12 +43,16 @@
</submenu>
<files folder="admin">
<filename>access.xml</filename>
<filename>config.xml</filename>
<folder>language</folder>
<folder>services</folder>
<folder>sql</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="admin/language">
<language tag="en-GB">en-GB/com_mokowaas.sys.ini</language>
</languages>
</administration>
<files folder="site">
@@ -50,6 +50,7 @@ class DisplayController extends BaseController
if ($user->guest)
{
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
return;
}
$input = Factory::getApplication()->getInput();
@@ -78,6 +79,7 @@ class DisplayController extends BaseController
if ($user->guest)
{
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
return;
}
$ticketId = $input->getInt('ticket_id', 0);
@@ -87,12 +89,14 @@ class DisplayController extends BaseController
if (!$ticket)
{
$this->jsonResponse(['success' => false, 'message' => 'Ticket not found.']);
return;
}
// Customers can only reply to their own tickets; staff can reply to any
if ((int) $ticket->created_by !== $user->id && !$this->isStaff($user))
{
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
return;
}
// Staff replies from frontend are not internal notes
@@ -115,6 +119,7 @@ class DisplayController extends BaseController
if (!$this->isStaff($user))
{
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
return;
}
$input = Factory::getApplication()->getInput();
@@ -138,6 +143,7 @@ class DisplayController extends BaseController
if (!$user->authorise('mokowaas.tickets.assign', 'com_mokowaas'))
{
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
return;
}
$input = Factory::getApplication()->getInput();
@@ -160,9 +166,31 @@ class DisplayController extends BaseController
catch (\Throwable $e)
{
$this->jsonResponse(['success' => false, 'message' => $e->getMessage()]);
return;
}
}
/**
* Submit a data privacy request from frontend.
*/
public function submitDataRequest()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
$user = Factory::getApplication()->getIdentity();
if ($user->guest)
{
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
return;
}
$type = Factory::getApplication()->getInput()->getString('type', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$this->jsonResponse($model->createRequest($user->id, $type, 'Submitted via self-service portal'));
}
/**
* Check if user is support staff (can manage tickets beyond their own).
*/
@@ -0,0 +1,68 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Privacy;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
class HtmlView extends BaseHtmlView
{
protected $requests = [];
protected $consent = [];
public function display($tpl = null)
{
$user = Factory::getApplication()->getIdentity();
if ($user->guest)
{
Factory::getApplication()->redirect(Route::_(
'index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_mokowaas&view=privacy'),
false
));
return;
}
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
// Get user's data requests
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_data_requests'))
->where($db->quoteName('user_id') . ' = ' . (int) $user->id)
->order($db->quoteName('created') . ' DESC');
try
{
$db->setQuery($query);
$this->requests = $db->loadObjectList() ?: [];
}
catch (\Throwable $e)
{
$this->requests = [];
}
// Get consent history
try
{
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . (int) $user->id)
->order($db->quoteName('created') . ' DESC')
->setLimit(20)
);
$this->consent = $db->loadObjectList() ?: [];
}
catch (\Throwable $e)
{
$this->consent = [];
}
parent::display($tpl);
}
}
@@ -0,0 +1,114 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$user = Factory::getApplication()->getIdentity();
$requests = $this->requests;
$consent = $this->consent;
$token = Session::getFormToken();
$statusLabel = ['pending' => 'Pending', 'processing' => 'Processing', 'completed' => 'Completed', 'denied' => 'Denied'];
$statusClass = ['pending' => 'warning', 'processing' => 'info', 'completed' => 'success', 'denied' => 'secondary'];
?>
<div class="mokowaas-portal">
<h2>My Privacy &amp; Data</h2>
<p class="text-muted">Manage your personal data, download your information, or request account deletion.</p>
<!-- Action buttons -->
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<button type="button" class="btn btn-primary w-100 py-3 btn-data-request" data-type="export">
<span class="icon-download d-block mb-1" style="font-size:1.5rem"></span>
Download My Data
</button>
</div>
<div class="col-12 col-md-4">
<button type="button" class="btn btn-outline-warning w-100 py-3 btn-data-request" data-type="anonymize">
<span class="icon-user-shield d-block mb-1" style="font-size:1.5rem"></span>
Anonymize My Account
</button>
</div>
<div class="col-12 col-md-4">
<button type="button" class="btn btn-outline-danger w-100 py-3 btn-data-request" data-type="delete">
<span class="icon-trash d-block mb-1" style="font-size:1.5rem"></span>
Delete My Account
</button>
</div>
</div>
<!-- My requests -->
<?php if (!empty($requests)): ?>
<div class="card mb-4">
<div class="card-header"><strong>My Data Requests</strong></div>
<div class="table-responsive">
<table class="table table-striped mb-0">
<thead><tr><th>Type</th><th>Status</th><th>Submitted</th><th>Processed</th></tr></thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td><?php echo ucfirst($r->type); ?></td>
<td><span class="badge bg-<?php echo $statusClass[$r->status] ?? 'secondary'; ?>"><?php echo $statusLabel[$r->status] ?? $r->status; ?></span></td>
<td><?php echo HTMLHelper::_('date', $r->created, 'M d, Y H:i'); ?></td>
<td><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, 'M d, Y H:i') : '—'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Consent history -->
<?php if (!empty($consent)): ?>
<div class="card mb-4">
<div class="card-header"><strong>Consent History</strong></div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>Category</th><th>Action</th><th>Date</th></tr></thead>
<tbody>
<?php foreach ($consent as $c): ?>
<tr>
<td><?php echo htmlspecialchars(ucwords(str_replace('_', ' ', $c->category))); ?></td>
<td><span class="badge bg-<?php echo $c->action === 'granted' ? 'success' : 'secondary'; ?>"><?php echo ucfirst($c->action); ?></span></td>
<td><?php echo HTMLHelper::_('date', $c->created, 'M d, Y H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<script>
document.querySelectorAll('.btn-data-request').forEach(function(btn) {
btn.addEventListener('click', function() {
var type = this.dataset.type;
var messages = {
'export': 'Request a download of all your personal data?',
'anonymize': 'Request your account to be anonymized? Your name, email, and personal details will be replaced. This cannot be undone.',
'delete': 'Request permanent deletion of your account and all data? This cannot be undone.'
};
if (!confirm(messages[type] || 'Submit this request?')) return;
this.disabled = true;
var fd = new FormData();
fd.append('type', type);
fd.append('<?php echo $token; ?>', '1');
fetch('<?php echo Route::_("index.php?option=com_mokowaas&task=display.submitDataRequest&format=json"); ?>', {
method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) { alert(d.message); location.reload(); }
else { alert(d.message || 'Failed.'); }
})
.catch(function() { alert('Network error.'); })
.finally(function() { btn.disabled = false; });
});
});
</script>
@@ -0,0 +1,3 @@
MOD_MOKOWAAS_CACHE="MokoWaaS Cache Cleaner"
MOD_MOKOWAAS_CACHE_DESC="One-click cache cleaner in the admin status bar. Clears all Joomla cache (site, admin, and expired)."
MOD_MOKOWAAS_CACHE_CLEAR_ALL="Clear All Cache"
@@ -0,0 +1,2 @@
MOD_MOKOWAAS_CACHE="MokoWaaS Cache Cleaner"
MOD_MOKOWAAS_CACHE_DESC="One-click cache cleaner in the admin status bar. Clears all Joomla cache (site, admin, and expired)."
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokowaas_cache</name>
<author>Moko Consulting</author>
<creationDate>2026-06-04</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<description>MOD_MOKOWAAS_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCache</namespace>
<files>
<folder module="mod_mokowaas_cache">services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/mod_mokowaas_cache.ini</language>
<language tag="en-GB">en-GB/mod_mokowaas_cache.sys.ini</language>
</languages>
</extension>
@@ -0,0 +1,23 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_cache
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoWaaSCache'));
$container->registerServiceProvider(new Module());
}
};
@@ -0,0 +1,14 @@
<?php
namespace Moko\Module\MokoWaaSCache\Administrator\Dispatcher;
defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
class Dispatcher extends AbstractModuleDispatcher
{
protected function getLayoutData()
{
return parent::getLayoutData();
}
}
@@ -0,0 +1,73 @@
<?php
/**
* MokoWaaS Cache Cleaner — status bar module
*
* One-click button in the admin status bar that clears all Joomla cache.
* Uses native Atum header-item markup.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Language\Text;
$token = Session::getFormToken();
$ajaxUrl = 'index.php?option=com_mokowaas&task=clearCache&format=json';
?>
<a href="#" class="header-item-content" title="<?php echo Text::_('MOD_MOKOWAAS_CACHE_CLEAR_ALL'); ?>" id="mokowaas-clear-cache">
<div class="header-item-icon">
<span class="icon-bolt" aria-hidden="true" id="mokowaas-cache-icon"></span>
</div>
<div class="header-item-text">
<?php echo Text::_('MOD_MOKOWAAS_CACHE'); ?>
</div>
</a>
<script>
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('mokowaas-clear-cache');
var icon = document.getElementById('mokowaas-cache-icon');
if (!btn || !icon) return;
btn.addEventListener('click', function(e) {
e.preventDefault();
if (btn.dataset.busy) return;
btn.dataset.busy = '1';
icon.className = 'icon-spinner icon-spin';
var formData = new FormData();
formData.append('<?php echo $token; ?>', '1');
fetch('<?php echo $ajaxUrl; ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: formData
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
icon.className = 'icon-check';
icon.style.color = '#198754';
} else {
icon.className = 'icon-times';
icon.style.color = '#dc3545';
}
setTimeout(function() {
icon.className = 'icon-bolt';
icon.style.color = '';
delete btn.dataset.busy;
}, 2000);
})
.catch(function() {
icon.className = 'icon-times';
icon.style.color = '#dc3545';
setTimeout(function() {
icon.className = 'icon-bolt';
icon.style.color = '';
delete btn.dataset.busy;
}, 2000);
});
});
});
</script>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.38</version>
<version>02.34.08-dev</version>
<description>MOD_MOKOWAAS_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCpanel</namespace>
@@ -24,6 +24,18 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
{
$data = parent::getLayoutData();
// Hide on MokoWaaS dashboard — the dashboard has its own info panels
$app = Factory::getApplication();
$option = $app->getInput()->get('option', '');
$view = $app->getInput()->get('view', '');
if ($option === 'com_mokowaas' && ($view === '' || $view === 'dashboard'))
{
$data['hidden'] = true;
return $data;
}
$db = Factory::getContainer()->get(DatabaseInterface::class);
$helper = $this->getHelperFactory()->getHelper('CpanelHelper');
@@ -33,6 +45,7 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
$data['counts'] = $helper->getCounts($db);
$data['disk'] = $helper->getDiskInfo();
$data['currentIp'] = $helper->getCurrentIp();
$data['ssl'] = $helper->getSslStatus();
return $data;
}
@@ -87,10 +87,11 @@ class CpanelHelper
public function getCounts(DatabaseInterface $db): object
{
$counts = (object) [
'articles' => 0,
'users' => 0,
'extensions' => 0,
'updates' => 0,
'articles' => 0,
'users' => 0,
'extensions' => 0,
'updates' => 0,
'moko_updates' => 0,
];
try
@@ -106,6 +107,20 @@ class CpanelHelper
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__updates'))->where($db->quoteName('extension_id') . ' != 0'));
$counts->updates = (int) $db->loadResult();
// MokoWaaS-specific updates
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__updates', 'u'))
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = u.extension_id')
->where('(' . $db->quoteName('e.element') . ' LIKE ' . $db->quote('mokowaas%')
. ' OR ' . $db->quoteName('e.element') . ' LIKE ' . $db->quote('pkg_mokowaas%')
. ' OR ' . $db->quoteName('e.element') . ' LIKE ' . $db->quote('com_mokowaas%')
. ' OR ' . $db->quoteName('e.element') . ' LIKE ' . $db->quote('mod_mokowaas%')
. ' OR ' . $db->quoteName('e.element') . ' = ' . $db->quote('mokoonyx') . ')')
);
$counts->moko_updates = (int) $db->loadResult();
}
catch (\Throwable $e)
{
@@ -136,4 +151,54 @@ class CpanelHelper
{
return $_SERVER['REMOTE_ADDR'] ?? '';
}
/**
* Check SSL certificate expiry (#148).
*
* @return object|null {expires, days_remaining, warning} or null if check fails
*/
public function getSslStatus(): ?object
{
try
{
$host = parse_url(\Joomla\CMS\Uri\Uri::root(), PHP_URL_HOST);
if (empty($host))
{
return null;
}
$context = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false]]);
$client = @stream_socket_client('ssl://' . $host . ':443', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $context);
if (!$client)
{
return null;
}
$params = stream_context_get_params($client);
fclose($client);
$cert = openssl_x509_parse($params['options']['ssl']['peer_certificate'] ?? '');
if (empty($cert['validTo_time_t']))
{
return null;
}
$expires = $cert['validTo_time_t'];
$days = (int) floor(($expires - time()) / 86400);
return (object) [
'expires' => date('Y-m-d', $expires),
'days_remaining' => $days,
'warning' => $days <= 30,
'critical' => $days <= 7,
];
}
catch (\Throwable $e)
{
return null;
}
}
}
@@ -12,6 +12,9 @@ use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
// Hidden when on MokoWaaS dashboard (redundant info)
if (!empty($hidden)) return;
$siteInfo = $siteInfo ?? (object) [];
$plugins = $plugins ?? [];
@@ -55,25 +58,47 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
?>
<div class="mod-mokowaas-cpanel card p-3 mb-4">
<!-- Header row (always visible, acts as collapse toggle) -->
<div class="d-flex align-items-center justify-content-between">
<a class="d-flex align-items-center gap-2 text-decoration-none text-reset" data-bs-toggle="collapse" href="#mokowaas-cpanel-body" role="button" aria-expanded="<?php echo $collapsed ? 'false' : 'true'; ?>" aria-controls="mokowaas-cpanel-body">
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span>
<strong>MokoWaaS</strong>
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokowaas_version ?? ''); ?></span>
<?php if (!empty($siteInfo->debug)): ?>
<span class="badge bg-warning text-dark">Debug</span>
<?php endif; ?>
<?php if (!empty($siteInfo->offline)): ?>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
<span class="icon-chevron-down small text-muted" aria-hidden="true"></span>
</a>
<a href="<?php echo Route::_('index.php?option=com_mokowaas'); ?>" class="btn btn-sm btn-primary">
<span class="icon-cogs" aria-hidden="true"></span>
<?php echo Text::_('MOD_MOKOWAAS_CPANEL_OPEN_DASHBOARD'); ?>
</a>
<!-- Header row -->
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-link p-0 text-muted" data-bs-toggle="collapse" data-bs-target="#mokowaas-cpanel-body" aria-expanded="<?php echo $collapsed ? 'false' : 'true'; ?>" aria-controls="mokowaas-cpanel-body" id="mokowaas-cpanel-toggle" style="font-size:1rem;line-height:1;width:1.5rem;">
<span class="fa-solid fa-caret-<?php echo $collapsed ? 'right' : 'down'; ?>" aria-hidden="true" id="mokowaas-cpanel-caret"></span>
</button>
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span>
<strong>MokoWaaS</strong>
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokowaas_version ?? ''); ?></span>
<?php if (!empty($siteInfo->debug)): ?>
<span class="badge bg-warning text-dark">Debug</span>
<?php endif; ?>
<?php if (!empty($siteInfo->offline)): ?>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
<?php if (($counts->moko_updates ?? 0) > 0): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-info text-decoration-none" title="MokoWaaS updates available">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->moko_updates; ?> MokoWaaS update<?php echo $counts->moko_updates > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<?php if ($counts->updates > 0 && $counts->updates !== ($counts->moko_updates ?? 0)): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none" title="Other updates available">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates - ($counts->moko_updates ?? 0); ?> update<?php echo ($counts->updates - ($counts->moko_updates ?? 0)) > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<span class="ms-auto">
<a href="<?php echo Route::_('index.php?option=com_mokowaas'); ?>" class="btn btn-sm btn-primary">
<span class="icon-cogs" aria-hidden="true"></span>
<?php echo Text::_('MOD_MOKOWAAS_CPANEL_OPEN_DASHBOARD'); ?>
</a>
</span>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var target = document.getElementById('mokowaas-cpanel-body');
var caret = document.getElementById('mokowaas-cpanel-caret');
if (target && caret) {
target.addEventListener('show.bs.collapse', function() { caret.className = 'fa-solid fa-caret-down'; });
target.addEventListener('hide.bs.collapse', function() { caret.className = 'fa-solid fa-caret-right'; });
}
});
</script>
<!-- Collapsible body -->
<div class="collapse<?php echo $collapsed ? '' : ' show'; ?> mt-3" id="mokowaas-cpanel-body">
@@ -130,6 +155,12 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
<?php if ($showIp && $currentIp): ?>
<span class="text-muted"><span class="icon-globe" aria-hidden="true"></span> <code><?php echo htmlspecialchars($currentIp); ?></code></span>
<?php endif; ?>
<?php $ssl = $ssl ?? null; if ($ssl): ?>
<span class="badge bg-<?php echo $ssl->critical ? 'danger' : ($ssl->warning ? 'warning text-dark' : 'success'); ?>" title="SSL expires <?php echo $ssl->expires; ?>">
<span class="icon-lock" aria-hidden="true"></span>
SSL <?php echo $ssl->days_remaining; ?>d
</span>
<?php endif; ?>
<?php if ($showVersions): ?>
<span class="text-muted">J<?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?> / PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
<?php endif; ?>
@@ -0,0 +1 @@
MOD_MOKOWAAS_MENU="MokoWaaS Admin Menu"
@@ -0,0 +1,2 @@
MOD_MOKOWAAS_MENU="MokoWaaS Admin Menu"
MOD_MOKOWAAS_MENU_DESC="Dedicated MokoWaaS section in the admin sidebar menu."
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokowaas_menu</name>
<author>Moko Consulting</author>
<creationDate>2026-06-04</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.08-dev</version>
<description>MokoWaaS admin sidebar menu — renders a dedicated MokoWaaS section in the admin menu before Joomla's default menu.</description>
<namespace path="src">Moko\Module\MokoWaaSMenu</namespace>
<files>
<folder module="mod_mokowaas_menu">services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/mod_mokowaas_menu.ini</language>
<language tag="en-GB">en-GB/mod_mokowaas_menu.sys.ini</language>
</languages>
</extension>
@@ -0,0 +1,18 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\HelperFactory;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoWaaSMenu'));
$container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoWaaSMenu\\Administrator\\Helper'));
$container->registerServiceProvider(new Module());
}
};
@@ -0,0 +1,14 @@
<?php
namespace Moko\Module\MokoWaaSMenu\Administrator\Dispatcher;
defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
class Dispatcher extends AbstractModuleDispatcher
{
protected function getLayoutData()
{
return parent::getLayoutData();
}
}
@@ -0,0 +1,191 @@
<?php
/**
* MokoWaaS Admin Sidebar Menu
*
* Renders MokoWaaS static views first, then auto-discovers installed
* Moko components from #__menu and renders their submenu items as
* nested MetisMenu collapsible sections.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$app = Factory::getApplication();
$currentOption = $app->getInput()->get('option', '');
$currentView = $app->getInput()->get('view', '');
// ── Static MokoWaaS views ────────────────────────────────────────────
$mokowaasItems = [
['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokowaas'],
['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokowaas&view=tickets'],
['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokowaas&view=extensions'],
['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokowaas&view=htaccess'],
['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokowaas&view=privacy'],
['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokowaas&view=waflog'],
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokowaas&view=database'],
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokowaas&view=cleanup'],
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokowaas'],
];
// ── Auto-discover Moko component menus from #__menu ──────────────────
$mokoComponents = [];
try
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
// Find all Moko component menu items (exclude com_mokowaas — handled above)
$db->setQuery(
"SELECT m.id, m.title, m.link, m.level, m.parent_id, m.img, e.element"
. " FROM " . $db->quoteName('#__menu') . " m"
. " LEFT JOIN " . $db->quoteName('#__extensions') . " e ON m.component_id = e.extension_id"
. " WHERE m.client_id = 1 AND m.level >= 1 AND m.published = 1"
. " AND e.element LIKE 'com_moko%'"
. " AND e.element != 'com_mokowaas'"
. " AND e.enabled = 1"
. " ORDER BY e.element, m.level, m.lft"
);
$menuItems = $db->loadObjectList() ?: [];
// Load sys.ini language files for discovered components
$lang = Factory::getLanguage();
$loadedLangs = [];
foreach ($menuItems as $m)
{
if (!isset($loadedLangs[$m->element]))
{
$lang->load($m->element . '.sys', JPATH_ADMINISTRATOR);
$lang->load($m->element, JPATH_ADMINISTRATOR);
$loadedLangs[$m->element] = true;
}
}
// Group: level 1 = component parent, level 2 = children
foreach ($menuItems as $m)
{
if ((int) $m->level === 1)
{
$mokoComponents[$m->element] = [
'id' => $m->id,
'title' => Text::_($m->title),
'link' => $m->link,
'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:puzzle-piece'),
'element' => $m->element,
'children' => [],
];
}
elseif ((int) $m->level === 2 && isset($mokoComponents[$m->element]))
{
$mokoComponents[$m->element]['children'][] = [
'title' => Text::_($m->title),
'link' => $m->link,
'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:cog'),
];
}
}
}
catch (\Throwable $e)
{
// Silent — menu works without auto-discovered components
}
// ── Determine active state ───────────────────────────────────────────
$mokowaasActive = ($currentOption === 'com_mokowaas');
$anyMokoActive = $mokowaasActive;
foreach ($mokoComponents as $comp)
{
$parsed = [];
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $parsed);
if (($parsed['option'] ?? '') === $currentOption)
{
$anyMokoActive = true;
}
}
$topClass = 'item parent item-level-1' . ($anyMokoActive ? ' mm-active' : '');
$topCollapse = 'collapse-level-1 mm-collapse' . ($anyMokoActive ? ' mm-show' : '');
?>
<style>
.sidebar-wrapper .item-level-1 > a { padding-inline-start: 1.5rem; }
.sidebar-wrapper .mokowaas-menu-item > a { padding-inline-start: 2rem; }
.sidebar-wrapper .mokowaas-menu-child > a { padding-inline-start: 2.5rem; }
</style>
<ul class="nav flex-column main-nav">
<li class="<?php echo $topClass; ?>">
<a class="has-arrow" href="#" aria-label="MokoWaaS">
<span class="icon-shield-alt" aria-hidden="true"></span>
<span class="sidebar-item-title">MokoWaaS</span>
</a>
<ul class="<?php echo $topCollapse; ?>" style="padding-inline-start:0.5rem;">
<?php // ── MokoWaaS static items ── ?>
<?php foreach ($mokowaasItems as $item): ?>
<?php
$active = false;
$parsed = [];
parse_str(parse_url($item['link'], PHP_URL_QUERY) ?? '', $parsed);
if (($parsed['option'] ?? '') === $currentOption)
{
$active = empty($parsed['view'])
? ($currentView === '' || $currentView === 'dashboard')
: ($currentView === ($parsed['view'] ?? ''));
}
$liClass = 'item mokowaas-menu-item' . ($active ? ' mm-active' : '');
$aClass = 'no-dropdown' . ($active ? ' mm-active' : '');
?>
<li class="<?php echo $liClass; ?>">
<a class="<?php echo $aClass; ?>" href="<?php echo Route::_($item['link']); ?>"<?php echo $active ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo $item['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
<span class="sidebar-item-title"><?php echo $item['title']; ?></span>
</a>
</li>
<?php endforeach; ?>
<?php // ── Auto-discovered Moko components with submenus ── ?>
<?php foreach ($mokoComponents as $comp): ?>
<?php
$compParsed = [];
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
$compActive = ($compParsed['option'] ?? '') === $currentOption;
$hasChildren = !empty($comp['children']);
$compLiClass = 'item mokowaas-menu-item' . ($hasChildren ? ' parent' : '') . ($compActive ? ' mm-active' : '');
$compAClass = ($hasChildren ? 'has-arrow' : 'no-dropdown') . ($compActive ? ' mm-active' : '');
$childCollapse = 'collapse-level-2 mm-collapse' . ($compActive ? ' mm-show' : '');
?>
<li class="<?php echo $compLiClass; ?>">
<a class="<?php echo $compAClass; ?>" href="<?php echo $hasChildren ? '#' : Route::_($comp['link']); ?>"<?php echo ($compActive && !$hasChildren) ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
</a>
<?php if ($hasChildren): ?>
<ul class="<?php echo $childCollapse; ?>" style="padding-inline-start:0.75rem;">
<?php foreach ($comp['children'] as $child): ?>
<?php
$childParsed = [];
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
$childActive = ($childParsed['option'] ?? '') === $currentOption
&& ($childParsed['view'] ?? '') === $currentView;
$childLiClass = 'item mokowaas-menu-child' . ($childActive ? ' mm-active' : '');
$childAClass = 'no-dropdown' . ($childActive ? ' mm-active' : '');
?>
<li class="<?php echo $childLiClass; ?>">
<a class="<?php echo $childAClass; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo $child['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
<span class="sidebar-item-title"><?php echo $child['title']; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</li>
</ul>
@@ -22,9 +22,9 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/Extension/MokoWaaS.php
* NOTE: Handles Joomla system events for rebranding functionality
* NOTE: Core system plugin for MokoWaaS admin tools suite
*/
namespace Moko\Plugin\System\MokoWaaS\Extension;
@@ -42,10 +42,9 @@ use Joomla\CMS\User\UserHelper;
use Psr\Container\ContainerInterface;
/**
* MokoWaaS Brand System Plugin
* MokoWaaS Core System Plugin
*
* This plugin rebrands the Joomla system interface with MokoWaaS identity.
* It applies language overrides and ensures consistent branding across the platform.
* This plugin provides core coordination for the MokoWaaS admin tools suite.
*
* @since 01.04.00
*/
@@ -187,18 +186,14 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
$this->handleMokoApi($mokoAction);
}
// Admin-only core controls (branding, emergency access, master user)
// Admin-only core controls (emergency access, master user)
// NOTE: enforceHttps, enforceDevMode, enforceAdminSessionTimeout,
// enforceUploadRestrictions are now in feature plugins
if ($this->app->isClient('administrator'))
{
$this->handleEmergencyAccess();
$this->enforceMasterUser();
$this->enforceLoginSupportUrls();
$this->enforceAtumBranding();
}
$this->loadLanguageOverrides();
}
/**
@@ -698,102 +693,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
return in_array($clientIp, $allowedIps, true);
}
/**
* Build the placeholder → value map from plugin params.
*
* @return array Associative array of placeholder => replacement value
*
* @since 02.01.08
*/
protected function getPlaceholders()
{
return [
'{{BRAND_NAME}}' => self::BRAND_NAME,
'{{COMPANY_NAME}}' => self::COMPANY_NAME,
'{{SUPPORT_URL}}' => self::SUPPORT_URL,
];
}
/**
* Load language override templates and inject resolved strings into Joomla.
*
* Reads the override template shipped with the plugin, replaces
* {{BRAND_NAME}}, {{COMPANY_NAME}} and {{SUPPORT_URL}} with the
* values from plugin params, then injects the resolved strings into
* the active Language object.
*
* @return void
*
* @since 02.01.08
*/
protected function loadLanguageOverrides()
{
$language = $this->app->getLanguage();
$tag = $language->getTag();
$pluginPath = JPATH_PLUGINS . '/system/mokowaas';
$isAdmin = $this->app->isClient('administrator');
$overridePath = $isAdmin
? $pluginPath . '/administrator/language/overrides/' . $tag . '.override.ini'
: $pluginPath . '/language/overrides/' . $tag . '.override.ini';
if (!file_exists($overridePath))
{
return;
}
$strings = $this->parseLanguageFile($overridePath);
$placeholders = $this->getPlaceholders();
foreach ($strings as $key => $value)
{
$language->_strings[$key] = str_replace(
array_keys($placeholders),
array_values($placeholders),
$value
);
}
}
/**
* Parse a language INI file and return the raw strings (with placeholders).
*
* @param string $filePath The path to the language file
*
* @return array Array of language strings (key => raw value)
*
* @since 02.01.08
*/
protected function parseLanguageFile($filePath)
{
$strings = [];
if (!file_exists($filePath))
{
return $strings;
}
$content = file_get_contents($filePath);
$lines = explode("\n", $content);
foreach ($lines as $line)
{
$line = trim($line);
if ($line === '' || $line[0] === ';')
{
continue;
}
if (preg_match('/^([A-Z0-9_]+)="(.+)"$/i', $line, $matches))
{
$strings[strtoupper($matches[1])] = $matches[2];
}
}
return $strings;
}
/**
* Event triggered after an extension's config is saved.
*
@@ -931,16 +830,66 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
*
* @since 02.01.08
*/
public function onAfterRoute()
// ------------------------------------------------------------------
// Automation event hooks (#151) — delegate to ticket automation engine
// ------------------------------------------------------------------
public function onUserLogin($user, $options = [])
{
if (!$this->app->isClient('administrator'))
// Security alert for admin logins (#131)
if ($this->app->isClient('administrator'))
{
return;
try
{
\Moko\Component\MokoWaaS\Administrator\Service\NotificationService::securityAlert(
'admin_login',
'Admin Login: ' . ($user['username'] ?? ''),
'User: ' . ($user['username'] ?? '') . "\nIP: " . ($_SERVER['REMOTE_ADDR'] ?? '') . "\nTime: " . gmdate('Y-m-d H:i:s') . ' UTC'
);
}
catch (\Throwable $e) {}
}
// NOTE: warnMissingLicenseKey and enforceAdminRestrictions
// are now handled by feature plugins (deferred / tenant)
$this->protectPlugin();
$this->fireTicketAutomation('user_login', [
'user_id' => $user['id'] ?? 0,
'username' => $user['username'] ?? '',
'subject' => 'User login: ' . ($user['username'] ?? ''),
'body' => 'User ' . ($user['username'] ?? '') . ' logged in from ' . ($_SERVER['REMOTE_ADDR'] ?? ''),
]);
}
public function onUserAfterSave($user, $isNew, $success, $msg)
{
if ($isNew && $success)
{
$this->fireTicketAutomation('user_register', [
'user_id' => $user['id'] ?? 0,
'username' => $user['username'] ?? '',
'subject' => 'New user registered: ' . ($user['username'] ?? ''),
'body' => 'New user: ' . ($user['name'] ?? '') . ' (' . ($user['email'] ?? '') . ')',
]);
}
}
public function onUserLoginFailure($response)
{
$this->fireTicketAutomation('user_login_failed', [
'subject' => 'Failed login attempt',
'body' => 'Failed login from ' . ($_SERVER['REMOTE_ADDR'] ?? '') . ': ' . ($response['username'] ?? ''),
]);
}
private function fireTicketAutomation(string $event, array $data): void
{
try
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\TicketsModel();
$model->runSystemEventAutomation($event, $data);
}
catch (\Throwable $e)
{
// Silent — automation should never break the main flow
}
}
/**
@@ -984,7 +933,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
return;
}
$this->injectFavicon($doc);
$this->redirectHelpMenu($doc);
// Hide MokoWaaS from plugin list for non-master users
@@ -1175,163 +1123,20 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
$doc->addScriptDeclaration("
document.addEventListener('DOMContentLoaded', function() {
var url = " . json_encode($supportUrl) . ";
document.querySelectorAll('a[href*=\"help.joomla.org\"], a[href*=\"docs.joomla.org\"]').forEach(function(link) {
link.href = " . json_encode($supportUrl) . ";
link.href = url;
link.target = '_blank';
});
document.querySelectorAll('a[href*=\"dashboard=help\"]').forEach(function(link) {
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
});
});
");
}
/**
* Protect the plugin from being disabled or uninstalled by non-master users.
* Does NOT self-heal (no lock) — master users can still disable if needed.
*
* @return void
*
* @since 02.03.04
*/
protected function protectPlugin()
{
// Ensure protected flag is set (self-healing — runs once per session)
static $flagChecked = false;
if (!$flagChecked)
{
$flagChecked = true;
$this->ensureProtectedFlag();
}
if ($this->isMasterUser())
{
return;
}
$option = $this->app->input->get('option', '');
$task = $this->app->input->get('task', '');
// Block non-master from uninstalling MokoWaaS
if ($option === 'com_installer' && strpos($task, 'manage.remove') !== false)
{
$cid = $this->app->input->get('cid', [], 'array');
if ($this->isOurExtension($cid))
{
$this->app->enqueueMessage('MokoWaaS cannot be uninstalled.', 'error');
$this->app->redirect('index.php?option=com_installer&view=manage');
}
}
// Block non-master from disabling via list toggle
if ($option === 'com_plugins' && strpos($task, 'plugins.publish') !== false)
{
$cid = $this->app->input->get('cid', [], 'array');
if ($this->isOurExtension($cid))
{
$this->app->enqueueMessage('MokoWaaS cannot be disabled.', 'error');
$this->app->redirect('index.php?option=com_plugins');
}
}
// Block non-master from viewing or editing MokoWaaS plugin settings
if ($option === 'com_plugins')
{
$view = $this->app->input->get('view', '');
$layout = $this->app->input->get('layout', '');
$extensionId = (int) $this->app->input->get('extension_id', 0);
if (($view === 'plugin' || $layout === 'edit') && $extensionId > 0)
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' = ' . $extensionId)
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
if ((int) $db->setQuery($query)->loadResult() > 0)
{
$this->app->enqueueMessage('MokoWaaS settings are restricted to the master user.', 'warning');
$this->app->redirect('index.php?option=com_plugins');
}
}
}
}
/**
* Ensure the protected flag is set on MokoWaaS extensions in the DB.
*
* Sets protected=1, locked=0 so the extension can't be disabled or
* uninstalled but can still receive updates and config changes.
*
* @return void
*
* @since 02.03.10
*/
protected function ensureProtectedFlag()
{
try
{
$db = Factory::getDbo();
// Set protected=1, locked=0 on MokoWaaS extensions
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('protected') . ' = 1')
->set($db->quoteName('locked') . ' = 0')
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')')
->where($db->quoteName('protected') . ' = 0');
$db->setQuery($query);
$db->execute();
// Ensure update site stays enabled (protected extensions get their update site disabled by Joomla)
$query = $db->getQuery(true)
->update($db->quoteName('#__update_sites') . ' AS us')
->join('INNER', $db->quoteName('#__update_sites_extensions') . ' AS use2 ON us.update_site_id = use2.update_site_id')
->join('INNER', $db->quoteName('#__extensions') . ' AS e ON use2.extension_id = e.extension_id')
->set('us.enabled = 1')
->where('us.enabled = 0')
->where('(' . $db->quoteName('e.element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
$db->setQuery($query);
$db->execute();
}
catch (\Throwable $e)
{
// Non-critical
}
}
/**
* Check if any of the given extension IDs belong to MokoWaaS.
*
* @param array $ids Extension IDs to check
*
* @return bool
*
* @since 02.03.04
*/
protected function isOurExtension(array $ids): bool
{
if (empty($ids))
{
return false;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('extension_id') . ' IN (' . implode(',', array_map('intval', $ids)) . ')')
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
return (int) $db->setQuery($query)->loadResult() > 0;
}
/**
* Prevent non-master users from disabling the plugin via save.
*
@@ -4359,115 +4164,7 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
* @since 02.01.08
*/
/**
* Set a parameter on a template style.
*
* @param string $template Template element name
* @param string $key Parameter key
* @param mixed $value Parameter value
*
* @return void
*
* @since 02.31.00
*/
private function setTemplateParam(string $template, string $key, $value): void
{
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('params')])
->from($db->quoteName('#__template_styles'))
->where($db->quoteName('template') . ' = ' . $db->quote($template));
$db->setQuery($query);
$styles = $db->loadObjectList();
foreach ($styles as $style)
{
$params = new \Joomla\Registry\Registry($style->params ?: '{}');
if ($params->get($key) != $value)
{
$params->set($key, $value);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__template_styles'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('id') . ' = ' . (int) $style->id)
)->execute();
}
}
}
catch (\Throwable $e)
{
// Silent
}
}
/**
* Enforce login support module URLs on admin requests.
*
* Checks the mod_loginsupport module params and corrects them if
* they have been changed away from the expected values.
*
* @return void
*
* @since 02.01.08
*/
protected function enforceLoginSupportUrls()
{
$expected = [
'forum_url' => 'https://mokoconsulting.tech/support',
'documentation_url' => 'https://mokoconsulting.tech/kb',
'news_url' => 'https://mokoconsulting.tech/news',
];
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('params')])
->from($db->quoteName('#__modules'))
->where($db->quoteName('module') . ' = '
. $db->quote('mod_loginsupport'));
$db->setQuery($query);
$modules = $db->loadObjectList();
if (empty($modules))
{
return;
}
foreach ($modules as $module)
{
$params = new \Joomla\Registry\Registry(
$module->params ?: '{}'
);
$needsFix = false;
foreach ($expected as $key => $url)
{
if ($params->get($key) !== $url)
{
$params->set($key, $url);
$needsFix = true;
}
}
if ($needsFix)
{
$update = $db->getQuery(true)
->update($db->quoteName('#__modules'))
->set($db->quoteName('params') . ' = '
. $db->quote($params->toString()))
->where($db->quoteName('id') . ' = '
. (int) $module->id);
$db->setQuery($update);
$db->execute();
}
}
}
// ------------------------------------------------------------------
// Tenant Restrictions (called from onAfterRoute)
@@ -4524,224 +4221,6 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
return $this->masterNames;
}
// ------------------------------------------------------------------
// Atum Template Branding (called from onAfterInitialise)
// ------------------------------------------------------------------
/**
* Enforce Atum admin template branding params.
*
* Sets logoBrandLarge, logoBrandSmall, loginLogo, and alt text
* in the Atum template style params. Uses the plugin's media
* folder as the image source. Only writes to DB when values
* have drifted.
*
* @return void
*
* @since 02.01.08
*/
protected function enforceAtumBranding()
{
$mediaBase = 'media/plg_system_mokowaas/';
// Logo params
$expected = [
'logoBrandLarge' => $mediaBase . 'logo.png',
'logoBrandSmall' => $mediaBase . 'favicon_256.png',
'loginLogo' => $mediaBase . 'logo.png',
'logoBrandLargeAlt' => '',
'logoBrandSmallAlt' => '',
'loginLogoAlt' => '',
'emptyLogoBrandLargeAlt' => '1',
'emptyLogoBrandSmallAlt' => '1',
'emptyLoginLogoAlt' => '1',
];
// Hardcoded color scheme
$primary = self::COLOR_PRIMARY;
$sidebar = self::COLOR_SIDEBAR;
$link = self::COLOR_LINK;
if (!empty($primary))
{
// Convert hex to HSL for Atum's hue param
$hsl = $this->hexToHsl($primary);
if ($hsl)
{
$expected['hue'] = sprintf(
'hsl(%d, %d%%, %d%%)',
$hsl[0], $hsl[1], $hsl[2]
);
}
$expected['special-color'] = $primary;
}
if (!empty($sidebar))
{
$expected['header-color'] = $sidebar;
}
if (!empty($link))
{
$expected['link-color'] = $link;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('params')])
->from($db->quoteName('#__template_styles'))
->where($db->quoteName('template') . ' = '
. $db->quote('atum'))
->where($db->quoteName('client_id') . ' = 1');
$db->setQuery($query);
$styles = $db->loadObjectList();
if (empty($styles))
{
return;
}
foreach ($styles as $style)
{
$params = new \Joomla\Registry\Registry(
$style->params ?: '{}'
);
$needsFix = false;
foreach ($expected as $key => $value)
{
if ($params->get($key) !== $value)
{
$params->set($key, $value);
$needsFix = true;
}
}
if ($needsFix)
{
$update = $db->getQuery(true)
->update($db->quoteName('#__template_styles'))
->set($db->quoteName('params') . ' = '
. $db->quote($params->toString()))
->where($db->quoteName('id') . ' = '
. (int) $style->id);
$db->setQuery($update);
$db->execute();
}
}
}
/**
* Convert a hex color to HSL values.
*
* @param string $hex Hex color (e.g., "#1a2744")
*
* @return array|null [hue, saturation%, lightness%] or null
*
* @since 02.01.08
*/
protected function hexToHsl($hex)
{
$hex = ltrim($hex, '#');
if (strlen($hex) !== 6)
{
return null;
}
$r = hexdec(substr($hex, 0, 2)) / 255;
$g = hexdec(substr($hex, 2, 2)) / 255;
$b = hexdec(substr($hex, 4, 2)) / 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$l = ($max + $min) / 2;
if ($max === $min)
{
return [0, 0, (int) round($l * 100)];
}
$d = $max - $min;
$s = $l > 0.5
? $d / (2 - $max - $min)
: $d / ($max + $min);
if ($max === $r)
{
$h = ($g - $b) / $d + ($g < $b ? 6 : 0);
}
elseif ($max === $g)
{
$h = ($b - $r) / $d + 2;
}
else
{
$h = ($r - $g) / $d + 4;
}
$h = $h / 6;
return [
(int) round($h * 360),
(int) round($s * 100),
(int) round($l * 100),
];
}
// ------------------------------------------------------------------
// Visual Branding (called from onBeforeCompileHead)
// ------------------------------------------------------------------
/**
* Replace the default favicon with a custom one.
*
* @param \Joomla\CMS\Document\HtmlDocument $doc
*
* @return void
*
* @since 02.01.08
*/
protected function injectFavicon($doc)
{
$mediaBase = 'media/plg_system_mokowaas/';
$root = Uri::root();
// Remove all existing favicon/icon links
foreach ($doc->_links as $href => $attrs)
{
if (isset($attrs['relation'])
&& strpos($attrs['relation'], 'icon') !== false)
{
unset($doc->_links[$href]);
}
}
// SVG favicon (modern browsers, preferred)
$doc->addHeadLink(
$root . $mediaBase . 'favicon.svg',
'icon',
'rel',
['type' => 'image/svg+xml']
);
// ICO fallback (legacy browsers)
$doc->addHeadLink(
$root . $mediaBase . 'favicon.ico',
'alternate icon',
'rel',
['type' => 'image/vnd.microsoft.icon']
);
// PNG for Apple/Android
$doc->addHeadLink(
$root . $mediaBase . 'favicon_256.png',
'apple-touch-icon',
'rel',
['sizes' => '256x256']
);
}
}
@@ -7,7 +7,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/Field/AllowedIpsField.php
* BRIEF: Custom form field that displays the current IP whitelist
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
@@ -7,7 +7,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/Field/CurrentIpField.php
* BRIEF: Read-only field that displays the current user's IP address
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/Field/DemoTaskInfoField.php
* BRIEF: Read-only field showing scheduled task info with link to manage it
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/Field/NextResetField.php
* BRIEF: Read-only field showing next reset time from Joomla scheduled task
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/Field/SnapshotTablesField.php
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
*/
@@ -52,7 +52,7 @@ final class MokoWaaSHelper
*
* @return array
*/
public static function getMasterUsernames(): array
private static function getMasterUsernames(): array
{
if (self::$masterNames !== null)
{
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
* VERSION: 02.32.38
* VERSION: 02.34.08
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
* VERSION: 02.32.38
* VERSION: 02.34.08
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/
@@ -10,7 +10,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
* VERSION: 02.32.38
* VERSION: 02.34.08
* BRIEF: Content-only snapshot/restore for demo site reset
*/
@@ -15,5 +15,5 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
@@ -15,5 +15,5 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
@@ -10,111 +10,109 @@
; Version: 02.01.08
; File: en-GB.override.ini
; Path: administrator/language/overrides/en-GB.override.ini
; Brief: Admin override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Admin language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_ATUM_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_ATUM_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Control panel greetings =====
COM_CPANEL_WELCOME_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_MSG_WELCOME="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_MSG_WELCOME="Welcome to MokoWaaS!"
; ===== Help/Docs phrasing =====
COM_ADMIN_HELP_SITE="{{BRAND_NAME}} Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="{{BRAND_NAME}} Help"
COM_ADMIN_HELP_SITE="MokoWaaS Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="MokoWaaS Help"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
COM_INSTALLER_TYPE_JOOMLA="{{BRAND_NAME}} Package"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
COM_INSTALLER_TYPE_JOOMLA="MokoWaaS Package"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== AdminLogin Support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit {{COMPANY_NAME}}:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to {{COMPANY_NAME}} support on the login screen."
TPL_ATUM_BACKEND_LOGIN="{{BRAND_NAME}} Administrator Login"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit Moko Consulting:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to Moko Consulting support on the login screen."
TPL_ATUM_BACKEND_LOGIN="MokoWaaS Administrator Login"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Admin-specific branding =====
COM_ADMIN_VIEW_HOME_TITLE="{{BRAND_NAME}} Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="{{BRAND_NAME}} Error: Save failed"
COM_ADMIN_VIEW_HOME_TITLE="MokoWaaS Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="MokoWaaS Error: Save failed"
; ===== Module list workaround (RegularLabs) =====
COM_MODULES_HEADING_POSITION="Position"
; ===== Extensions =====
COM_INSTALLER_TYPE_TYPE_JOOMLA="{{BRAND_NAME}}"
COM_INSTALLER_TYPE_TYPE_JOOMLA="MokoWaaS"
COM_INSTALLER_MSG_UPDATE_SUCCESS="Update installed successfully"
; ===== Dashboard =====
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Documentation</a></li><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in {{BRAND_NAME}}"
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Documentation</a></li><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in MokoWaaS"
; ===== Quick Icons =====
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="{{BRAND_NAME}} is up to date."
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="MokoWaaS is up to date."
; ===== System Info =====
COM_ADMIN_JOOMLA_VERSION="{{BRAND_NAME}} Version"
COM_ADMIN_HELP="{{BRAND_NAME}} Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="{{BRAND_NAME}} Backward Compatibility Plugin"
COM_ADMIN_JOOMLA_VERSION="MokoWaaS Version"
COM_ADMIN_HELP="MokoWaaS Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="MokoWaaS Backward Compatibility Plugin"
; ===== Installer =====
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install {{BRAND_NAME}} Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The {{BRAND_NAME}} package cannot be installed through the Extension Manager. Please use the {{BRAND_NAME}} Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The {{BRAND_NAME}} temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The {{BRAND_NAME}} temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your {{BRAND_NAME}} installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install MokoWaaS Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The MokoWaaS package cannot be installed through the Extension Manager. Please use the MokoWaaS Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The MokoWaaS temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The MokoWaaS temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your MokoWaaS installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
; ===== Global Configuration =====
COM_CONFIG_FIELD_METAVERSION_LABEL="{{BRAND_NAME}} Version"
COM_CONFIG_FIELD_METAVERSION_LABEL="MokoWaaS Version"
; ===== Update component =====
COM_JOOMLAUPDATE_CONFIGURATION="{{BRAND_NAME}} Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="{{BRAND_NAME}} Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where {{BRAND_NAME}} gets its update information from."
COM_JOOMLAUPDATE_CONFIGURATION="MokoWaaS Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="MokoWaaS Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where MokoWaaS gets its update information from."
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="{{BRAND_NAME}} Update Component"
COM_JOOMLAUPDATE_NOCHANGE="{{BRAND_NAME}} is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="{{BRAND_NAME}} Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="MokoWaaS Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="MokoWaaS Update Component"
COM_JOOMLAUPDATE_NOCHANGE="MokoWaaS is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="MokoWaaS Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="MokoWaaS Update"
COM_JOOMLAUPDATE_LIVEUPDATE="Live Update"
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for {{BRAND_NAME}} updates."
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for MokoWaaS updates."
; ===== Privacy =====
COM_PRIVACY_HEADING_CORE_CAPABILITIES="{{BRAND_NAME}} Core Capabilities"
COM_PRIVACY_HEADING_CORE_CAPABILITIES="MokoWaaS Core Capabilities"
; ===== Database & Library errors =====
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum {{BRAND_NAME}} version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find {{BRAND_NAME}} XML setup file."
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum MokoWaaS version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find MokoWaaS XML setup file."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ADMIN_HELP_DOCUMENTATION="{{BRAND_NAME}} Documentation"
COM_ADMIN_HELP_SUPPORT="{{BRAND_NAME}} Support"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
COM_ADMIN_HELP_DOCUMENTATION="MokoWaaS Documentation"
COM_ADMIN_HELP_SUPPORT="MokoWaaS Support"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
@@ -10,111 +10,109 @@
; Version: 02.01.08
; File: en-US.override.ini
; Path: administrator/language/overrides/en-US.override.ini
; Brief: Admin override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Admin language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_ATUM_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_ATUM_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Control panel greetings =====
COM_CPANEL_WELCOME_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_MSG_WELCOME="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_MSG_WELCOME="Welcome to MokoWaaS!"
; ===== Help/Docs phrasing =====
COM_ADMIN_HELP_SITE="{{BRAND_NAME}} Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="{{BRAND_NAME}} Help"
COM_ADMIN_HELP_SITE="MokoWaaS Help"
COM_ADMIN_HELPSITE_FIELD_LABEL="MokoWaaS Help"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
COM_INSTALLER_TYPE_JOOMLA="{{BRAND_NAME}} Package"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
COM_INSTALLER_TYPE_JOOMLA="MokoWaaS Package"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== AdminLogin Support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit {{COMPANY_NAME}}:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to {{COMPANY_NAME}} support on the login screen."
TPL_ATUM_BACKEND_LOGIN="{{BRAND_NAME}} Administrator Login"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit Moko Consulting:"
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to Moko Consulting support on the login screen."
TPL_ATUM_BACKEND_LOGIN="MokoWaaS Administrator Login"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Admin-specific branding =====
COM_ADMIN_VIEW_HOME_TITLE="{{BRAND_NAME}} Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="{{BRAND_NAME}} Error: Save failed"
COM_ADMIN_VIEW_HOME_TITLE="MokoWaaS Control Panel"
JLIB_APPLICATION_ERROR_SAVE_FAILED="MokoWaaS Error: Save failed"
; ===== Module list workaround (RegularLabs) =====
COM_MODULES_HEADING_POSITION="Position"
; ===== Extensions =====
COM_INSTALLER_TYPE_TYPE_JOOMLA="{{BRAND_NAME}}"
COM_INSTALLER_TYPE_TYPE_JOOMLA="MokoWaaS"
COM_INSTALLER_MSG_UPDATE_SUCCESS="Update installed successfully"
; ===== Dashboard =====
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to {{BRAND_NAME}}!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Documentation</a></li><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in {{BRAND_NAME}}"
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to MokoWaaS!"
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Documentation</a></li><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Support</a></li></ul>"
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in MokoWaaS"
; ===== Quick Icons =====
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown {{BRAND_NAME}}…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="{{BRAND_NAME}} is up to date."
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown MokoWaaS…"
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="MokoWaaS is up to date."
; ===== System Info =====
COM_ADMIN_JOOMLA_VERSION="{{BRAND_NAME}} Version"
COM_ADMIN_HELP="{{BRAND_NAME}} Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="{{BRAND_NAME}} Backward Compatibility Plugin"
COM_ADMIN_JOOMLA_VERSION="MokoWaaS Version"
COM_ADMIN_HELP="MokoWaaS Help"
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="MokoWaaS Backward Compatibility Plugin"
; ===== Installer =====
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install {{BRAND_NAME}} Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The {{BRAND_NAME}} package cannot be installed through the Extension Manager. Please use the {{BRAND_NAME}} Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The {{BRAND_NAME}} temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The {{BRAND_NAME}} temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your {{BRAND_NAME}} installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install MokoWaaS Extension"
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The MokoWaaS package cannot be installed through the Extension Manager. Please use the MokoWaaS Update component to update."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The MokoWaaS temporary folder is not set."
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The MokoWaaS temporary folder is not writable or does not exist."
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your MokoWaaS installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
; ===== Global Configuration =====
COM_CONFIG_FIELD_METAVERSION_LABEL="{{BRAND_NAME}} Version"
COM_CONFIG_FIELD_METAVERSION_LABEL="MokoWaaS Version"
; ===== Update component =====
COM_JOOMLAUPDATE_CONFIGURATION="{{BRAND_NAME}} Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="{{BRAND_NAME}} Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where {{BRAND_NAME}} gets its update information from."
COM_JOOMLAUPDATE_CONFIGURATION="MokoWaaS Update: Options"
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="MokoWaaS Next"
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where MokoWaaS gets its update information from."
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="{{BRAND_NAME}} Update Component"
COM_JOOMLAUPDATE_NOCHANGE="{{BRAND_NAME}} is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="{{BRAND_NAME}} Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="{{BRAND_NAME}} Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="MokoWaaS Update"
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="MokoWaaS Update Component"
COM_JOOMLAUPDATE_NOCHANGE="MokoWaaS is up to date."
COM_JOOMLAUPDATE_PREUPDATE_CHECK="MokoWaaS Pre-Update Check"
COM_JOOMLAUPDATE_UPDATE_HEADER="MokoWaaS Update"
COM_JOOMLAUPDATE_LIVEUPDATE="Live Update"
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for {{BRAND_NAME}} updates."
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for MokoWaaS updates."
; ===== Privacy =====
COM_PRIVACY_HEADING_CORE_CAPABILITIES="{{BRAND_NAME}} Core Capabilities"
COM_PRIVACY_HEADING_CORE_CAPABILITIES="MokoWaaS Core Capabilities"
; ===== Database & Library errors =====
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum {{BRAND_NAME}} version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find {{BRAND_NAME}} XML setup file."
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum MokoWaaS version requirement of J%s"
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find MokoWaaS XML setup file."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ADMIN_HELP_DOCUMENTATION="{{BRAND_NAME}} Documentation"
COM_ADMIN_HELP_SUPPORT="{{BRAND_NAME}} Support"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
COM_ADMIN_HELP_DOCUMENTATION="MokoWaaS Documentation"
COM_ADMIN_HELP_SUPPORT="MokoWaaS Support"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
@@ -16,8 +16,8 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL="Enable Branding"
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_DESC="Enable or disable the branding overrides across the system."
@@ -111,7 +111,7 @@ PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The health_api_token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The heartbeat token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
@@ -121,7 +121,7 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for externa
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_LABEL="Enable Health Endpoint"
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_DESC="Expose a JSON health check endpoint at <code>/?mokowaas=health</code>. Requires a valid API token. A random token is generated automatically when enabled."
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Health API Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter."
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_LABEL="Grafana URL"
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_DESC="Base URL of your Grafana instance (e.g. <code>https://grafana.example.com</code>). When provided along with an API key, the plugin will auto-provision a datasource and dashboard in Grafana when the health endpoint is enabled."
@@ -15,5 +15,5 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
@@ -16,8 +16,8 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL="Enable Branding"
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_DESC="Enable or disable the branding overrides across the system."
@@ -111,7 +111,7 @@ PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The health_api_token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The heartbeat token from the remote site's MokoWaaS plugin settings."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
@@ -121,7 +121,7 @@ PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for externa
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_LABEL="Enable Health Endpoint"
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_DESC="Expose a JSON health check endpoint at <code>/?mokowaas=health</code>. Requires a valid API token. A random token is generated automatically when enabled."
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Health API Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer &lt;token&gt;</code> header or <code>&amp;token=&lt;value&gt;</code> query parameter."
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_LABEL="Grafana URL"
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_DESC="Base URL of your Grafana instance (e.g. <code>https://grafana.example.com</code>). When provided along with an API key, the plugin will auto-provision a datasource and dashboard in Grafana when the health endpoint is enabled."
@@ -15,5 +15,5 @@
; Variables: (none)
; -----------------------------------------------------------------------------
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
@@ -10,38 +10,36 @@
; Version: 02.01.08
; File: en-GB.override.ini
; Path: language/overrides/en-GB.override.ini
; Brief: Site/frontend override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Site/frontend language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Installer / Sample data =====
INSTL_SITE_NAME_LABEL="{{BRAND_NAME}} Site Name"
INSTL_SAMPLE_BLOG_SET="{{BRAND_NAME}} Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="{{BRAND_NAME}} Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="{{BRAND_NAME}} Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="{{BRAND_NAME}} Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="{{BRAND_NAME}} Sample Data - Testing"
INSTL_SITE_NAME_LABEL="MokoWaaS Site Name"
INSTL_SAMPLE_BLOG_SET="MokoWaaS Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="MokoWaaS Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="MokoWaaS Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="MokoWaaS Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="MokoWaaS Sample Data - Testing"
; ===== Login support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
; ===== Site offline =====
JOFFLINE_MESSAGE="This site is down for maintenance.<br>Please check back again soon."
@@ -52,15 +50,15 @@ JERROR_AN_ERROR_HAS_OCCURRED="An error has occurred."
JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND="Component not found."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
@@ -10,38 +10,36 @@
; Version: 02.01.08
; File: en-US.override.ini
; Path: language/overrides/en-US.override.ini
; Brief: Site/frontend override TEMPLATE — placeholders resolved at runtime/install.
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
; Brief: Site/frontend language overrides — values are hardcoded.
; -----------------------------------------------------------------------------
; ===== Footer & template branding =====
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Generic replacements =====
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
LIB_JOOMLA="{{BRAND_NAME}} Library"
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
LIB_JOOMLA="MokoWaaS Library"
; ===== System messages =====
JERROR_JOOMLA="{{BRAND_NAME}} Error"
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
JERROR_JOOMLA="MokoWaaS Error"
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
; ===== Error messages =====
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
; ===== Installer / Sample data =====
INSTL_SITE_NAME_LABEL="{{BRAND_NAME}} Site Name"
INSTL_SAMPLE_BLOG_SET="{{BRAND_NAME}} Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="{{BRAND_NAME}} Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="{{BRAND_NAME}} Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="{{BRAND_NAME}} Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="{{BRAND_NAME}} Sample Data - Testing"
INSTL_SITE_NAME_LABEL="MokoWaaS Site Name"
INSTL_SAMPLE_BLOG_SET="MokoWaaS Sample Data - Blog"
INSTL_SAMPLE_BROCHURE_SET="MokoWaaS Sample Data - Brochure Site"
INSTL_SAMPLE_DATA_SET="MokoWaaS Sample Data - Default"
INSTL_SAMPLE_LEARN_SET="MokoWaaS Sample Data - Learn"
INSTL_SAMPLE_TESTING_SET="MokoWaaS Sample Data - Testing"
; ===== Login support =====
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
; ===== Site offline =====
JOFFLINE_MESSAGE="This site is down for maintenance.<br>Please check back again soon."
@@ -52,15 +50,15 @@ JERROR_AN_ERROR_HAS_OCCURRED="An error has occurred."
JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND="Component not found."
; ===== Version and About =====
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
; ===== Akeeba Ticket System (ATS) =====
COM_ATS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
COM_ATS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
@@ -16,13 +16,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.04
VERSION: 02.34.00
PATH: /src/mokowaas.xml
BRIEF: Plugin manifest for MokoWaaS system plugin
NOTE: Defines installation metadata, files, and configuration for Joomla
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoWaaS</name>
<name>System - MokoWaaS Core</name>
<element>mokowaas</element>
<author>Moko Consulting</author>
<creationDate>2026-05-22</creationDate>
@@ -30,8 +30,8 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.38</version>
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
<version>02.34.08-dev</version>
<description>MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile>
+2 -73
View File
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.32.38
* VERSION: 02.34.08
* PATH: /src/script.php
* BRIEF: Installation script for MokoWaaS plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
@@ -127,7 +127,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
$this->ensureMokoCassiopeia();
$this->installLanguageOverrides();
$this->updateLoginSupportUrls();
$this->updateAtumBranding();
$this->registerActionLogExtension();
$this->provisionHealthEndpoint();
$this->sendInstallNotification($type);
@@ -552,7 +551,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
$params = $this->getPluginParams();
return [
'{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'),
'{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'),
'{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech/support'),
];
@@ -696,7 +694,7 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
$supportUrls = [
'forum_url' => 'https://mokoconsulting.tech/support',
'documentation_url' => 'https://mokoconsulting.tech/kb',
'documentation_url' => 'https://mokoconsulting.tech/support/products',
'news_url' => 'https://mokoconsulting.tech/news',
];
@@ -727,75 +725,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
);
}
/**
* Set Atum admin template branding params at install time.
*
* @return void
*
* @since 02.01.08
*/
private function updateAtumBranding()
{
$mediaBase = 'media/plg_system_mokowaas/';
$expected = [
'logoBrandLarge' => $mediaBase . 'logo.png',
'logoBrandSmall' => $mediaBase . 'favicon_256.png',
'loginLogo' => $mediaBase . 'logo.png',
'logoBrandLargeAlt' => '',
'logoBrandSmallAlt' => '',
'loginLogoAlt' => '',
'emptyLogoBrandLargeAlt' => '1',
'emptyLogoBrandSmallAlt' => '1',
'emptyLoginLogoAlt' => '1',
'hue' => 'hsl(219, 44%, 18%)',
'special-color' => '#1a2744',
'link-color' => '#0051ad',
];
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('params')])
->from($db->quoteName('#__template_styles'))
->where($db->quoteName('template') . ' = '
. $db->quote('atum'))
->where($db->quoteName('client_id') . ' = 1');
$db->setQuery($query);
$styles = $db->loadObjectList();
if (empty($styles))
{
return;
}
foreach ($styles as $style)
{
$params = new \Joomla\Registry\Registry(
$style->params ?: '{}'
);
foreach ($expected as $key => $value)
{
$params->set($key, $value);
}
$update = $db->getQuery(true)
->update($db->quoteName('#__template_styles'))
->set($db->quoteName('params') . ' = '
. $db->quote($params->toString()))
->where($db->quoteName('id') . ' = '
. (int) $style->id);
$db->setQuery($update);
$db->execute();
}
Factory::getApplication()->enqueueMessage(
'Updated Atum template branding.', 'message'
);
}
/**
* Register the plugin in #__action_logs_extensions so it appears
* as a filterable extension in System > Action Logs.

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