Compare commits

..

69 Commits

Author SHA1 Message Date
Jonathan Miller d1ee2ef3f4 fix: API controller execute() signatures compatible with BaseController (#183)
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
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (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
All API controllers now accept the $task parameter required by
Joomla\CMS\MVC\Controller\BaseController::execute($task).

Closes #183

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:25:18 -05:00
Jonathan Miller 7f9b59a36d chore: update updates.xml for 02.34.10-dev
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:21:50 -05:00
Jonathan Miller 79047e37b5 chore: bump version to 02.34.10-dev
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:08:03 -05:00
Jonathan Miller 3d5f9346c6 chore: bump version to 02.34.09-dev
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:04:35 -05:00
Jonathan Miller 93c82a9cee refactor: strip core plugin to heartbeat-only config
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
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Remove branding, security hardening, demo mode, content sync, and API
actions from the core plugin. Config now shows only the heartbeat token.
Extension class reduced from 4226 to 2051 lines. Deleted 5 Field classes,
3 Service classes, 2 form XMLs, and media assets. Core retains health
checks, Grafana provisioning, site aliases, and extension cascade.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 05:47:13 -05:00
Jonathan Miller 384b8824c6 refactor: remove tpl_mokoonyx submodule
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
- .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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
- 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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.2) (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (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
- 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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)
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
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
Generic: Repo Health / Access control (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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
- 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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
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 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
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
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
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
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
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 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
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (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
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:
#	.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
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
Generic: Repo Health / Site Health (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (push) Has been cancelled
Generic: Repo Health / Access control (pull_request) Has been cancelled
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Universal: Build & Release / Promote to RC (pull_request) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Has been cancelled
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
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
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
- 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
jmiller c5aef3c939 chore: sync updates.xml SHA256 from dev [skip ci] 2026-06-04 17:57:03 +00:00
jmiller 0d96174f75 chore: shorten update server name [skip ci] 2026-06-04 17:20:12 +00:00
jmiller 6acae6d20f chore: update server pretty name [skip ci] 2026-06-04 17:15:56 +00:00
jmiller 9dacc01a67 chore: recreate updates.xml [skip ci] 2026-06-04 16:59:51 +00: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
92 changed files with 1386 additions and 6425 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.33.01</version>
<version>02.34.10</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 -318
View File
@@ -1,318 +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 }}" \
--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: |
# 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 }}" \
--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 /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.33.01
# VERSION: 02.34.10
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+12 -68
View File
@@ -66,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" \
@@ -96,31 +95,23 @@ jobs:
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Read current version (bump already handled by push workflow)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
# Strip any existing suffix from version before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# RC and stable consolidate dev patches into a clean minor bump
# e.g. 02.33.15 → 02.34.00 (not 02.33.15-rc)
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate)
MAJOR=$(echo "$VERSION" | cut -d. -f1)
MINOR=$(echo "$VERSION" | cut -d. -f2)
MINOR=$(printf "%02d" $((10#$MINOR + 1)))
VERSION="${MAJOR}.${MINOR}.00"
;;
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 "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
# Verify version consistency across all files
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Update VERSION variable with suffix
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
@@ -212,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
+2 -1
View File
@@ -14,7 +14,7 @@
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md
VERSION: 02.33.01
VERSION: 02.34.10
BRIEF: Version history using `Keep a Changelog`
-->
@@ -41,6 +41,7 @@
### 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)
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.01
VERSION: 02.34.10
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
VERSION: 02.33.01
VERSION: 02.34.10
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.33.01
VERSION: 02.34.10
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.33.01
VERSION: 02.34.10
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.33.01
VERSION: 02.34.10
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.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Build Guide (VERSION: 02.34.10)
## 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.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Configuration Guide (VERSION: 02.34.10)
## 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.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Installation Guide (VERSION: 02.34.10)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Operations Guide (VERSION: 02.34.10)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.10)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Testing Guide (VERSION: 02.34.10)
## 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.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.10)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.10)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.01
VERSION: 02.34.10
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.33.01)
# MokoWaaS Documentation Index (VERSION: 02.34.10)
## 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.33.01
VERSION: 02.34.10
BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoWaaS Plugin Overview (VERSION: 02.33.01)
# MokoWaaS Plugin Overview (VERSION: 02.34.10)
## 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.33.01
VERSION: 02.34.10
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
@@ -580,12 +580,40 @@ class DisplayController extends BaseController
return;
}
$input = Factory::getApplication()->getInput();
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$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),
$input->getString('action', 'deny')
$action
));
}
@@ -267,48 +267,18 @@ class DashboardModel extends BaseDatabaseModel
{
try
{
// Purge all file-based cache directories
$root = JPATH_ROOT;
$dirs = [
$root . '/cache',
$root . '/administrator/cache',
// Use Joomla's native cache API — same as com_cache
$cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class);
$cache->createCacheController('', ['defaultgroup' => ''])->cache->clean('');
// Also clean admin cache
$conf = Factory::getApplication()->get('cache_handler', 'file');
$options = [
'defaultgroup' => '',
'cachebase' => JPATH_ADMINISTRATOR . '/cache',
'storage' => $conf,
];
foreach ($dirs as $dir)
{
if (!is_dir($dir))
{
continue;
}
$it = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $file)
{
$name = $file->getFilename();
if ($name === 'index.html' || $name === '.htaccess')
{
continue;
}
if ($file->isDir())
{
@rmdir($file->getPathname());
}
else
{
@unlink($file->getPathname());
}
}
}
// Also run Joomla's built-in cache GC for non-file handlers
Factory::getCache('', '')->gc();
Factory::getCache('', '', 'administrator')->gc();
$cache->createCacheController('', $options)->cache->clean('');
// Clear opcache if available
if (\function_exists('opcache_reset'))
@@ -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>
@@ -53,6 +53,63 @@ $typeBadge = [
</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">
@@ -158,6 +215,32 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
// 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() {
@@ -29,7 +29,7 @@ class CacheController extends BaseController
*
* @since 1.0.0
*/
public function execute(): void
public function execute($task = 'cache'): void
{
$app = Factory::getApplication();
@@ -42,7 +42,7 @@ class InstallController extends BaseController
*
* @since 02.21.00
*/
public function execute(): void
public function execute($task = 'install'): void
{
$app = Factory::getApplication();
@@ -104,7 +104,7 @@ class PluginsController extends BaseController
*
* @return void
*/
public function execute(): void
public function execute($task = 'plugins'): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
@@ -35,7 +35,7 @@ class ResetController extends BaseController
*
* @since 02.21.00
*/
public function execute(): void
public function execute($task = 'reset'): void
{
$app = Factory::getApplication();
@@ -68,7 +68,7 @@ class SnapshotController extends BaseController
*
* @since 02.21.00
*/
public function execute(): void
public function execute($task = 'snapshot'): void
{
$app = Factory::getApplication();
@@ -26,7 +26,7 @@ use Joomla\Registry\Registry;
*/
class SyncController extends BaseController
{
public function execute(): void
public function execute($task = 'sync'): void
{
$app = Factory::getApplication();
@@ -24,7 +24,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
*/
class SyncReceiveController extends BaseController
{
public function execute(): void
public function execute($task = 'syncReceive'): void
{
$app = Factory::getApplication();
@@ -29,7 +29,7 @@ class UpdateController extends BaseController
*
* @since 1.0.0
*/
public function execute(): void
public function execute($task = 'update'): void
{
$app = Factory::getApplication();
+2 -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.10
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.33.01-dev</version>
<version>02.34.10</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>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>MOD_MOKOWAAS_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCache</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>MOD_MOKOWAAS_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCpanel</namespace>
@@ -58,35 +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; ?>
<?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="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">
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</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>
+148 -23
View File
@@ -2,45 +2,130 @@
/**
* MokoWaaS Admin Sidebar Menu
*
* Uses native Joomla admin menu classes (MetisMenu) so it renders
* identically to Joomla's own sidebar menu items.
* 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;
$items = [
['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'],
];
$app = \Joomla\CMS\Factory::getApplication();
$app = Factory::getApplication();
$currentOption = $app->getInput()->get('option', '');
$currentView = $app->getInput()->get('view', '');
// Determine if any child is active (auto-expand)
$anyActive = ($currentOption === 'com_mokowaas');
$parentClass = 'item parent item-level-1' . ($anyActive ? ' mm-active' : '');
$collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : '');
// ── 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 $parentClass; ?>">
<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 $collapseClass; ?>">
<?php foreach ($items as $item): ?>
<ul class="<?php echo $topCollapse; ?>" style="padding-inline-start:0.5rem;">
<?php // ── MokoWaaS static items ── ?>
<?php foreach ($mokowaasItems as $item): ?>
<?php
$active = false;
$parsed = [];
@@ -51,7 +136,7 @@ $collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : '')
? ($currentView === '' || $currentView === 'dashboard')
: ($currentView === ($parsed['view'] ?? ''));
}
$liClass = 'item item-level-2' . ($active ? ' mm-active' : '');
$liClass = 'item mokowaas-menu-item' . ($active ? ' mm-active' : '');
$aClass = 'no-dropdown' . ($active ? ' mm-active' : '');
?>
<li class="<?php echo $liClass; ?>">
@@ -61,6 +146,46 @@ $collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : '')
</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>
File diff suppressed because it is too large Load Diff
@@ -1,72 +0,0 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.33.01
* PATH: /src/Field/AllowedIpsField.php
* BRIEF: Custom form field that displays the current IP whitelist
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
class AllowedIpsField extends FormField
{
protected $type = 'AllowedIps';
protected function getInput()
{
$config = Factory::getApplication()->getConfig();
$allowedRaw = $config->get('mokowaas_allowed_ips', '');
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
if (empty($allowedRaw))
{
$status = '<span class="badge bg-danger">Not configured</span>';
$ipList = '<em>No IPs set — emergency access is blocked.</em>';
}
else
{
$ips = array_map('trim', explode(',', $allowedRaw));
$status = '<span class="badge bg-success">'
. count($ips) . ' IP(s) configured</span>';
$ipItems = [];
foreach ($ips as $ip)
{
$match = ($ip === $currentIp)
? ' <span class="badge bg-info">your IP</span>'
: '';
$ipItems[] = '<code>' . htmlspecialchars($ip)
. '</code>' . $match;
}
$ipList = implode(', ', $ipItems);
}
$yourIp = '<code>' . htmlspecialchars($currentIp) . '</code>';
return '<div class="alert alert-info mb-0">'
. '<strong>IP Whitelist:</strong> ' . $status . '<br>'
. '<strong>Allowed IPs:</strong> ' . $ipList . '<br>'
. '<strong>Your current IP:</strong> ' . $yourIp . '<br>'
. '<small class="text-muted">Set <code>public '
. '$mokowaas_allowed_ips = \'1.2.3.4,5.6.7.8\';</code>'
. ' in configuration.php to change.</small>'
. '</div>';
}
protected function getLabel()
{
return '';
}
}
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.33.01
* VERSION: 02.34.10
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
@@ -1,40 +0,0 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.33.01
* PATH: /src/Field/CurrentIpField.php
* BRIEF: Read-only field that displays the current user's IP address
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
class CurrentIpField extends FormField
{
protected $type = 'CurrentIp';
protected function getInput()
{
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
return '<div class="alert alert-info mb-0 py-2">'
. '<strong>Your current IP:</strong> '
. '<code>' . htmlspecialchars($currentIp) . '</code> '
. '<small class="text-muted">&mdash; add this to the table below to keep your session alive.</small>'
. '</div>';
}
protected function getLabel()
{
return '';
}
}
@@ -1,237 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.33.01
* PATH: /src/Field/DemoTaskInfoField.php
* BRIEF: Read-only field showing scheduled task info with link to manage it
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Router\Route;
/**
* Displays the demo reset scheduled task status: schedule, next run,
* last run, and a direct link to edit the task in Joomla's Scheduler.
*
* @since 02.29.00
*/
class DemoTaskInfoField extends FormField
{
protected $type = 'DemoTaskInfo';
protected function getInput()
{
// Query the scheduled task — if it exists and is enabled, demo mode is on
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
$newTaskLink = Route::_('index.php?option=com_scheduler&task=task.add');
if (!$task)
{
return '<div class="alert alert-info mb-0 py-2">'
. 'No demo reset task configured. '
. '<a href="' . $newTaskLink . '" class="alert-link">Create a Scheduled Task</a> '
. 'and select <strong>MokoWaaS Demo Reset</strong> to enable demo mode.</div>';
}
$taskId = (int) $task['id'];
$state = (int) $task['state'];
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
// Parse schedule from execution_rules
$rules = json_decode($task['execution_rules'] ?? '{}', true);
$ruleType = $rules['rule-type'] ?? '';
switch ($ruleType)
{
case 'cron-expression':
$schedule = $rules['cron-expression'] ?? '';
$friendlySchedule = $this->friendlySchedule($schedule);
break;
case 'interval-minutes':
$mins = (int) ($rules['interval-minutes'] ?? 0);
if ($mins >= 1440 && $mins % 1440 === 0)
{
$days = $mins / 1440;
$schedule = 'Every ' . $days . ' day' . ($days > 1 ? 's' : '');
}
elseif ($mins >= 60 && $mins % 60 === 0)
{
$hours = $mins / 60;
$schedule = 'Every ' . $hours . ' hour' . ($hours > 1 ? 's' : '');
}
else
{
$schedule = 'Every ' . $mins . ' minute' . ($mins !== 1 ? 's' : '');
}
$friendlySchedule = $schedule;
break;
case 'interval-hours':
$hours = (int) ($rules['interval-hours'] ?? 0);
$schedule = 'Every ' . $hours . ' hour' . ($hours !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
case 'interval-days':
$days = (int) ($rules['interval-days'] ?? 0);
$schedule = 'Every ' . $days . ' day' . ($days !== 1 ? 's' : '');
$friendlySchedule = $schedule;
break;
default:
$schedule = $ruleType ?: 'Not set';
$friendlySchedule = 'Custom';
}
// Next execution
$nextExec = $task['next_execution'] ?? '';
$nextFormatted = 'Not scheduled';
$nextBadge = '';
if (!empty($nextExec) && $nextExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($nextExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$nextFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$nextFormatted = $nextExec;
}
$diff = strtotime($nextExec . ' UTC') - time();
if ($diff <= 0)
{
$nextBadge = '<span class="badge bg-warning text-dark">DUE</span>';
}
elseif ($diff < 3600)
{
$nextBadge = '<span class="badge bg-info">in ' . (int) ceil($diff / 60) . ' min</span>';
}
elseif ($diff < 86400)
{
$nextBadge = '<span class="badge bg-info">in ' . round($diff / 3600, 1) . 'h</span>';
}
else
{
$nextBadge = '<span class="badge bg-secondary">in ' . round($diff / 86400, 1) . 'd</span>';
}
}
// Last execution
$lastExec = $task['last_execution'] ?? '';
$lastFormatted = 'Never';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$dt = new \DateTime($lastExec, new \DateTimeZone('UTC'));
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$lastFormatted = $dt->format('M j, Y g:i A T');
}
catch (\Throwable $e)
{
$lastFormatted = $lastExec;
}
}
// State badge
$stateBadge = $state === 1
? '<span class="badge bg-success">Enabled</span>'
: '<span class="badge bg-danger">Disabled</span>';
// Link to edit the task
$editLink = Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $taskId);
// Task params — default to On when keys are missing (matches form defaults)
$taskParams = json_decode($task['params'] ?? '{}', true) ?: [];
$bannerOn = !isset($taskParams['banner_enabled']) || (int) $taskParams['banner_enabled'] === 1;
$mediaOn = !isset($taskParams['include_media']) || (int) $taskParams['include_media'] === 1;
$countdownOn = !isset($taskParams['show_countdown']) || (int) $taskParams['show_countdown'] === 1;
// Check if snapshot exists
$snapshotExists = is_dir(JPATH_ROOT . '/mokowaas-snapshots/default');
// Build info card
return '<div class="card card-body bg-light py-2 px-3 mb-0">'
. '<table class="table table-sm table-borderless mb-1" style="max-width:550px">'
. '<tr><td class="text-muted" style="width:130px">Status</td><td>' . $stateBadge . '</td></tr>'
. '<tr><td class="text-muted">Schedule</td><td>' . htmlspecialchars($friendlySchedule) . '</td></tr>'
. '<tr><td class="text-muted">Next Reset</td><td>' . htmlspecialchars($nextFormatted) . ' ' . $nextBadge . '</td></tr>'
. '<tr><td class="text-muted">Last Reset</td><td>' . htmlspecialchars($lastFormatted) . '</td></tr>'
. '<tr><td class="text-muted">Runs</td><td>' . (int) ($task['times_executed'] ?? 0) . ' executed, ' . (int) ($task['times_failed'] ?? 0) . ' failed</td></tr>'
. '<tr><td class="text-muted">Baseline</td><td>' . ($snapshotExists ? '<span class="badge bg-success">Saved</span>' : '<span class="badge bg-warning text-dark">Not taken yet</span>') . '</td></tr>'
. '<tr><td class="text-muted">Banner</td><td>' . ($bannerOn ? 'On' : 'Off') . ($countdownOn ? ' + countdown' : '') . '</td></tr>'
. '<tr><td class="text-muted">Images</td><td>' . ($mediaOn ? 'Included' : 'Excluded') . '</td></tr>'
. '</table>'
. '<a href="' . $editLink . '" class="btn btn-sm btn-outline-primary">'
. '<span class="icon-cog" aria-hidden="true"></span> Manage Scheduled Task</a>'
. '</div>';
}
protected function getLabel()
{
return '<label class="form-label"><strong>Scheduled Reset</strong></label>';
}
/**
* Convert a cron expression to a human-readable string.
*
* @param string $cron Cron expression
*
* @return string
*/
private function friendlySchedule(string $cron): string
{
$map = [
'* * * * *' => 'Every minute',
'*/5 * * * *' => 'Every 5 minutes',
'*/15 * * * *' => 'Every 15 minutes',
'*/30 * * * *' => 'Every 30 minutes',
'0 */1 * * *' => 'Every hour',
'0 */4 * * *' => 'Every 4 hours',
'0 */6 * * *' => 'Every 6 hours',
'0 */12 * * *' => 'Every 12 hours',
'0 0 * * *' => 'Daily at midnight',
'0 6 * * *' => 'Daily at 6:00 AM',
'0 0 * * 0' => 'Weekly (Sunday)',
'0 0 1 * *' => 'Monthly (1st)',
];
return $map[$cron] ?? 'Custom';
}
}
@@ -1,156 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.33.01
* PATH: /src/Field/NextResetField.php
* BRIEF: Read-only field showing next reset time from Joomla scheduled task
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Pulls the next execution time directly from the Joomla scheduled task
* (#__scheduler_tasks) and displays it formatted in the site timezone.
*
* @since 02.29.00
*/
class NextResetField extends FormField
{
protected $type = 'NextReset';
protected function getInput()
{
// Check if demo mode is enabled
$demoEnabled = false;
if ($this->form)
{
$demoEnabled = (int) $this->form->getValue('demo_mode_enabled', 'params', 0) === 1;
}
if (!$demoEnabled)
{
return '<span class="form-control-plaintext text-muted">Demo mode is off</span>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Query the actual next_execution from the scheduled task
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select([
$db->quoteName('next_execution'),
$db->quoteName('last_execution'),
$db->quoteName('state'),
])
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
$db->setQuery($query);
$task = $db->loadAssoc();
}
catch (\Throwable $e)
{
$task = null;
}
if (!$task)
{
return '<div class="alert alert-secondary mb-0 py-2">No scheduled task found — save to create one automatically.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
if ((int) $task['state'] !== 1)
{
return '<div class="alert alert-warning mb-0 py-2">Scheduled task is disabled.</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
$nextExec = $task['next_execution'];
$lastExec = $task['last_execution'];
if (empty($nextExec) || $nextExec === '0000-00-00 00:00:00')
{
return '<div class="alert alert-secondary mb-0 py-2">Waiting for first run...</div>'
. '<input type="hidden" name="' . $this->name . '" value="" />';
}
// Convert to site timezone
$utcTimestamp = strtotime($nextExec);
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
try
{
$dt = new \DateTime('@' . $utcTimestamp);
$dt->setTimezone(new \DateTimeZone($siteTimezone));
$formatted = $dt->format('l, F j, Y \a\t g:i A T');
}
catch (\Throwable $e)
{
$formatted = $nextExec . ' UTC';
}
// Relative time
$diff = $utcTimestamp - time();
$relative = '';
if ($diff <= 0)
{
$relative = '<span class="badge bg-warning text-dark">overdue</span>';
}
elseif ($diff < 3600)
{
$mins = (int) ceil($diff / 60);
$relative = '<span class="badge bg-info">in ' . $mins . ' min</span>';
}
elseif ($diff < 86400)
{
$hours = round($diff / 3600, 1);
$relative = '<span class="badge bg-info">in ' . $hours . 'h</span>';
}
else
{
$days = round($diff / 86400, 1);
$relative = '<span class="badge bg-secondary">in ' . $days . 'd</span>';
}
// Last run info
$lastInfo = '';
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
{
try
{
$lastDt = new \DateTime($lastExec);
$lastDt->setTimezone(new \DateTimeZone($siteTimezone));
$lastInfo = '<small class="text-muted ms-2">Last run: ' . $lastDt->format('M j, g:i A') . '</small>';
}
catch (\Throwable $e)
{
// skip
}
}
return '<div class="d-flex align-items-center gap-2 flex-wrap">'
. '<span class="form-control-plaintext" style="font-weight:500">'
. '<span class="icon-calendar" aria-hidden="true"></span> '
. htmlspecialchars($formatted) . '</span> '
. $relative
. $lastInfo
. '<input type="hidden" name="' . $this->name . '" value="' . htmlspecialchars($nextExec) . '" />'
. '</div>';
}
}
@@ -1,175 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* VERSION: 02.33.01
* PATH: /src/Field/SnapshotTablesField.php
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
*/
namespace Moko\Plugin\System\MokoWaaS\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField;
/**
* Renders a multi-select list box of all Joomla database tables, with
* content-related tables pre-selected by default.
*
* @since 02.26.00
*/
class SnapshotTablesField extends FormField
{
protected $type = 'SnapshotTables';
/**
* Tables selected by default when no value is stored yet.
*
* @var array
* @since 02.25.00
*/
private const DEFAULT_TABLES = [
'#__content',
'#__categories',
'#__fields',
'#__fields_values',
'#__fields_groups',
'#__menu',
'#__menu_types',
'#__modules',
'#__modules_menu',
'#__users',
'#__user_usergroup_map',
'#__user_profiles',
'#__tags',
'#__contentitem_tag_map',
'#__assets',
];
/**
* Table suffixes grouped by category.
*
* @var array
* @since 02.25.00
*/
private const TABLE_GROUPS = [
'Content' => ['content', 'categories', 'fields', 'fields_values', 'fields_groups', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
'Users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
'Menus' => ['menu', 'menu_types'],
'Modules' => ['modules', 'modules_menu'],
'Assets' => ['assets'],
];
protected function getInput()
{
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$tables = $db->getTableList();
// Resolve selected values
$selected = $this->value;
if ($selected === null || $selected === '')
{
$selected = self::DEFAULT_TABLES;
}
elseif (is_string($selected))
{
$selected = array_filter(array_map('trim', explode("\n", $selected)));
}
$selected = (array) $selected;
// Flatten nested arrays from broken save format [["#__content"],["#__categories"]]
$selected = array_map(function ($v) {
return is_array($v) ? reset($v) : $v;
}, $selected);
// Group tables
$grouped = [];
foreach ($tables as $table)
{
if (strpos($table, $prefix) !== 0)
{
continue;
}
$suffix = substr($table, strlen($prefix));
$logical = '#__' . $suffix;
$group = 'Other';
foreach (self::TABLE_GROUPS as $groupName => $patterns)
{
if (in_array($suffix, $patterns, true))
{
$group = $groupName;
break;
}
}
$grouped[$group][] = $logical;
}
// Build HTML select with optgroups
$size = (int) ($this->element['size'] ?? 15);
$html = '<select name="' . $this->name . '" id="' . $this->id . '"'
. ' multiple="multiple" size="' . $size . '"'
. ' class="form-select">';
$priority = ['Content', 'Users', 'Menus', 'Modules', 'Assets'];
foreach ($priority as $g)
{
if (!empty($grouped[$g]))
{
$html .= '<optgroup label="' . $g . '">';
foreach ($grouped[$g] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
unset($grouped[$g]);
}
}
if (!empty($grouped['Other']))
{
$html .= '<optgroup label="Other">';
foreach ($grouped['Other'] as $t)
{
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
}
$html .= '</optgroup>';
}
$html .= '</select>';
// "Reset to defaults" link
$defaultsJson = htmlspecialchars(json_encode(self::DEFAULT_TABLES), ENT_QUOTES, 'UTF-8');
$html .= '<div class="mt-1">'
. '<a href="#" class="small" onclick="'
. 'var sel=document.getElementById(\'' . $this->id . '\');'
. 'var defs=' . $defaultsJson . ';'
. 'Array.from(sel.options).forEach(function(o){o.selected=defs.indexOf(o.value)!==-1;});'
. 'return false;'
. '"><span class="icon-refresh" aria-hidden="true"></span> Reset to defaults</a>'
. '</div>';
return $html;
}
}
@@ -1,819 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
* VERSION: 02.33.01
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Content Sync Receiver — applies incoming sync payload to the local site.
*
* Processes categories, articles, menu types, menu items, and modules
* from a JSON payload sent by a source MokoWaaS site. Content is matched
* by alias (upsert pattern): existing content is updated, new content
* is inserted.
*
* @since 02.21.00
*/
class ContentSyncReceiver
{
/**
* @var \Joomla\Database\DatabaseInterface
* @since 02.21.00
*/
private $db;
/**
* Warnings collected during sync.
*
* @var array
* @since 02.21.00
*/
private array $warnings = [];
/**
* Cache of resolved category paths → local IDs.
*
* @var array
* @since 02.21.00
*/
private array $catPathCache = [];
/**
* Constructor.
*
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
*
* @since 02.21.00
*/
public function __construct($db = null)
{
$this->db = $db ?: Factory::getDbo();
}
/**
* Process an incoming sync payload.
*
* @param array $payload Decoded JSON payload from the source site
*
* @return array Result summary with per-type counts and warnings
*
* @since 02.21.00
*/
public function receive(array $payload): array
{
if (empty($payload['mokowaas_sync']))
{
return ['status' => 'error', 'message' => 'Invalid payload — missing mokowaas_sync version'];
}
$this->warnings = [];
$results = [];
// Apply in dependency order
try
{
$results['categories'] = $this->applyCategories($payload['categories'] ?? []);
}
catch (\Throwable $e)
{
$results['categories'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Categories failed: ' . $e->getMessage();
}
try
{
$results['articles'] = $this->applyArticles($payload['articles'] ?? []);
}
catch (\Throwable $e)
{
$results['articles'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Articles failed: ' . $e->getMessage();
}
try
{
$results['menu_types'] = $this->applyMenuTypes($payload['menu_types'] ?? []);
}
catch (\Throwable $e)
{
$results['menu_types'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Menu types failed: ' . $e->getMessage();
}
try
{
$results['menu_items'] = $this->applyMenuItems($payload['menu_items'] ?? []);
}
catch (\Throwable $e)
{
$results['menu_items'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Menu items failed: ' . $e->getMessage();
}
try
{
$results['modules'] = $this->applyModules($payload['modules'] ?? []);
}
catch (\Throwable $e)
{
$results['modules'] = ['error' => $e->getMessage()];
$this->warnings[] = 'Modules failed: ' . $e->getMessage();
}
Log::add(
sprintf('Content sync received from %s', $payload['source_site'] ?? 'unknown'),
Log::INFO,
'mokowaas'
);
return [
'status' => 'ok',
'message' => 'Sync applied',
'source_site' => $payload['source_site'] ?? '',
'results' => $results,
'warnings' => $this->warnings,
];
}
/**
* Apply categories — sorted by path depth (shallow first).
*
* @param array $categories Category data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyCategories(array $categories): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
// Sort by path depth — parents before children
usort($categories, function ($a, $b) {
return substr_count($a['path'], '/') - substr_count($b['path'], '/');
});
foreach ($categories as $cat)
{
$alias = $cat['alias'] ?? '';
$path = $cat['path'] ?? $alias;
if (empty($alias) || !preg_match('/^[a-z0-9\-\/]+$/i', $path))
{
$this->warnings[] = 'Skipped category with invalid alias/path: ' . $alias;
continue;
}
// Resolve parent ID from path
$parentId = 1; // Root
$pathParts = explode('/', $path);
if (count($pathParts) > 1)
{
$parentPath = implode('/', array_slice($pathParts, 0, -1));
$parentId = $this->resolveCategoryPath($parentPath);
if ($parentId === 0)
{
$this->warnings[] = 'Parent category not found for: ' . $path;
$parentId = 1;
}
}
// Check if category exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('parent_id') . ' = ' . (int) $parentId);
$db->setQuery($query);
$existingId = (int) $db->loadResult();
$now = Factory::getDate()->toSql();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__categories'))
->set($db->quoteName('title') . ' = ' . $db->quote($cat['title']))
->set($db->quoteName('description') . ' = ' . $db->quote($cat['description'] ?? ''))
->set($db->quoteName('published') . ' = ' . (int) ($cat['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($cat['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($cat['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($cat['params'] ?? new \stdClass)))
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($cat['metadata'] ?? new \stdClass)))
->set($db->quoteName('modified_time') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
$this->catPathCache[$path] = $existingId;
}
else
{
$obj = (object) [
'title' => $cat['title'],
'alias' => $alias,
'path' => $path,
'extension' => 'com_content',
'description' => $cat['description'] ?? '',
'published' => (int) ($cat['published'] ?? 1),
'access' => (int) ($cat['access'] ?? 1),
'language' => $cat['language'] ?? '*',
'params' => json_encode($cat['params'] ?? new \stdClass),
'metadata' => json_encode($cat['metadata'] ?? new \stdClass),
'parent_id' => $parentId,
'level' => count($pathParts),
'lft' => 0,
'rgt' => 0,
'created_time' => $now,
'modified_time' => $now,
];
$db->insertObject('#__categories', $obj, 'id');
$inserted++;
$this->catPathCache[$path] = (int) $obj->id;
// Rebuild category tree for this extension
$this->rebuildCategoryTree();
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply articles — resolve category by alias path, upsert by alias.
*
* @param array $articles Article data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyArticles(array $articles): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($articles as $article)
{
$alias = $article['alias'] ?? '';
if (empty($alias))
{
continue;
}
// Resolve category
$catPath = $article['catid_alias_path'] ?? 'uncategorised';
$catId = $this->resolveCategoryPath($catPath);
if ($catId === 0)
{
$catId = 2; // Joomla's built-in Uncategorised
$this->warnings[] = 'Category "' . $catPath . '" not found for article "' . $alias . '" — assigned to Uncategorised';
}
// Check if article exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias));
$db->setQuery($query);
$existingId = (int) $db->loadResult();
$now = Factory::getDate()->toSql();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('title') . ' = ' . $db->quote($article['title']))
->set($db->quoteName('introtext') . ' = ' . $db->quote($article['introtext'] ?? ''))
->set($db->quoteName('fulltext') . ' = ' . $db->quote($article['fulltext'] ?? ''))
->set($db->quoteName('state') . ' = ' . (int) ($article['state'] ?? 1))
->set($db->quoteName('catid') . ' = ' . $catId)
->set($db->quoteName('access') . ' = ' . (int) ($article['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($article['language'] ?? '*'))
->set($db->quoteName('featured') . ' = ' . (int) ($article['featured'] ?? 0))
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($article['metadata'] ?? new \stdClass)))
->set($db->quoteName('attribs') . ' = ' . $db->quote(json_encode($article['attribs'] ?? new \stdClass)))
->set($db->quoteName('images') . ' = ' . $db->quote(json_encode($article['images'] ?? new \stdClass)))
->set($db->quoteName('urls') . ' = ' . $db->quote(json_encode($article['urls'] ?? new \stdClass)))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'title' => $article['title'],
'alias' => $alias,
'introtext' => $article['introtext'] ?? '',
'fulltext' => $article['fulltext'] ?? '',
'state' => (int) ($article['state'] ?? 1),
'catid' => $catId,
'access' => (int) ($article['access'] ?? 1),
'language' => $article['language'] ?? '*',
'featured' => (int) ($article['featured'] ?? 0),
'publish_up' => $article['publish_up'] ?? $now,
'publish_down' => $article['publish_down'],
'metadata' => json_encode($article['metadata'] ?? new \stdClass),
'attribs' => json_encode($article['attribs'] ?? new \stdClass),
'images' => json_encode($article['images'] ?? new \stdClass),
'urls' => json_encode($article['urls'] ?? new \stdClass),
'created' => $now,
'modified' => $now,
];
$db->insertObject('#__content', $obj, 'id');
$inserted++;
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply menu types — insert if not exists.
*
* @param array $menuTypes Menu type data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyMenuTypes(array $menuTypes): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($menuTypes as $mt)
{
$menutype = $mt['menutype'] ?? '';
if (empty($menutype))
{
continue;
}
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
$db->setQuery($query);
if ($db->loadResult())
{
$query = $db->getQuery(true)
->update($db->quoteName('#__menu_types'))
->set($db->quoteName('title') . ' = ' . $db->quote($mt['title'] ?? $menutype))
->set($db->quoteName('description') . ' = ' . $db->quote($mt['description'] ?? ''))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'title' => $mt['title'] ?? $menutype,
'menutype' => $menutype,
'description' => $mt['description'] ?? '',
];
$db->insertObject('#__menu_types', $obj);
$inserted++;
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply menu items — resolve parent aliases and {catid:path} tokens.
*
* @param array $items Menu item data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyMenuItems(array $items): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
// Sort: root items first, then children
usort($items, function ($a, $b) {
$aIsRoot = empty($a['parent_alias']);
$bIsRoot = empty($b['parent_alias']);
if ($aIsRoot && !$bIsRoot) return -1;
if (!$aIsRoot && $bIsRoot) return 1;
return 0;
});
// Resolve component IDs
$compQuery = $db->getQuery(true)
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($compQuery);
$compMap = [];
foreach ($db->loadAssocList() ?: [] as $c)
{
$compMap[$c['element']] = (int) $c['extension_id'];
}
foreach ($items as $item)
{
$alias = $item['alias'] ?? '';
$menutype = $item['menutype'] ?? '';
if (empty($alias) || empty($menutype))
{
continue;
}
// Resolve parent
$parentId = 1; // Root menu item
if (!empty($item['parent_alias']))
{
$parentId = $this->resolveMenuAlias($menutype, $item['parent_alias']);
if ($parentId === 0)
{
$this->warnings[] = 'Parent menu item "' . $item['parent_alias'] . '" not found for "' . $alias . '"';
$parentId = 1;
}
}
// Resolve {catid:path} tokens in link
$link = $item['link'] ?? '';
if (preg_match_all('/\{catid:([^}]+)\}/', $link, $matches))
{
foreach ($matches[1] as $i => $catPath)
{
$localCatId = $this->resolveCategoryPath($catPath);
if ($localCatId > 0)
{
$link = str_replace($matches[0][$i], (string) $localCatId, $link);
}
else
{
$this->warnings[] = 'Could not resolve {catid:' . $catPath . '} in menu item "' . $alias . '"';
$link = str_replace($matches[0][$i], '0', $link);
}
}
}
$componentId = $compMap[$item['component_name'] ?? ''] ?? 0;
// Check if menu item exists
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__menu'))
->set($db->quoteName('title') . ' = ' . $db->quote($item['title']))
->set($db->quoteName('link') . ' = ' . $db->quote($link))
->set($db->quoteName('type') . ' = ' . $db->quote($item['type'] ?? 'component'))
->set($db->quoteName('published') . ' = ' . (int) ($item['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($item['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($item['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($item['params'] ?? new \stdClass)))
->set($db->quoteName('parent_id') . ' = ' . $parentId)
->set($db->quoteName('component_id') . ' = ' . $componentId)
->set($db->quoteName('home') . ' = ' . (int) ($item['home'] ?? 0))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
}
else
{
$obj = (object) [
'menutype' => $menutype,
'title' => $item['title'],
'alias' => $alias,
'path' => $alias,
'link' => $link,
'type' => $item['type'] ?? 'component',
'published' => (int) ($item['published'] ?? 1),
'parent_id' => $parentId,
'level' => $parentId <= 1 ? 1 : 2,
'component_id' => $componentId,
'access' => (int) ($item['access'] ?? 1),
'language' => $item['language'] ?? '*',
'params' => json_encode($item['params'] ?? new \stdClass),
'home' => (int) ($item['home'] ?? 0),
'client_id' => 0,
'lft' => 0,
'rgt' => 0,
];
$db->insertObject('#__menu', $obj, 'id');
$inserted++;
}
}
// Rebuild menu tree for each affected menutype
$affectedMenuTypes = array_unique(array_column($items, 'menutype'));
foreach ($affectedMenuTypes as $mt)
{
$this->rebuildMenuTree($mt);
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Apply modules — upsert by title+position+client_id, rebuild menu assignments.
*
* @param array $modules Module data from payload
*
* @return array ['inserted' => N, 'updated' => N]
*
* @since 02.21.00
*/
private function applyModules(array $modules): array
{
$db = $this->db;
$inserted = 0;
$updated = 0;
foreach ($modules as $mod)
{
$title = $mod['title'] ?? '';
$position = $mod['position'] ?? '';
$clientId = (int) ($mod['client_id'] ?? 0);
if (empty($title))
{
continue;
}
// Check existence by title + position + client_id
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__modules'))
->where($db->quoteName('title') . ' = ' . $db->quote($title))
->where($db->quoteName('position') . ' = ' . $db->quote($position))
->where($db->quoteName('client_id') . ' = ' . $clientId);
$db->setQuery($query);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$query = $db->getQuery(true)
->update($db->quoteName('#__modules'))
->set($db->quoteName('module') . ' = ' . $db->quote($mod['module'] ?? ''))
->set($db->quoteName('content') . ' = ' . $db->quote($mod['content'] ?? ''))
->set($db->quoteName('published') . ' = ' . (int) ($mod['published'] ?? 1))
->set($db->quoteName('access') . ' = ' . (int) ($mod['access'] ?? 1))
->set($db->quoteName('language') . ' = ' . $db->quote($mod['language'] ?? '*'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($mod['params'] ?? new \stdClass)))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($query);
$db->execute();
$updated++;
$moduleId = $existingId;
}
else
{
$obj = (object) [
'title' => $title,
'module' => $mod['module'] ?? '',
'position' => $position,
'content' => $mod['content'] ?? '',
'published' => (int) ($mod['published'] ?? 1),
'access' => (int) ($mod['access'] ?? 1),
'language' => $mod['language'] ?? '*',
'params' => json_encode($mod['params'] ?? new \stdClass),
'client_id' => $clientId,
'ordering' => 0,
];
$db->insertObject('#__modules', $obj, 'id');
$inserted++;
$moduleId = (int) $obj->id;
}
// Rebuild menu assignments
$query = $db->getQuery(true)
->delete($db->quoteName('#__modules_menu'))
->where($db->quoteName('moduleid') . ' = ' . $moduleId);
$db->setQuery($query);
$db->execute();
$assignment = $mod['menu_assignment'] ?? [];
$assignType = (int) ($assignment['assignment'] ?? 0);
$aliases = $assignment['menu_item_aliases'] ?? [];
if ($assignType === 0 || empty($aliases))
{
// All pages
$obj = (object) ['moduleid' => $moduleId, 'menuid' => 0];
$db->insertObject('#__modules_menu', $obj);
}
else
{
foreach ($aliases as $aliasRef)
{
// Format: "menutype:alias"
$parts = explode(':', $aliasRef, 2);
if (count($parts) !== 2)
{
continue;
}
$menuId = $this->resolveMenuAlias($parts[0], $parts[1]);
if ($menuId > 0)
{
$menuidValue = $assignType === -1 ? -$menuId : $menuId;
$obj = (object) ['moduleid' => $moduleId, 'menuid' => $menuidValue];
$db->insertObject('#__modules_menu', $obj);
}
else
{
$this->warnings[] = 'Module "' . $title . '": menu item "' . $aliasRef . '" not found';
}
}
}
}
return ['inserted' => $inserted, 'updated' => $updated];
}
/**
* Resolve a category alias path to a local category ID.
*
* @param string $path Slash-delimited alias path (e.g. "blog/news")
*
* @return int Category ID, or 0 if not found
*
* @since 02.21.00
*/
private function resolveCategoryPath(string $path): int
{
if (isset($this->catPathCache[$path]))
{
return $this->catPathCache[$path];
}
$db = $this->db;
$segments = explode('/', $path);
$parentId = 1;
foreach ($segments as $segment)
{
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__categories'))
->where($db->quoteName('alias') . ' = ' . $db->quote($segment))
->where($db->quoteName('parent_id') . ' = ' . $parentId)
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$id = (int) $db->loadResult();
if ($id === 0)
{
$this->catPathCache[$path] = 0;
return 0;
}
$parentId = $id;
}
$this->catPathCache[$path] = $parentId;
return $parentId;
}
/**
* Resolve a menu item alias to a local menu ID.
*
* @param string $menutype Menu type key
* @param string $alias Menu item alias
*
* @return int Menu item ID, or 0 if not found
*
* @since 02.21.00
*/
private function resolveMenuAlias(string $menutype, string $alias): int
{
$db = $this->db;
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
return (int) $db->loadResult();
}
/**
* Rebuild the nested set (lft/rgt) for the category tree.
*
* Uses Joomla's built-in Table rebuild method.
*
* @return void
*
* @since 02.21.00
*/
private function rebuildCategoryTree(): void
{
try
{
$table = \Joomla\CMS\Table\Table::getInstance('Category');
$table->rebuild();
}
catch (\Throwable $e)
{
$this->warnings[] = 'Category tree rebuild failed: ' . $e->getMessage();
}
}
/**
* Rebuild the nested set (lft/rgt) for a menu type.
*
* @param string $menutype Menu type to rebuild
*
* @return void
*
* @since 02.21.00
*/
private function rebuildMenuTree(string $menutype): void
{
try
{
$table = \Joomla\CMS\Table\Table::getInstance('Menu');
$table->rebuild();
}
catch (\Throwable $e)
{
$this->warnings[] = 'Menu tree rebuild failed for "' . $menutype . '": ' . $e->getMessage();
}
}
}
@@ -1,634 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
* VERSION: 02.33.01
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
/**
* Content Sync Service — builds a JSON payload of site content and pushes
* it to one or more remote MokoWaaS sites.
*
* Content is matched by alias on the receiving end (upsert-by-alias).
* Category IDs in menu item links are encoded as {catid:alias/path} tokens
* so the receiver can resolve them to local IDs.
*
* @since 02.21.00
*/
class ContentSyncService
{
/**
* Maximum items per content type to prevent unbounded memory.
*
* @var int
* @since 02.21.00
*/
private const MAX_ITEMS = 2000;
/**
* HTTP timeout for push requests in seconds.
*
* @var int
* @since 02.21.00
*/
private const HTTP_TIMEOUT = 60;
/**
* @var \Joomla\Database\DatabaseInterface
* @since 02.21.00
*/
private $db;
/**
* Category ID → alias path map cache.
*
* @var array
* @since 02.21.00
*/
private array $catPathMap = [];
/**
* Constructor.
*
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
*
* @since 02.21.00
*/
public function __construct($db = null)
{
$this->db = $db ?: Factory::getDbo();
}
/**
* Build the full sync payload from local content.
*
* @return array Structured payload ready for JSON encoding
*
* @since 02.21.00
*/
public function buildPayload(): array
{
$this->catPathMap = $this->buildCategoryPathMap();
return [
'mokowaas_sync' => '1.0',
'source_site' => rtrim(Uri::root(), '/'),
'generated_at' => gmdate('Y-m-d\TH:i:s\Z'),
'categories' => $this->buildCategoryPayload(),
'articles' => $this->buildArticlePayload(),
'menu_types' => $this->buildMenuTypePayload(),
'menu_items' => $this->buildMenuItemPayload(),
'modules' => $this->buildModulePayload(),
];
}
/**
* Push the sync payload to a single target site.
*
* @param string $targetUrl Base URL of the target site
* @param string $token health_api_token for the target
*
* @return array Result with status, message, and per-type counts
*
* @since 02.21.00
*/
public function pushToTarget(string $targetUrl, string $token): array
{
$payload = $this->buildPayload();
$jsonBody = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$endpoint = rtrim($targetUrl, '/') . '/?mokowaas=sync-receive';
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n",
'content' => $jsonBody,
'timeout' => self::HTTP_TIMEOUT,
'ignore_errors' => true,
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$response = @file_get_contents($endpoint, false, $context);
if ($response === false)
{
return [
'status' => 'error',
'target' => $targetUrl,
'message' => 'Connection failed — target unreachable',
];
}
// Parse HTTP status from response headers
$httpCode = 0;
if (isset($http_response_header[0]))
{
preg_match('/\d{3}/', $http_response_header[0], $matches);
$httpCode = (int) ($matches[0] ?? 0);
}
$result = json_decode($response, true);
if ($httpCode >= 400 || !$result)
{
return [
'status' => 'error',
'target' => $targetUrl,
'http_code' => $httpCode,
'message' => $result['error'] ?? $result['message'] ?? 'Unknown error from target',
];
}
$result['target'] = $targetUrl;
return $result;
}
/**
* Push content to all configured sync targets.
*
* @param array $targets Array of ['url' => ..., 'token' => ..., 'label' => ...]
*
* @return array Per-target results
*
* @since 02.21.00
*/
public function syncAllTargets(array $targets): array
{
$results = [];
foreach ($targets as $target)
{
$url = $target['url'] ?? '';
$token = $target['token'] ?? '';
$label = $target['label'] ?? $url;
if (empty($url) || empty($token))
{
$results[] = [
'status' => 'skipped',
'target' => $label,
'message' => 'Missing URL or token',
];
continue;
}
try
{
$result = $this->pushToTarget($url, $token);
$result['label'] = $label;
$results[] = $result;
}
catch (\Throwable $e)
{
$results[] = [
'status' => 'error',
'target' => $label,
'message' => $e->getMessage(),
];
}
}
Log::add(
sprintf('Content sync pushed to %d target(s)', count($targets)),
Log::INFO,
'mokowaas'
);
return [
'status' => 'ok',
'message' => sprintf('Sync completed for %d target(s)', count($results)),
'targets' => $results,
];
}
/**
* Build category ID → alias path map.
*
* @return array [id => 'parent-alias/child-alias']
*
* @since 02.21.00
*/
private function buildCategoryPathMap(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('alias'), $db->quoteName('parent_id')])
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('published') . ' != -2')
->where($db->quoteName('id') . ' > 1');
$db->setQuery($query);
$rows = $db->loadAssocList('id');
$map = [];
foreach ($rows as $id => $row)
{
$map[$id] = $this->resolvePathFromRows($id, $rows);
}
return $map;
}
/**
* Recursively build alias path for a category ID.
*
* @param int $id Category ID
* @param array $rows All category rows keyed by ID
*
* @return string Slash-delimited alias path
*
* @since 02.21.00
*/
private function resolvePathFromRows(int $id, array $rows): string
{
if (!isset($rows[$id]))
{
return '';
}
$row = $rows[$id];
$parentId = (int) $row['parent_id'];
if ($parentId <= 1 || !isset($rows[$parentId]))
{
return $row['alias'];
}
return $this->resolvePathFromRows($parentId, $rows) . '/' . $row['alias'];
}
/**
* Build category payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildCategoryPayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('title'),
$db->quoteName('alias'),
$db->quoteName('description'),
$db->quoteName('published'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('params'),
$db->quoteName('metadata'),
])
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('published') . ' != -2')
->where($db->quoteName('id') . ' > 1')
->order($db->quoteName('lft') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
$categories = [];
foreach ($rows as $row)
{
$categories[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'path' => $this->catPathMap[(int) $row['id']] ?? $row['alias'],
'description' => $row['description'] ?? '',
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'metadata' => json_decode($row['metadata'] ?: '{}', true),
];
}
return $categories;
}
/**
* Build article payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildArticlePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('alias'),
$db->quoteName('introtext'),
$db->quoteName('fulltext'),
$db->quoteName('state'),
$db->quoteName('catid'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('featured'),
$db->quoteName('publish_up'),
$db->quoteName('publish_down'),
$db->quoteName('metadata'),
$db->quoteName('attribs'),
$db->quoteName('images'),
$db->quoteName('urls'),
])
->from($db->quoteName('#__content'))
->where($db->quoteName('state') . ' != -2')
->order($db->quoteName('id') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
$articles = [];
foreach ($rows as $row)
{
$articles[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'introtext' => $row['introtext'],
'fulltext' => $row['fulltext'],
'state' => (int) $row['state'],
'catid_alias_path' => $this->catPathMap[(int) $row['catid']] ?? 'uncategorised',
'access' => (int) $row['access'],
'language' => $row['language'],
'featured' => (int) $row['featured'],
'publish_up' => $row['publish_up'],
'publish_down' => $row['publish_down'],
'metadata' => json_decode($row['metadata'] ?: '{}', true),
'attribs' => json_decode($row['attribs'] ?: '{}', true),
'images' => json_decode($row['images'] ?: '{}', true),
'urls' => json_decode($row['urls'] ?: '{}', true),
];
}
return $articles;
}
/**
* Build menu type payload.
*
* @return array
*
* @since 02.21.00
*/
private function buildMenuTypePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('menutype'),
$db->quoteName('description'),
])
->from($db->quoteName('#__menu_types'));
$db->setQuery($query);
return $db->loadAssocList() ?: [];
}
/**
* Build menu item payload with {catid:path} tokens in links.
*
* @return array
*
* @since 02.21.00
*/
private function buildMenuItemPayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('a.title'),
$db->quoteName('a.alias'),
$db->quoteName('a.menutype'),
$db->quoteName('a.parent_id'),
$db->quoteName('a.link'),
$db->quoteName('a.type'),
$db->quoteName('a.published'),
$db->quoteName('a.access'),
$db->quoteName('a.language'),
$db->quoteName('a.params'),
$db->quoteName('a.home'),
$db->quoteName('a.component_id'),
$db->quoteName('b.alias', 'parent_alias'),
])
->from($db->quoteName('#__menu', 'a'))
->leftJoin(
$db->quoteName('#__menu', 'b') . ' ON '
. $db->quoteName('a.parent_id') . ' = ' . $db->quoteName('b.id')
)
->where($db->quoteName('a.published') . ' != -2')
->where($db->quoteName('a.client_id') . ' = 0')
->where($db->quoteName('a.level') . ' >= 1')
->order($db->quoteName('a.lft') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
// Get component name map
$compQuery = $db->getQuery(true)
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($compQuery);
$components = $db->loadAssocList('extension_id') ?: [];
$items = [];
foreach ($rows as $row)
{
$link = $row['link'];
// Encode category IDs in com_content links as {catid:path} tokens
if (preg_match('/option=com_content/', $link) && preg_match('/&id=(\d+)/', $link, $m))
{
$catId = (int) $m[1];
if (isset($this->catPathMap[$catId]))
{
$link = preg_replace('/&id=\d+/', '&id={catid:' . $this->catPathMap[$catId] . '}', $link);
}
}
$compName = '';
if (!empty($row['component_id']) && isset($components[$row['component_id']]))
{
$compName = $components[$row['component_id']]['element'];
}
// Root-level items have parent_id=1 (Joomla's root menu item)
$parentAlias = ((int) $row['parent_id'] <= 1) ? '' : ($row['parent_alias'] ?? '');
$items[] = [
'title' => $row['title'],
'alias' => $row['alias'],
'menutype' => $row['menutype'],
'parent_alias' => $parentAlias,
'link' => $link,
'type' => $row['type'],
'component_name' => $compName,
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'home' => (int) $row['home'],
];
}
return $items;
}
/**
* Build module payload with menu assignments.
*
* @return array
*
* @since 02.21.00
*/
private function buildModulePayload(): array
{
$db = $this->db;
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('title'),
$db->quoteName('module'),
$db->quoteName('position'),
$db->quoteName('content'),
$db->quoteName('published'),
$db->quoteName('access'),
$db->quoteName('language'),
$db->quoteName('params'),
$db->quoteName('client_id'),
])
->from($db->quoteName('#__modules'))
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('published') . ' != -2')
->order($db->quoteName('ordering') . ' ASC')
->setLimit(self::MAX_ITEMS);
$db->setQuery($query);
$rows = $db->loadAssocList();
// Get all module-menu assignments
$mmQuery = $db->getQuery(true)
->select([
$db->quoteName('mm.moduleid'),
$db->quoteName('mm.menuid'),
$db->quoteName('m.alias', 'menu_alias'),
$db->quoteName('m.menutype'),
])
->from($db->quoteName('#__modules_menu', 'mm'))
->leftJoin(
$db->quoteName('#__menu', 'm') . ' ON '
. $db->quoteName('mm.menuid') . ' = ' . $db->quoteName('m.id')
);
$db->setQuery($mmQuery);
$allAssignments = $db->loadAssocList();
// Group assignments by module ID
$assignmentsByModule = [];
foreach ($allAssignments as $a)
{
$assignmentsByModule[(int) $a['moduleid']][] = $a;
}
$modules = [];
foreach ($rows as $row)
{
$moduleId = (int) $row['id'];
$assignments = $assignmentsByModule[$moduleId] ?? [];
// Determine assignment type: 0 = all pages, positive = selected, negative = excluded
$menuAliases = [];
$assignType = 0;
if (!empty($assignments))
{
$firstMenuId = (int) $assignments[0]['menuid'];
if ($firstMenuId === 0)
{
$assignType = 0; // All pages
}
elseif ($firstMenuId < 0)
{
$assignType = -1; // All except selected
foreach ($assignments as $a)
{
if (!empty($a['menu_alias']))
{
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
}
}
}
else
{
$assignType = 1; // Selected only
foreach ($assignments as $a)
{
if (!empty($a['menu_alias']))
{
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
}
}
}
}
$modules[] = [
'title' => $row['title'],
'module' => $row['module'],
'position' => $row['position'],
'content' => $row['content'],
'published' => (int) $row['published'],
'access' => (int) $row['access'],
'language' => $row['language'],
'params' => json_decode($row['params'] ?: '{}', true),
'client_id' => (int) $row['client_id'],
'menu_assignment' => [
'assignment' => $assignType,
'menu_item_aliases' => $menuAliases,
],
];
}
return $modules;
}
}
@@ -1,606 +0,0 @@
<?php
/**
* @package MokoWaaS
* @subpackage plg_system_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
* VERSION: 02.33.01
* BRIEF: Content-only snapshot/restore for demo site reset
*/
namespace Moko\Plugin\System\MokoWaaS\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Demo Reset Service — content-only snapshot and restore.
*
* Only touches safe content tables (articles, categories, menus, modules,
* users, tags, fields). Never touches extensions, assets, sessions,
* schemas, update sites, or any system tables.
*
* @since 02.31.00
*/
class DemoResetService
{
private const MAX_NAME_LENGTH = 64;
private const BATCH_SIZE = 500;
/**
* Safe content tables to snapshot/restore.
* These can be wiped and restored without breaking the Joomla installation.
*/
private const SAFE_TABLES = [
// Content
'#__content',
'#__content_frontpage',
'#__categories',
'#__fields',
'#__fields_values',
'#__fields_groups',
'#__tags',
'#__contentitem_tag_map',
'#__ucm_content',
// Menus
'#__menu',
'#__menu_types',
// Modules
'#__modules',
'#__modules_menu',
// Users
'#__users',
'#__user_usergroup_map',
'#__user_profiles',
// Contact
'#__contact_details',
// Banners
'#__banners',
'#__banner_clients',
'#__banner_tracks',
// Community Builder
'#__comprofiler',
'#__comprofiler_fields',
'#__comprofiler_field_values',
'#__comprofiler_tabs',
'#__comprofiler_members',
'#__comprofiler_lists',
'#__comprofiler_plugin',
'#__comprofiler_userreports',
];
/**
* @var string
*/
private string $snapshotDir;
/**
* @var bool
*/
private bool $includeMedia;
/**
* @param bool $includeMedia Include /images/ directory
* @param string $baseDir Override snapshot root
*/
public function __construct(bool $includeMedia = true, string $baseDir = '')
{
$this->includeMedia = $includeMedia;
$this->snapshotDir = $baseDir ?: JPATH_ROOT . '/mokowaas-snapshots';
}
/**
* List all available snapshots.
*
* @return array
*/
public function listSnapshots(): array
{
$snapshots = [];
if (!is_dir($this->snapshotDir))
{
return $snapshots;
}
foreach (glob($this->snapshotDir . '/*/manifest.json') as $path)
{
$data = json_decode(file_get_contents($path), true);
if ($data && isset($data['name']))
{
$snapshots[$data['name']] = $data;
}
}
return $snapshots;
}
/**
* Create a content snapshot.
*
* @param string $name Snapshot name
*
* @return array Result
*/
public function createSnapshot(string $name): array
{
$this->validateSnapshotName($name);
$this->ensureSnapshotDir();
$path = $this->getSnapshotPath($name);
if (is_dir($path))
{
$this->removeDirectory($path);
}
mkdir($path, 0755, true);
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$allTables = $db->getTableList();
$dumped = 0;
foreach (self::SAFE_TABLES as $logicalName)
{
$realName = str_replace('#__', $prefix, $logicalName);
if (!in_array($realName, $allTables))
{
continue;
}
$this->dumpTable($logicalName, $realName, $path, $db);
$dumped++;
}
// Media
$hasMedia = false;
if ($this->includeMedia && is_dir(JPATH_ROOT . '/images'))
{
$hasMedia = $this->zipDirectory(JPATH_ROOT . '/images', $path . '/media.zip');
}
$manifest = [
'name' => $name,
'created_at' => gmdate('Y-m-d\TH:i:s\Z'),
'type' => 'content-only',
'tables' => $dumped,
'has_media' => $hasMedia,
'joomla_version' => JVERSION,
];
file_put_contents($path . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
Log::add(sprintf('Demo snapshot "%s" created (%d tables, media=%s)', $name, $dumped, $hasMedia ? 'yes' : 'no'), Log::INFO, 'mokowaas');
return [
'status' => 'ok',
'message' => 'Snapshot created',
'name' => $name,
'tables' => $dumped,
'has_media' => $hasMedia,
];
}
/**
* Restore from a snapshot.
*
* @param string $name Snapshot name
*
* @return array Result
*/
public function restoreSnapshot(string $name): array
{
$this->validateSnapshotName($name);
$path = $this->getSnapshotPath($name);
$manifest = $path . '/manifest.json';
if (!file_exists($manifest))
{
throw new \RuntimeException('Snapshot not found: ' . $name);
}
$manifestData = json_decode(file_get_contents($manifest), true);
// Clear cache
try { Factory::getCache('')->clean(''); } catch (\Throwable $e) {}
$db = Factory::getDbo();
$prefix = $db->getPrefix();
$restored = 0;
$sqlFiles = glob($path . '/*.sql');
foreach ($sqlFiles as $sqlFile)
{
try
{
$this->restoreTable($sqlFile, $db, $prefix);
$restored++;
}
catch (\Throwable $e)
{
Log::add('Demo reset: failed to restore ' . basename($sqlFile) . ': ' . $e->getMessage(), Log::ERROR, 'mokowaas');
}
}
// Restore /images/
$mediaRestored = false;
if (($manifestData['has_media'] ?? false) && file_exists($path . '/media.zip'))
{
$this->clearDirectory(JPATH_ROOT . '/images');
$zip = new \ZipArchive();
if ($zip->open($path . '/media.zip') === true)
{
$zip->extractTo(JPATH_ROOT . '/images');
$zip->close();
$mediaRestored = true;
}
}
// Rebuild assets table to fix ACL after content restore
$this->rebuildAssets();
Log::add(sprintf('Demo site reset (%d tables, media=%s)', $restored, $mediaRestored ? 'yes' : 'no'), Log::WARNING, 'mokowaas');
return [
'status' => 'ok',
'message' => 'Site content restored',
'baseline' => $name,
'restored_tables' => $restored,
'media_restored' => $mediaRestored,
];
}
/**
* Delete a snapshot.
*/
public function deleteSnapshot(string $name): bool
{
$this->validateSnapshotName($name);
$path = $this->getSnapshotPath($name);
if (!is_dir($path))
{
return false;
}
$this->removeDirectory($path);
return true;
}
/**
* Rebuild the assets table after content restore.
*
* Deletes content-related asset entries (which now have stale IDs)
* and rebuilds them using Joomla's Table classes. Extension and
* component-level assets are left untouched.
*
* @return void
*/
private function rebuildAssets(): void
{
try
{
$db = Factory::getDbo();
// Delete content-level assets (articles, categories, modules, etc.)
// Keep component-level and root assets intact
$contentAssetPrefixes = [
'com_content.article.%',
'com_content.category.%',
'com_contact.contact.%',
'com_banners.banner.%',
'com_banners.category.%',
'com_modules.module.%',
'com_menus.menu.%',
'com_users.user.%',
];
foreach ($contentAssetPrefixes as $prefix)
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__assets'))
->where($db->quoteName('name') . ' LIKE ' . $db->quote($prefix))
);
$db->execute();
}
// Rebuild category tree (also fixes category assets)
$catTable = \Joomla\CMS\Table\Table::getInstance('Category');
if ($catTable)
{
$catTable->rebuild();
}
// Rebuild menu tree
$menuTable = \Joomla\CMS\Table\Table::getInstance('Menu');
if ($menuTable)
{
$menuTable->rebuild();
}
// Rebuild asset tree
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
if ($assetTable)
{
$assetTable->rebuild();
}
// Re-create assets for content items that lost theirs
$this->fixContentAssets($db);
}
catch (\Throwable $e)
{
Log::add('Asset rebuild warning: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
/**
* Re-create missing asset entries for content items.
*
* After deleting stale assets and restoring content, some items
* may reference asset_id=0. This creates new asset rows for them.
*
* @param \Joomla\Database\DatabaseInterface $db
*
* @return void
*/
private function fixContentAssets($db): void
{
// Fix articles with missing assets
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('title'), $db->quoteName('alias')])
->from($db->quoteName('#__content'))
->where($db->quoteName('asset_id') . ' = 0');
$db->setQuery($query);
$articles = $db->loadAssocList() ?: [];
// Find the com_content component asset as parent
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__assets'))
->where($db->quoteName('name') . ' = ' . $db->quote('com_content'))
);
$contentAssetId = (int) $db->loadResult();
foreach ($articles as $article)
{
$assetName = 'com_content.article.' . (int) $article['id'];
$asset = (object) [
'parent_id' => $contentAssetId ?: 1,
'lft' => 0,
'rgt' => 0,
'level' => 0,
'name' => $assetName,
'title' => $article['title'],
'rules' => '{}',
];
$db->insertObject('#__assets', $asset, 'id');
// Update content row with new asset_id
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('asset_id') . ' = ' . (int) $asset->id)
->where($db->quoteName('id') . ' = ' . (int) $article['id'])
);
$db->execute();
}
// Rebuild asset tree again after inserts
try
{
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
if ($assetTable)
{
$assetTable->rebuild();
}
}
catch (\Throwable $e)
{
// Best effort
}
}
// ------------------------------------------------------------------
// Private helpers
// ------------------------------------------------------------------
private function dumpTable(string $logicalName, string $realName, string $dir, $db): void
{
$safeFileName = str_replace('#__', 'jml__', $logicalName);
$fp = fopen($dir . '/' . $safeFileName . '.sql', 'w');
$columns = $db->getTableColumns($realName, false);
$colNames = array_keys($columns);
$quotedCols = array_map([$db, 'quoteName'], $colNames);
$colList = implode(', ', $quotedCols);
$offset = 0;
while (true)
{
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName($realName))
->setLimit(self::BATCH_SIZE, $offset);
$db->setQuery($query);
$rows = $db->loadAssocList();
if (empty($rows))
{
break;
}
$values = [];
foreach ($rows as $row)
{
$vals = [];
foreach ($colNames as $col)
{
$vals[] = $row[$col] === null ? 'NULL' : $db->quote($row[$col]);
}
$values[] = '(' . implode(', ', $vals) . ')';
}
fwrite($fp, 'INSERT INTO ' . $db->quoteName($realName) . ' (' . $colList . ') VALUES ' . "\n" . implode(",\n", $values) . ";\n\n");
$offset += self::BATCH_SIZE;
if (count($rows) < self::BATCH_SIZE)
{
break;
}
}
fclose($fp);
}
private function restoreTable(string $sqlFile, $db, string $prefix): void
{
$baseName = basename($sqlFile, '.sql');
$realTable = str_replace('jml__', $prefix, $baseName);
$db->setQuery('TRUNCATE TABLE ' . $db->quoteName($realTable));
$db->execute();
$sql = file_get_contents($sqlFile);
if (empty(trim($sql)))
{
return;
}
$statements = array_filter(
array_map('trim', explode(";\n", $sql)),
function ($s) { return !empty($s) && $s !== ';'; }
);
foreach ($statements as $statement)
{
$statement = rtrim($statement, ';');
if (empty($statement))
{
continue;
}
$db->setQuery($statement);
$db->execute();
}
}
private function zipDirectory(string $sourceDir, string $zipPath): bool
{
$zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true)
{
return false;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item)
{
$rel = str_replace('\\', '/', substr($item->getPathname(), strlen($sourceDir) + 1));
$item->isDir() ? $zip->addEmptyDir($rel) : $zip->addFile($item->getPathname(), $rel);
}
$zip->close();
return true;
}
private function ensureSnapshotDir(): void
{
if (!is_dir($this->snapshotDir))
{
mkdir($this->snapshotDir, 0755, true);
}
if (!file_exists($this->snapshotDir . '/.htaccess'))
{
file_put_contents($this->snapshotDir . '/.htaccess', "Deny from all\n");
}
}
private function getSnapshotPath(string $name): string
{
return $this->snapshotDir . '/' . $name;
}
private function validateSnapshotName(string $name): void
{
if ($name === '' || strlen($name) > self::MAX_NAME_LENGTH || !preg_match('/^[a-zA-Z0-9_-]+$/', $name))
{
throw new \InvalidArgumentException('Invalid snapshot name');
}
}
private function removeDirectory(string $dir): void
{
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
@rmdir($dir);
}
private function clearDirectory(string $dir): void
{
if (!is_dir($dir)) return;
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
}
}
}
@@ -1 +0,0 @@
<!DOCTYPE html><title></title>
@@ -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>"
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field name="url" type="url"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC"
required="true" hint="https://client.example.com" />
<field name="token" type="text"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC"
required="true" hint="health_api_token from target site" />
<field name="label" type="text"
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC"
hint="e.g. Client A" />
</form>
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field
name="ip"
type="text"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC"
required="true"
hint="e.g. 192.168.1.100 or 10.0.0.0/24"
/>
<field
name="label"
type="text"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC"
hint="e.g. Office network"
/>
<field
name="enabled"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</form>
@@ -7,180 +7,20 @@
; FILE INFORMATION
; Defgroup: Joomla Language
; Ingroup: MokoWaaS
; Version: 02.01.08
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
; File: plg_system_mokowaas.ini
; Path: /src/language/en-GB/plg_system_mokowaas.ini
; Brief: English language strings for MokoWaaS system plugin
; Notes: Contains translatable strings for plugin functionality
; Variables: (none)
; Brief: English language strings for MokoWaaS core system plugin
; -----------------------------------------------------------------------------
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, heartbeat, health checks, 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."
; ===== Core fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration."
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_LABEL="Brand Name"
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_DESC="The brand name that replaces 'Joomla' throughout the interface. Used in all language overrides."
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
; ===== WaaS Access fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_SUCCESS="Emergency access LOGIN by {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_BLOCKED_IP="Emergency access BLOCKED (unauthorized IP) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_WRONG_PASSWORD="Emergency access FAILED (wrong password) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_VERIFY_FILE_CREATED="Emergency access verification file created — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_PENDING_FILE_DELETE="Emergency access pending file deletion — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access requires an IP whitelist. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Emergency access is BLOCKED if no IPs are configured."
; ===== Maintenance fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_LABEL="Maintenance"
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_DESC="One-time maintenance actions. Set to Yes and save to execute. Resets to No automatically after execution."
PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL="Development Mode"
PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC="Disables all Joomla caching at runtime. Useful during development and testing. Does not modify configuration.php."
PLG_SYSTEM_MOKOWAAS_RESET_HITS_LABEL="Reset All Hits"
PLG_SYSTEM_MOKOWAAS_RESET_HITS_DESC="Set all article hit counters to zero across the site. This action executes on save and resets to No."
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_DESC="Purge all content version history from the database. This action executes on save and resets to No."
; ===== Visual Branding fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_LABEL="Visual Branding"
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_DESC="Admin color scheme and CSS injection. Logos and favicon are shipped in the plugin media folder."
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_LABEL="Logos & Favicon"
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_DESC="Logos and favicon are automatically applied from the plugin media folder (<code>/media/plg_system_mokowaas/</code>). Replace <code>logo.png</code>, <code>favicon.ico</code>, and <code>favicon_256.png</code> to change them."
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_LABEL="Primary Color"
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_DESC="Main accent color used in the admin template header and buttons."
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_LABEL="Sidebar Color"
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_DESC="Background color for the admin sidebar navigation."
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_LABEL="Header Color"
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_DESC="Background color for the admin top header bar."
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_LABEL="Link Color"
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_DESC="Color for hyperlinks in the admin interface."
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_LABEL="Brand Icon (FontAwesome)"
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_DESC="FontAwesome unicode codepoint for the brand icon that replaces the Joomla logo icon. Enter the hex code only (e.g. f6d5 for fa-hat-cowboy). Find codes at fontawesome.com/icons."
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_LABEL="Custom CSS"
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_DESC="Additional CSS injected into admin pages. Use for fine-tuning visual presentation."
; ===== Tenant Restrictions fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL="Tenant Restrictions"
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master users. Master user always has full access."
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_DESC="Block non-master users from changing Global Configuration. Component config is still accessible."
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_LABEL="Restrict Template Code Editing"
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_DESC="Block non-master users from editing template source code. Template styles remain accessible."
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_LABEL="Disable Install from URL"
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from URL for ALL users (including master) as a safety measure."
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
; ===== Content Sync fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
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_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
; ===== Diagnostics fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
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"
; ===== Diagnostics =====
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."
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_LABEL="Grafana API Key"
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_DESC="Service account token or API key with Editor role in Grafana. Required for auto-provisioning the MokoWaaS datasource and dashboard."
; ===== Security fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL="Security Hardening"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC="HTTPS enforcement, session timeouts, password policy, and upload restrictions."
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
PLG_SYSTEM_MOKOWAAS_PASSWORD_NUMBER_LABEL="Require Number"
PLG_SYSTEM_MOKOWAAS_PASSWORD_SPECIAL_LABEL="Require Special Character"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_LABEL="Allowed Upload Types"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file extensions for media uploads."
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Demo Mode fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
@@ -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."
@@ -7,180 +7,20 @@
; FILE INFORMATION
; Defgroup: Joomla Language
; Ingroup: MokoWaaS
; Version: 02.01.08
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
; File: plg_system_mokowaas.ini
; Path: /src/language/en-GB/plg_system_mokowaas.ini
; Brief: English language strings for MokoWaaS system plugin
; Notes: Contains translatable strings for plugin functionality
; Variables: (none)
; Brief: English language strings for MokoWaaS core system plugin
; -----------------------------------------------------------------------------
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, heartbeat, health checks, 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."
; ===== Core fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration."
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_LABEL="Brand Name"
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_DESC="The brand name that replaces 'Joomla' throughout the interface. Used in all language overrides."
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
; ===== WaaS Access fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_SUCCESS="Emergency access LOGIN by {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_BLOCKED_IP="Emergency access BLOCKED (unauthorized IP) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_WRONG_PASSWORD="Emergency access FAILED (wrong password) — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_VERIFY_FILE_CREATED="Emergency access verification file created — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_PENDING_FILE_DELETE="Emergency access pending file deletion — {username} from {ip}"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access requires an IP whitelist. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Emergency access is BLOCKED if no IPs are configured."
; ===== Maintenance fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_LABEL="Maintenance"
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_DESC="One-time maintenance actions. Set to Yes and save to execute. Resets to No automatically after execution."
PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL="Development Mode"
PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC="Disables all Joomla caching at runtime. Useful during development and testing. Does not modify configuration.php."
PLG_SYSTEM_MOKOWAAS_RESET_HITS_LABEL="Reset All Hits"
PLG_SYSTEM_MOKOWAAS_RESET_HITS_DESC="Set all article hit counters to zero across the site. This action executes on save and resets to No."
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_DESC="Purge all content version history from the database. This action executes on save and resets to No."
; ===== Visual Branding fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_LABEL="Visual Branding"
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_DESC="Admin color scheme and CSS injection. Logos and favicon are shipped in the plugin media folder."
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_LABEL="Logos & Favicon"
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_DESC="Logos and favicon are automatically applied from the plugin media folder (<code>/media/plg_system_mokowaas/</code>). Replace <code>logo.png</code>, <code>favicon.ico</code>, and <code>favicon_256.png</code> to change them."
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_LABEL="Primary Color"
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_DESC="Main accent color used in the admin template header and buttons."
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_LABEL="Sidebar Color"
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_DESC="Background color for the admin sidebar navigation."
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_LABEL="Header Color"
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_DESC="Background color for the admin top header bar."
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_LABEL="Link Color"
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_DESC="Color for hyperlinks in the admin interface."
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_LABEL="Brand Icon (FontAwesome)"
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_DESC="FontAwesome unicode codepoint for the brand icon that replaces the Joomla logo icon. Enter the hex code only (e.g. f6d5 for fa-hat-cowboy). Find codes at fontawesome.com/icons."
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_LABEL="Custom CSS"
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_DESC="Additional CSS injected into admin pages. Use for fine-tuning visual presentation."
; ===== Tenant Restrictions fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL="Tenant Restrictions"
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master users. Master user always has full access."
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_DESC="Block non-master users from changing Global Configuration. Component config is still accessible."
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_LABEL="Restrict Template Code Editing"
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_DESC="Block non-master users from editing template source code. Template styles remain accessible."
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_LABEL="Disable Install from URL"
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from URL for ALL users (including master) as a safety measure."
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
; ===== Content Sync fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
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_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
; ===== Diagnostics fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
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"
; ===== Diagnostics =====
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."
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_LABEL="Grafana API Key"
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_DESC="Service account token or API key with Editor role in Grafana. Required for auto-provisioning the MokoWaaS datasource and dashboard."
; ===== Security fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL="Security Hardening"
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC="HTTPS enforcement, session timeouts, password policy, and upload restrictions."
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
PLG_SYSTEM_MOKOWAAS_PASSWORD_NUMBER_LABEL="Require Number"
PLG_SYSTEM_MOKOWAAS_PASSWORD_SPECIAL_LABEL="Require Special Character"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_LABEL="Allowed Upload Types"
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file extensions for media uploads."
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
; ===== Demo Mode fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
; ===== Site Aliases fieldset =====
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
@@ -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>"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

+4 -49
View File
@@ -16,13 +16,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.32.04
VERSION: 02.34.10
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.33.01-dev</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.10</version>
<description>MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
<scriptfile>script.php</scriptfile>
@@ -40,7 +40,6 @@
<folder>Extension</folder>
<folder>Field</folder>
<folder>Helper</folder>
<folder>Service</folder>
<folder>forms</folder>
<folder>payload</folder>
<folder>services</folder>
@@ -48,14 +47,6 @@
<folder>administrator</folder>
</files>
<media destination="plg_system_mokowaas" folder="media">
<filename>index.html</filename>
<filename>favicon.ico</filename>
<filename>favicon.svg</filename>
<filename>favicon_256.png</filename>
<filename>logo.png</filename>
</media>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas.ini</language>
<language tag="en-US">en-US/plg_system_mokowaas.ini</language>
@@ -89,42 +80,6 @@
readonly="true"
/>
</fieldset>
<fieldset name="demo_mode"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field name="demo_scheduled_task" type="DemoTaskInfo"
label="PLG_SYSTEM_MOKOWAAS_DEMO_TASK_INFO_LABEL"
/>
</fieldset>
<fieldset name="security"
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
>
<field
name="emergency_access"
type="radio"
label="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="allowed_ips_display"
type="AllowedIps"
label=""
/>
<field
name="current_ip_display"
type="CurrentIp"
label=""
/>
</fieldset>
</fields>
</config>
</extension>
+1 -72
View File
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.33.01
* VERSION: 02.34.10
* 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'),
];
@@ -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.
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/mokowaas
* VERSION: 02.33.01
* VERSION: 02.34.10
* PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container
@@ -13,3 +13,5 @@ PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_LABEL="Reset All Hits"
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_DESC="One-shot: reset article hit counters on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_LABEL="Reset Download Keys"
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_DESC="One-shot: clear all download keys (dlid) from update sites on save. Automatically turns off after execution."
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSDevTools</namespace>
@@ -52,6 +52,14 @@
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="reset_download_keys" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
</fields>
</config>
@@ -111,6 +111,13 @@ class DevTools extends CMSPlugin implements SubscriberInterface
$params->set('delete_versions', 0);
}
// Reset download keys on save if toggled on
if ($params->get('reset_download_keys', 0))
{
$this->resetDownloadKeys();
$params->set('reset_download_keys', 0);
}
// Reset the one-shot toggles
if ($table->params !== $params->toString())
{
@@ -152,4 +159,41 @@ class DevTools extends CMSPlugin implements SubscriberInterface
return $count;
}
private function resetDownloadKeys(): int
{
$db = Factory::getDbo();
// Find update sites that have a dlid in extra_query
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('extra_query') . ' LIKE ' . $db->quote('%dlid=%'))
);
$sites = $db->loadObjectList();
$count = 0;
foreach ($sites as $site)
{
// Parse the query string, remove dlid, rebuild
parse_str($site->extra_query, $parsed);
unset($parsed['dlid']);
$newQuery = http_build_query($parsed);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__update_sites'))
->set($db->quoteName('extra_query') . ' = ' . $db->quote($newQuery))
->where($db->quoteName('update_site_id') . ' = ' . (int) $site->update_site_id)
)->execute();
$count++;
}
$this->getApplication()->enqueueMessage(\sprintf('Cleared download keys from %d update sites.', $count), 'message');
return $count;
}
}
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace>
@@ -65,6 +65,7 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
{
return [
'onAfterInitialise' => 'onAfterInitialise',
'onAfterRoute' => 'onAfterRoute',
'onUserBeforeSave' => 'onUserBeforeSave',
];
}
@@ -792,4 +793,177 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
$config->set('upload_maxsize', $maxMb);
}
}
// ==================================================================
// Extension Protection (#155)
// ==================================================================
/**
* Protect MokoWaaS extensions after routing.
*
* @return void
*
* @since 02.35.00
*/
public function onAfterRoute(): void
{
$app = $this->getApplication();
if (!$app->isClient('administrator'))
{
return;
}
$this->protectPlugin();
}
/**
* 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
*/
private function protectPlugin(): void
{
// Ensure protected flag is set (self-healing -- runs once per session)
static $flagChecked = false;
if (!$flagChecked)
{
$flagChecked = true;
$this->ensureProtectedFlag();
}
if (MokoWaaSHelper::isMasterUser())
{
return;
}
$app = $this->getApplication();
$option = $app->input->get('option', '');
$task = $app->input->get('task', '');
// Block non-master from uninstalling MokoWaaS
if ($option === 'com_installer' && strpos($task, 'manage.remove') !== false)
{
$cid = $app->input->get('cid', [], 'array');
if ($this->isOurExtension($cid))
{
$app->enqueueMessage('MokoWaaS cannot be uninstalled.', 'error');
$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 = $app->input->get('cid', [], 'array');
if ($this->isOurExtension($cid))
{
$app->enqueueMessage('MokoWaaS cannot be disabled.', 'error');
$app->redirect('index.php?option=com_plugins');
}
}
// Block non-master from viewing or editing MokoWaaS plugin settings
if ($option === 'com_plugins')
{
$view = $app->input->get('view', '');
$layout = $app->input->get('layout', '');
$extensionId = (int) $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)
{
$app->enqueueMessage('MokoWaaS settings are restricted to the master user.', 'warning');
$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
*/
private function ensureProtectedFlag(): void
{
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
*/
private 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;
}
}
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>PLG_SYSTEM_MOKOWAAS_MONITOR_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSMonitor</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>PLG_SYSTEM_MOKOWAAS_OFFLINE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSOffline</namespace>
@@ -30,11 +30,11 @@ final class Tos extends CMSPlugin implements SubscriberInterface
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
'onAfterInitialise' => 'onAfterInitialise',
];
}
public function onAfterRoute(): void
public function onAfterInitialise(): void
{
$app = $this->getApplication();
@@ -61,9 +61,17 @@ final class Tos extends CMSPlugin implements SubscriberInterface
$slugs = (array) $slugs;
}
// Default bypassed pages when none configured
if (empty($slugs))
{
return;
$slugs = [
'legal/terms-of-service',
'legal/privacy-policy',
'legal/community-guidelines',
'support',
'support/tickets',
'support/submit-a-ticket',
];
}
$includeChildren = (int) $this->params->get('include_children', 1);
@@ -167,6 +175,5 @@ final class Tos extends CMSPlugin implements SubscriberInterface
private function bypassOffline($config, $app): void
{
$config->set('offline', 0);
$app->getInput()->set('tmpl', 'component');
}
}
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>PLG_SYSTEM_MOKOWAAS_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSTenant</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSTickets</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>PLG_TASK_MOKOWAASDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>PLG_TASK_MOKOWAASSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoWaaSSync</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
<files>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<description>Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.</description>
<namespace path="src">Moko\Plugin\WebServices\PerfectPublisher</namespace>
<files>
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
* VERSION: 02.33.01
* VERSION: 02.34.10
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
*/
@@ -8,7 +8,7 @@
* INGROUP: MokoWaaS
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
* VERSION: 02.33.01
* VERSION: 02.34.10
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
*/
+3 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoWaaS</name>
<packagename>mokowaas</packagename>
<version>02.33.01-dev</version>
<version>02.34.10</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -10,6 +10,8 @@
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GNU General Public License version 3 or later; see LICENSE</license>
<description>MokoWaaS site management suite — admin dashboard, security firewall, tenant restrictions, health monitoring, developer tools, and REST API.</description>
<dlid prefix="dlid=" suffix=""/>
<blockChildUninstall>true</blockChildUninstall>
<scriptfile>script.php</scriptfile>
<files folder="packages">
+209 -5
View File
@@ -89,6 +89,9 @@ class Pkg_MokowaasInstallerScript
// Set menu_icon params on submenu items (Joomla only renders img on level 1)
$this->fixMenuIcons();
// Set up MokoWaaS guided tours and unpublish Joomla defaults
$this->setupGuidedTours();
// Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level)
$this->protectExtensions();
@@ -98,6 +101,9 @@ class Pkg_MokowaasInstallerScript
// Clean up stale/duplicate update sites
$this->cleanupStaleUpdateSites();
// Fix orphaned update records (extension_id=0)
$this->fixUpdateRecords();
// Trigger heartbeat registration
$this->sendHeartbeat();
@@ -552,6 +558,29 @@ class Pkg_MokowaasInstallerScript
*
* @since 02.31.00
*/
private function fixUpdateRecords(): void
{
try
{
$db = Factory::getDbo();
// Link orphaned #__updates records to the installed extension
$db->setQuery(
"UPDATE " . $db->quoteName('#__updates') . " u"
. " JOIN " . $db->quoteName('#__extensions') . " e"
. " ON u.element = e.element AND u.type = e.type"
. " SET u.extension_id = e.extension_id"
. " WHERE u.extension_id = 0"
. " AND u.element LIKE " . $db->quote('%mokowaas%')
);
$db->execute();
}
catch (\Throwable $e)
{
// Non-critical
}
}
private function cleanupStaleUpdateSites(): void
{
try
@@ -951,8 +980,8 @@ class Pkg_MokowaasInstallerScript
$iconMap = [
'class:cogs' => 'icon-cogs',
'class:puzzle-piece' => 'icon-puzzle-piece',
'class:headphones' => 'icon-headphones',
'class:file-code' => 'icon-file-code',
'class:headphones' => 'fa-solid fa-handshake-angle',
'class:file-code' => 'fa-solid fa-file-code',
'class:lock' => 'icon-lock',
'class:shield-alt' => 'icon-shield-alt',
'class:database' => 'icon-database',
@@ -963,10 +992,15 @@ class Pkg_MokowaasInstallerScript
'class:bolt' => 'icon-bolt',
];
// Find all MokoWaaS component submenu items (including those linking to other components)
$db->setQuery(
"SELECT id, img, params FROM #__menu"
. " WHERE client_id = 1 AND level >= 2"
. " AND link LIKE '%com_mokowaas%'"
$db->getQuery(true)
->select(['m.id', 'm.img', 'm.params'])
->from($db->quoteName('#__menu', 'm'))
->where('m.client_id = 1')
->where('m.level >= 2')
->where('m.parent_id IN (SELECT id FROM ' . $db->quoteName('#__menu')
. ' WHERE client_id = 1 AND level = 1 AND link LIKE ' . $db->quote('%com_mokowaas%') . ')')
);
foreach ($db->loadObjectList() as $item)
@@ -1001,6 +1035,176 @@ class Pkg_MokowaasInstallerScript
}
}
/**
* Unpublish default Joomla guided tours and create MokoWaaS tours.
* Re-enables the guided tours plugin if disabled.
*/
private function setupGuidedTours(): void
{
try
{
$db = Factory::getDbo();
// Re-enable guided tours plugin (may have been disabled)
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('element') . ' = ' . $db->quote('guidedtours'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
)->execute();
// Re-enable the guided tours module (shows our tours, not Joomla's)
$db->setQuery(
"UPDATE " . $db->quoteName('#__modules')
. " SET published = 1, title = 'MokoWaaS Tours'"
. " WHERE module = 'mod_guidedtours'"
);
$db->execute();
// Override the guided tours module language string
$overridePath = JPATH_ADMINISTRATOR . '/language/overrides/en-GB.override.ini';
$overrides = file_exists($overridePath) ? parse_ini_file($overridePath) : [];
if (empty($overrides['MOD_GUIDEDTOURS']))
{
$overrides['MOD_GUIDEDTOURS'] = 'MokoWaaS Tours';
$overrides['MOD_GUIDEDTOURS_TITLE'] = 'MokoWaaS Tours';
$lines = [];
foreach ($overrides as $k => $v)
{
$lines[] = $k . '="' . str_replace('"', '\"', $v) . '"';
}
file_put_contents($overridePath, implode("\n", $lines) . "\n");
}
// Unpublish all default Joomla tours
$db->setQuery(
"UPDATE " . $db->quoteName('#__guidedtours')
. " SET published = 0"
. " WHERE " . $db->quoteName('uid') . " LIKE 'joomla-%'"
);
$db->execute();
// Define MokoWaaS tours
$tours = [
[
'uid' => 'mokowaas-welcome',
'title' => 'Welcome to MokoWaaS',
'desc' => 'Get started with the MokoWaaS Admin Tools Suite. This tour shows you the key areas of your admin dashboard.',
'url' => 'administrator/index.php?option=com_mokowaas',
'steps' => [
['title' => 'MokoWaaS Dashboard', 'desc' => 'This is your MokoWaaS control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.', 'target' => '#mokowaas-dashboard', 'type' => 0],
['title' => 'Site Information', 'desc' => 'The info bar shows your Joomla version, PHP version, database type, and debug/offline status at a glance.', 'target' => '.mokowaas-info-bar', 'type' => 0],
['title' => 'Quick Actions', 'desc' => 'Use these buttons to clear cache, check updates, manage extensions, and perform common admin tasks with one click.', 'target' => '#mokowaas-btn-cache', 'type' => 0],
['title' => 'Feature Plugins', 'desc' => 'MokoWaaS features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.', 'target' => '.mokowaas-plugin-grid', 'type' => 0],
['title' => 'MokoWaaS Menu', 'desc' => 'The MokoWaaS sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.', 'target' => '.mokowaas-admin-menu, [class*="mokowaas"]', 'type' => 0],
],
],
[
'uid' => 'mokowaas-firewall',
'title' => 'MokoWaaS Firewall Setup',
'desc' => 'Configure the Web Application Firewall to protect your site from common attacks.',
'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&filter[search]=mokowaas_firewall',
'steps' => [
['title' => 'Firewall Plugin', 'desc' => 'The MokoWaaS Firewall provides 10 security shields including SQL injection, XSS, and malicious user agent detection.', 'target' => '', 'type' => 0],
['title' => 'WAF Shields', 'desc' => 'Enable or disable individual WAF shields. Each shield protects against a specific attack vector. All shields are enabled by default.', 'target' => '', 'type' => 0],
['title' => 'Security Headers', 'desc' => 'Configure HTTP security headers like X-Frame-Options, Content-Security-Policy, and HSTS to harden your site against browser-based attacks.', 'target' => '', 'type' => 0],
['title' => 'IP Blocklist', 'desc' => 'Block specific IP addresses, CIDR ranges, or wildcard patterns. The auto-ban feature automatically blocks IPs that trigger too many WAF alerts.', 'target' => '', 'type' => 0],
],
],
[
'uid' => 'mokowaas-helpdesk',
'title' => 'MokoWaaS Helpdesk',
'desc' => 'Learn how to manage support tickets, categories, and automation rules.',
'url' => 'administrator/index.php?option=com_mokowaas&view=tickets',
'steps' => [
['title' => 'Ticket List', 'desc' => 'View all support tickets with status, priority, SLA tracking, and assignment. Filter by status or search to find specific tickets.', 'target' => '', 'type' => 0],
['title' => 'Create a Ticket', 'desc' => 'Click the New button to create a support ticket. Assign a category, priority, and optional SLA deadline.', 'target' => '', 'type' => 0],
['title' => 'Ticket Automation', 'desc' => 'Set up automation rules that trigger on ticket events (new ticket, status change) or Joomla events (user login, registration). Automate assignment, notifications, and status changes.', 'target' => '', 'type' => 0],
],
],
[
'uid' => 'mokowaas-extensions',
'title' => 'Moko Extensions Manager',
'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.',
'url' => 'administrator/index.php?option=com_mokowaas&view=extensions',
'steps' => [
['title' => 'Extension Catalog', 'desc' => 'Browse all available Moko Consulting extensions. Each card shows the extension name, description, install status, and current version.', 'target' => '', 'type' => 0],
['title' => 'Install Extensions', 'desc' => 'Click Install to add an extension from the Moko Consulting repository. Updates are handled through Joomla\'s standard update system.', 'target' => '', 'type' => 0],
],
],
];
foreach ($tours as $tourDef)
{
// Check if tour already exists
$db->setQuery(
$db->getQuery(true)
->select('id')
->from($db->quoteName('#__guidedtours'))
->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid']))
);
if ($db->loadResult())
{
continue;
}
$tour = (object) [
'title' => $tourDef['title'],
'uid' => $tourDef['uid'],
'description' => $tourDef['desc'],
'extensions' => '',
'url' => $tourDef['url'],
'created' => date('Y-m-d H:i:s'),
'created_by' => 0,
'modified' => date('Y-m-d H:i:s'),
'modified_by' => 0,
'published' => 1,
'language' => '*',
'note' => 'MokoWaaS',
'access' => 3,
'ordering' => 0,
'autostart' => 0,
];
$db->insertObject('#__guidedtours', $tour, 'id');
$tourId = (int) $tour->id;
foreach ($tourDef['steps'] as $i => $stepDef)
{
$step = (object) [
'tour_id' => $tourId,
'title' => $stepDef['title'],
'description' => $stepDef['desc'],
'target' => $stepDef['target'],
'type' => $stepDef['type'],
'interactive_type' => 1,
'url' => '',
'position' => 'bottom',
'ordering' => $i + 1,
'published' => 1,
'created' => date('Y-m-d H:i:s'),
'created_by' => 0,
'modified' => date('Y-m-d H:i:s'),
'modified_by' => 0,
'language' => '*',
'note' => '',
'params' => '{}',
];
$db->insertObject('#__guidedtour_steps', $step, 'id');
}
}
}
catch (\Throwable $e)
{
Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
/**
* Create a "Support" menu item on the frontend main menu.
*/
+4 -5
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 02.33.01-dev
VERSION: 02.34.10-dev
-->
<updates>
@@ -11,13 +11,12 @@
<element>pkg_mokowaas</element>
<type>package</type>
<client>site</client>
<version>02.33.01-dev</version>
<creationDate>2026-06-04</creationDate>
<version>02.34.10-dev</version>
<creationDate>2026-06-06</creationDate>
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.33.01-dev.zip</downloadurl>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.34.10-dev.zip</downloadurl>
</downloads>
<sha256>be79af0f02a31ec83f15f3319af00cbf084590e72ef2cf13693a23028206539a</sha256>
<tags><tag>dev</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>