Compare commits

...

275 Commits

Author SHA1 Message Date
Jonathan Miller 55ec926fdc chore: remove updates.xml references from CLAUDE.md
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 15s
Universal: Build & Release / Promote to RC (pull_request) Successful in 19s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 33s
updates.xml is deprecated — MokoGitea generates feeds dynamically.
2026-06-06 11:26:51 -05:00
Jonathan Miller b4f916addb chore: remove deprecated updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
MokoGitea generates update feeds dynamically from releases.
Static updates.xml is no longer needed.
2026-06-06 11:16:51 -05:00
Jonathan Miller a51f04c841 feat: provision reset API for new client setup from Base
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
ProvisionController: POST /api/v1/mokowaas/provision-reset
- Resets article hits to zero
- Deletes content version history
- Regenerates heartbeat token (optional, for breach response)
- Revokes all user API tokens with email notification (optional)
- Sets setup-required flag for new client info collection

Core plugin: checkSetupRequired() shows persistent admin banner
until plugin settings are saved. Clears flag on save.

Route registered in webservices plugin.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 11:10:02 -05:00
Jonathan Miller db2ed26e65 feat: heartbeat sends full health payload and client info
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Monitor plugin now includes live health data (16 checks) and client
info (company name, email from Joomla config) in the heartbeat payload.
Health data is fetched via local HTTP call to /?mokowaas=health to
reuse existing auth and check logic without internal coupling.

MokoWaaSBase can use client_info for auto-contact creation and the
health payload for dashboard visualization without separate polling.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 11:01:33 -05:00
Jonathan Miller 509470b20b feat: remote login endpoint for MokoWaaSBase auto-login
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
- RemoteLoginController: POST /api/v1/mokowaas/remote-login validates
  health token, generates a one-time login token (60s TTL), returns
  a URL that auto-authenticates the master user
- Core plugin: handleOneTimeLogin() in onAfterInitialise checks for
  mokowaas_otl query param, validates and consumes the token, logs in
  the master user, logs the event, redirects to admin dashboard
- Webservices plugin: register remote-login route

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 10:31:32 -05:00
Jonathan Miller 76254db28c feat: show support verification PIN below heartbeat token
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
Derives MOKO-XXXX-XXXX from health token for phone-based identity
verification during support calls. Displayed below the token field
in the core plugin config.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:55:46 -05:00
Jonathan Miller f176f424b5 feat: monitor plugin sends heartbeat to MokoWaaSBase REST API
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 29s
Replace ad-hoc Grafana receiver with MokoWaaSBase API integration.
The heartbeat now posts to /api/index.php/v1/mokowaasbase/heartbeat
with token, domain, site_name, versions. Configurable base URL
(defaults to mokoconsulting.tech). Removed hardcoded HEARTBEAT_URL
and HEARTBEAT_KEY constants.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:33:16 -05:00
Jonathan Miller a5b94284cb chore: move CLAUDE.md to .mokogitea/ directory
Relocate CLAUDE.md from repo root to .mokogitea/ per project convention.
Content updated with focused, repo-specific architecture and rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:31:20 -05:00
Jonathan Miller b24c563cc9 chore: update .mokogitea/CLAUDE.md to reflect current architecture
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Remove outdated branding/identity description, fix source directory
paths, update extension list to include all 17 sub-extensions,
document updates.xml is now in-repo, add Joomla 5/6 event compat note.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:13:21 -05:00
Jonathan Miller d94909eb91 fix: relocate service classes to their owning plugins
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
DemoResetService, ContentSyncService, and ContentSyncReceiver were
deleted from the core plugin but still referenced by the demo task
plugin and API controllers. Moved service classes into their owning
packages with updated namespaces:

- DemoResetService → plg_task_mokowaasdemo/src/Service/
- ContentSyncService → plg_task_mokowaassync/src/Service/
- ContentSyncReceiver → plg_task_mokowaassync/src/Service/

Updated all require_once paths and FQCN references in API controllers
and DemoReset task plugin.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:10:28 -05:00
Jonathan Miller 19590cef8c refactor: remove dead placeholder resolver from install script
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
Override values are now hardcoded in the .ini files. Removed
getPlaceholders(), getPluginParams(), and resolvePlaceholders() methods
that were doing no-op string replacements.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 09:03:40 -05:00
Jonathan Miller d353b1ee36 fix: Joomla 5/6 event compatibility in DevTools and Monitor plugins
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Both plugins used Joomla 6-only event->getArgument() pattern without
fallback for Joomla 5 individual args. Added dual-compat pattern
matching DemoReset plugin: check for event object, fallback to
func_get_arg for Joomla 5.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 08:28:23 -05:00
Jonathan Miller d07eb89f66 feat: implement block_frontend_superuser in firewall plugin
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
The config field existed but had no backing implementation. Super admin
users are now redirected to the admin panel when accessing the frontend
if the toggle is enabled.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 08:25:54 -05:00
Jonathan Miller 398fefe2fd feat: show component and module version tiles on dashboard
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Add getMokoExtensions() to DashboardModel to fetch installed MokoWaaS
component and modules with versions. Renders version badges below the
info bar for com_mokowaas, mod_mokowaas_cpanel, mod_mokowaas_menu,
mod_mokowaas_cache, and mod_mokowaas_categories.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 08:17:12 -05:00
Jonathan Miller 5e33f94cce chore: bump version to 02.34.16-dev, update updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 08:08:45 -05:00
jmiller e7cdc41648 Merge pull request 'chore: rename src/ to source/ per moko-platform standards (#188)' (#189) from chore/rename-src-to-source into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
2026-06-06 12:48:06 +00:00
Jonathan Miller e3c15979b8 chore: rename src/ to source/ per moko-platform standards (#188)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 21s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 26s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Failing after 7s
Rename top-level src/ directory to source/ and update all references
in .gitignore, CLAUDE.md, manifest.xml, docs, and PATH comments.
Internal namespace path="src" attributes within extension packages
are unchanged (they refer to the package-internal src/ folder).

Closes #188

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:43:59 -05:00
Jonathan Miller 68ab5bdd44 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 32s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
2026-06-06 07:33:25 -05:00
Jonathan Miller 1fe19fe5f1 fix: cache module uses split button layout "Clear: Cache | Temp"
Single header item with two clickable halves instead of two separate
status bar entries.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:32:19 -05:00
jmiller 17ef84e867 chore: sync .mokogitea/workflows/pre-release.yml from moko-platform [skip ci] 2026-06-06 12:31:31 +00:00
Jonathan Miller 8d4a5b7a04 chore: bump version to 02.34.15-dev, update updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 5s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 20s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:29:51 -05:00
Jonathan Miller 9fed55d5c0 fix: rename core plugin label, add temp cleaner to cache module
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
- Dashboard: rename "Core — Branding & Identity" to "Core" with updated description
- Cache module: add temp directory cleaner button (trash icon) alongside cache cleaner
- DashboardModel: add clearTemp() method that removes tmp contents preserving .htaccess
- DisplayController: add clearTemp task handler

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:24:52 -05:00
Jonathan Miller d79d5393be chore: bump version to 02.34.14-dev, update updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:17:47 -05:00
Jonathan Miller 3cfe653b18 fix: firewall language strings and subform path after core cleanup
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
- Add forms/trusted_ip_entry.xml to firewall plugin (was deleted from core)
- Update formsource paths to point to firewall's own forms directory
- Add missing Security Headers fieldset language strings
- Use hardcoded English labels in subform to avoid cross-plugin translation issues

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:08:32 -05:00
Jonathan Miller 882a4bfb5c chore: bump version to 02.34.13-dev, update updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 27s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:06:39 -05:00
Jonathan Miller d792b7ff0c fix: preserve download keys across Joomla extension updates (#187)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 33s
Joomla's installer can wipe extra_query (dlid) from #__update_sites
when rebuilding or reinstalling. The core plugin now backs up all
download keys and auto-restores any that get cleared. Runs on every
admin page load with a single lightweight query.

Closes #187

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 07:02:22 -05:00
Jonathan Miller 68ffffe2af chore: bump version to 02.34.12-dev, update updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 27s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:53:56 -05:00
Jonathan Miller 0fb82306bb feat: XML-based extension catalog with update server discovery (#186)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 31s
Replace hardcoded CATALOG constant with catalog.xml that points to each
extension's updates.xml. The model fetches update servers at runtime to
resolve latest version and download URL. Adds update_available status
with Update button in the extensions view.

Closes #186

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:49:52 -05:00
Jonathan Miller b170894228 chore: bump version to 02.34.11-dev, update updates.xml
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:37:21 -05:00
Jonathan Miller 082fa0798c feat: add auto-category menu module for knowledge base sections (#184)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
New module mod_mokowaas_categories auto-discovers article categories
from a configurable root and renders them as a collapsible sidebar tree.
Supports configurable depth, article counts, empty category filtering,
and ACL-aware access. Matches existing MokoWaaS sidebar styling.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 06:34:17 -05:00
Jonathan Miller d1ee2ef3f4 fix: API controller execute() signatures compatible with BaseController (#183)
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
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) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 38s
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) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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) Failing after 24s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 6s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
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
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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
- 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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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 skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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 skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
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 skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
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 / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
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 skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 35s
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 skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
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) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
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) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 22s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
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 skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
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 skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 10s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 27s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
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 skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Successful in 12s
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request_target) Failing after 14s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 32s
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
Jonathan Miller ee7260b435 fix: change /kb/ URLs to /support/products/ across all references
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 25s
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 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
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:53:39 -05:00
Jonathan Miller 9498a56f98 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
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 13:50:22 -05:00
Jonathan Miller 8ea6df020b refactor: remove branding/identity from core plugin (#154)
Remove 8 branding methods and 4 call sites (-428 lines):
- enforceAtumBranding(), hexToHsl(), injectFavicon()
- enforceLoginSupportUrls(), loadLanguageOverrides()
- getPlaceholders(), parseLanguageFile(), setTemplateParam()

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

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

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:23:18 -05:00
Jonathan Miller e858130375 fix: hide cpanel module on MokoWaaS dashboard (redundant info)
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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 dashboard already shows site info, plugins, WAF blocks, logins,
and updates. The cpanel module duplicates this. Now it auto-hides
when option=com_mokowaas and view is dashboard or empty.

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

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 13:14:09 -05:00
Jonathan Miller f401a76227 chore: changelog [Unreleased] workflow, CI gate, contributing docs
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
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
- CHANGELOG.md now uses [Unreleased] section as staging area
- Only minor version headings (no dev patch numbers)
- pr-check.yml blocks PRs to main if [Unreleased] is empty
- pre-release.yml and auto-release.yml extract from [Unreleased] for release notes
- CONTRIBUTING.md documents the changelog workflow
- RC pre-release bumps minor version consolidating dev patches

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Submenu: MokoWaaS > Privacy Guard

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

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

Email format: plain text with ticket details + view link

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

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

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

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

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 22:03:54 -05:00
gitea-actions[bot] 9cf3b51024 chore: update development channel 02.32.38 [skip ci] 2026-06-03 00:29:36 +00:00
gitea-actions[bot] 6f762534fe chore(version): auto-bump 02.32.38 [skip ci] 2026-06-03 00:29:35 +00:00
Jonathan Miller c8af0fa5ca Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 19:29:18 -05:00
Jonathan Miller ac920b997a feat: helpdesk automation engine + scheduled task plugin
Automation engine in TicketsModel:
- Condition evaluator: field/op/value with eq, neq, gt, lt, in, not_in
- Action executor: set_status, set_priority, assign, add_note
- Trigger events: ticket_created, ticket_replied, status_changed, scheduled
- Hooks wired into createTicket, addReply, updateStatus

Scheduled task plugin (plg_task_mokowaas_tickets):
- Runs all 'scheduled' automation rules against non-closed tickets
- Evaluates age_hours, status, priority, sla_responded
- Joomla Scheduler integration via TaskPluginTrait

Default automation rules:
1. Auto-close resolved tickets after 7 days
2. Escalate urgent tickets with no response in 1 hour
3. Notify on high/urgent ticket creation

Also:
- Added #__mokowaas_ticket_automation table
- Fixed dashboard ImportModel null error (direct instantiation)
- Added task plugin to package manifest + script.php

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 19:29:10 -05:00
gitea-actions[bot] c5552a94fb chore: update development channel 02.32.37 [skip ci] 2026-06-02 23:48:58 +00:00
gitea-actions[bot] 8168bfb2dc chore(version): auto-bump 02.32.37 [skip ci] 2026-06-02 23:48:57 +00:00
Jonathan Miller d73b8b06ef Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Update Server / Update Server (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 32s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 18:48:42 -05:00
Jonathan Miller f3a3bc90b3 feat: frontend ticket management with smart permissions
Ticket List (view=tickets):
- Staff see all tickets with Submitted By + Assigned To columns
- Customers see only their own tickets
- Status filter dropdown for staff
- isStaff check: core.admin OR mokowaas.tickets ACL

Ticket Detail (view=ticket):
- Staff see all tickets + internal notes (yellow highlight)
- Customers see only their own + no internal notes
- Staff sidebar: ticket details, change status buttons, assign to me
- Staff can post internal notes from frontend
- Customers can reply (disabled for closed tickets)
- Ownership enforced: customers can't view/reply to others' tickets

Controller:
- updateStatus: staff only
- assignTicket: mokowaas.tickets.assign ACL
- submitReply: verifies ownership OR staff

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 18:48:13 -05:00
jmiller 756c2bff32 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-02 23:47:10 +00:00
gitea-actions[bot] b8fbb0d1d6 chore: update development channel 02.32.36 [skip ci] 2026-06-02 23:46:31 +00:00
gitea-actions[bot] fd6c79d3a2 chore(version): auto-bump 02.32.36 [skip ci] 2026-06-02 23:46:30 +00:00
Jonathan Miller f350cd0169 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 18:46:15 -05:00
Jonathan Miller 4a18318cb9 feat: search KB before submitting ticket (frontend + backend)
Both frontend and admin ticket creation now show a KB search step
first. User types their issue, Smart Search (com_finder) returns
matching articles. If nothing helps, they click through to the
ticket form with the search query pre-filled as the subject.

Frontend (/support/submit-ticket):
- Search input with live results from #__finder_links
- Article links open in new tab
- "Submit Anyway" shows the ticket form
- Falls back to direct form if Smart Search has no indexed content

Admin (New Ticket modal):
- Same KB search step before the form
- Results as clickable list-group items
- Modal resets on close

Also:
- Added searchKb controller task (both frontend + admin)
- Added "Submit a Ticket" child menu item under Support on install
- Fixed innerHTML → safe DOM methods for search results

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 18:46:12 -05:00
jmiller ce04701616 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-02 21:51:27 +00:00
Jonathan Miller b37120341f feat: auto-create Support menu item on package install
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Failing after 8s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
Creates a frontend menu item at /support pointing to the customer
portal (com_mokowaas&view=tickets). Access level: Registered.
Idempotent — skips if already exists.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 16:47:21 -05:00
Jonathan Miller 7f0b7756e4 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Failing after 8s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 16:39:14 -05:00
Jonathan Miller 80c2658b06 feat: customer portal - frontend ticket submission and history
Frontend (site-side) views for authenticated users:

Ticket List (view=tickets):
- Shows user's own tickets with status badges
- Collapsible new ticket form with category + priority
- Redirects guests to login with return URL

Ticket Detail (view=ticket):
- Conversation thread (excludes internal notes)
- Staff replies highlighted with badge
- Reply form (disabled for closed/resolved tickets)
- User can only see their own tickets

Also fixed .gitignore to allow src/packages/*/site/ directories.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 16:39:06 -05:00
Moko Consulting 995fc4b591 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:33:07 +00:00
Moko Consulting 240a947bec chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:33:06 +00:00
Moko Consulting a2091b1a67 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:33:04 +00:00
gitea-actions[bot] eab0ed1b80 chore: update development channel 02.32.35 [skip ci] 2026-06-02 20:51:48 +00:00
gitea-actions[bot] 23d6a1ad44 chore(version): auto-bump 02.32.35 [skip ci] 2026-06-02 20:51:45 +00:00
Jonathan Miller 2706d81267 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Update Server / Update Server (push) Successful in 17s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 31s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 15:51:29 -05:00
Jonathan Miller ed138fdc57 feat: Akeeba Admin Tools + Ticket System importers with auto-disable
New ImportModel with two importers:

Admin Tools Importer:
- Reads #__admintools_wafconfig → maps WAF shields, admin secret,
  session timeout, template blocking to firewall plugin params
- Reads #__admintools_storage → maps server signature, HSTS, GZip,
  expires, ETag, www redirect, directory listing to htaccess options
- Reads #__admintools_ipblock → merges into firewall IP deny list
- Disables com_admintools + all admintools plugins after import

Akeeba Ticket System Importer:
- Imports tickets with status/priority mapping
- First post becomes ticket body, rest become replies
- Imports canned replies
- Disables com_ats + all ATS plugins after import

Dashboard shows blue import banner when Akeeba data is detected.
Both importers require core.admin (Super Users only).
Idempotent — safe to run multiple times.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:51:22 -05:00
gitea-actions[bot] 883e7c72f0 chore: update development channel 02.32.34 [skip ci] 2026-06-02 20:47:40 +00:00
gitea-actions[bot] cb33aabb0c chore(version): auto-bump 02.32.34 [skip ci] 2026-06-02 20:47:38 +00:00
Jonathan Miller fe87e9038a feat: Akeeba Ticket System importer
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 29s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 15s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Auto-detects ATS tables (#__ats_tickets, #__ats_posts, #__ats_cannedreplies)
and shows an import button on the helpdesk ticket list with counts.

Imports:
- Tickets: maps ATS status (O/P/C) to MokoWaaS statuses, priority 1-5
  to low/normal/high/urgent, preserves created_by/assigned_to/dates
- Posts: first post becomes ticket body, subsequent posts become replies
- Canned replies: imported with title and body (HTML stripped)
- Skips duplicates on re-import (idempotent)

ACL enforced: requires mokowaas.tickets permission.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:44:32 -05:00
Jonathan Miller c4b3892d9c fix: resolve merge conflict in offline plugin manifest [skip ci]
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:39:28 -05:00
Jonathan Miller adccf3bd2a feat: rename mokowaas_tos to mokowaas_offline + auto-migrate from standalone MokoJoomTOS
Renamed plugin element from mokowaas_tos to mokowaas_offline to better
reflect its purpose (offline bypass, not just TOS).

Added migrateStandalonePlugins() to package script:
- Detects if standalone mokojoomtos plugin is installed
- Copies its params (configured slugs) to mokowaas_offline
- Removes old extension record, update sites, and files
- Logs the migration

Future standalone-to-MokoWaaS migrations can be added to the
$migrations array.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:39:04 -05:00
gitea-actions[bot] bb03cd94d6 chore: update development channel 02.32.33 [skip ci] 2026-06-02 20:33:50 +00:00
gitea-actions[bot] 6ae5daffa2 chore(version): auto-bump 02.32.33 [skip ci] 2026-06-02 20:33:48 +00:00
Jonathan Miller 614b813056 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 0s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 15:33:39 -05:00
Jonathan Miller 33ce8b115c feat: wire ACL checks into all controller actions and views
Every view and AJAX task now checks its specific ACL permission:
- display() checks VIEW_ACL map per view name
- togglePlugin → mokowaas.plugins.toggle
- clearCache → mokowaas.cache
- installExtension → mokowaas.extensions
- saveHtaccess/generateHtaccess → mokowaas.htaccess
- createTicket → mokowaas.tickets.create
- addTicketReply/updateTicketStatus → mokowaas.tickets

Super admins (core.admin) always bypass all checks.
Refactored to use checkAcl/jsonResponse/jsonForbidden helpers.

Default ACL applied on dev:
- Super Users: all permissions
- Administrator: cache + ticket assign
- Manager: dashboard + tickets (view/create)
- Others: no access

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:33:33 -05:00
gitea-actions[bot] cf85a560e4 chore: update development channel 02.32.32 [skip ci] 2026-06-02 20:31:15 +00:00
gitea-actions[bot] 4684c4a1eb chore(version): auto-bump 02.32.32 [skip ci] 2026-06-02 20:31:13 +00:00
Jonathan Miller e69953ad17 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Update Server / Update Server (push) Successful in 14s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 28s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 15:31:02 -05:00
Jonathan Miller b50661d9ee feat: component ACL permissions for MokoWaaS
Added access.xml with granular permissions:
- core.admin / core.manage (Joomla standard)
- mokowaas.dashboard — view the control panel
- mokowaas.extensions — install/uninstall Moko packages
- mokowaas.htaccess — edit .htaccess configuration
- mokowaas.tickets — view helpdesk tickets
- mokowaas.tickets.create — create new tickets
- mokowaas.tickets.assign — assign tickets to users
- mokowaas.plugins.toggle — enable/disable feature plugins
- mokowaas.cache — clear Joomla cache

Configurable per user group in Options > Permissions tab.
Also moved MokoWaaS to top of admin sidebar menu.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:30:57 -05:00
gitea-actions[bot] 1ce287cb2e chore: update development channel 02.32.31 [skip ci] 2026-06-02 20:25:39 +00:00
gitea-actions[bot] 6798a5da7e chore(version): auto-bump 02.32.31 [skip ci] 2026-06-02 20:25:37 +00:00
Jonathan Miller 7425c412fc Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 15:25:14 -05:00
Jonathan Miller 7f64651517 feat: helpdesk / ticket system with SLA tracking
Full helpdesk built into com_mokowaas:

Ticket List View (view=tickets):
- Status summary cards (open, in progress, waiting, resolved, closed, SLA overdue)
- Filterable table (status, priority, category)
- SLA highlighting (red=overdue, yellow=at risk)
- New ticket modal with category, priority, description

Ticket Detail View (view=ticket):
- Conversation thread with replies
- Internal notes (staff-only, yellow highlight)
- Canned response selector
- Status action buttons (reopen, in progress, waiting, resolve, close)
- SLA response/resolution due with overdue indicators
- Ticket metadata sidebar

Model (TicketsModel):
- getTickets with filters, getTicket with replies
- createTicket with auto-assign from category + SLA deadlines
- addReply with SLA response tracking
- updateStatus with resolved/closed timestamps
- getCategories, getCannedResponses
- getStatusCounts, getOverdueTickets

Database:
- #__mokowaas_tickets (status, priority, SLA fields)
- #__mokowaas_ticket_replies (with internal notes)
- #__mokowaas_ticket_categories (SLA + auto-assign config)
- #__mokowaas_ticket_canned (response templates)
- 5 default categories with SLA presets

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 15:25:08 -05:00
gitea-actions[bot] db260008a2 chore: update development channel 02.32.30 [skip ci] 2026-06-02 19:59:14 +00:00
gitea-actions[bot] 8ea724116c chore(version): auto-bump 02.32.30 [skip ci] 2026-06-02 19:59:12 +00:00
Jonathan Miller 94b20b0c54 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 13s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 26s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 14:58:59 -05:00
Jonathan Miller 0e5caf6b3f fix: move boot() session lifetime logic from core to firewall plugin
Core plugin's boot() called ipIsTrusted() which was deleted in the
cleanup. The session lifetime extension for trusted IPs is now in the
firewall plugin's boot() method where ipIsTrusted() still exists.

Firewall now implements BootableExtensionInterface.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:58:24 -05:00
gitea-actions[bot] 25e2c29e2e chore: update development channel 02.32.29 [skip ci] 2026-06-02 19:52:39 +00:00
gitea-actions[bot] b5eebb0acc chore(version): auto-bump 02.32.29 [skip ci] 2026-06-02 19:52:38 +00:00
Jonathan Miller f3d6ef948b Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 14:52:17 -05:00
Jonathan Miller 1cdbfd035d feat: integrate MokoJoomTOS as Offline Bypass feature plugin + add OpenGraph to catalog
New plugin: plg_system_mokowaas_tos (Offline Bypass)
- Keeps configured pages accessible during offline mode
- SEF path matching + Itemid fallback
- Custom MenuslugField for multi-select menu items
- Include children option for parent menu matching
- Renamed from MokoJoomTOS to MokoWaaS Offline Bypass

Also:
- Added MokoJoomOpenGraph to extension manager catalog
- Added to package manifest, script.php, dashboard, cascade list

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:52:06 -05:00
gitea-actions[bot] b7d90f9b18 chore: update development channel 02.32.28 [skip ci] 2026-06-02 19:34:30 +00:00
gitea-actions[bot] 3be42ec37a chore(version): auto-bump 02.32.28 [skip ci] 2026-06-02 19:34:28 +00:00
Jonathan Miller 9565911089 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 14s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 14:34:13 -05:00
Jonathan Miller 9a375740b9 refactor: remove 12 dead method bodies from core plugin (-607 lines)
Removed: resetAllHits, deleteAllVersions, warnMissingLicenseKey,
enforceDevMode, onDevModeDisabled, enforceHttps,
enforceAdminSessionTimeout, ipIsTrusted, enforceUploadRestrictions,
enforceAdminRestrictions, blockAccess, getHiddenMenuComponents

These methods were already unreachable after the call sites were
removed in the previous commit. The functionality lives in the
firewall, tenant, and devtools feature plugins.

Core plugin: 5366 -> 4759 lines

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:33:57 -05:00
gitea-actions[bot] a89d516623 chore: update development channel 02.32.27 [skip ci] 2026-06-02 19:31:59 +00:00
gitea-actions[bot] cf39c169d2 chore(version): auto-bump 02.32.27 [skip ci] 2026-06-02 19:31:57 +00:00
Jonathan Miller 1ad1f1c010 refactor: remove duplicate features from core plugin
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Removed from onAfterInitialise:
- enforceHttps (now in firewall plugin)
- enforceDevMode (now in devtools plugin)
- enforceAdminSessionTimeout (now in firewall plugin)
- enforceUploadRestrictions (now in firewall plugin)

Removed from onAfterRoute:
- warnMissingLicenseKey (deferred - licensing not ready)
- enforceAdminRestrictions (now in tenant plugin)

Removed event handlers:
- onPreprocessMenuItems (now in tenant plugin)
- onUserBeforeSave (now in firewall plugin)

Removed from onExtensionAfterSave:
- resetAllHits / deleteAllVersions (now in devtools plugin)

Removed param fieldsets from manifest:
- tenant_restrictions (now in tenant plugin)
- dev_mode, reset_hits, delete_versions (now in devtools plugin)
- force_https, session_timeout, trusted_ips, password_*, upload_*
  (now in firewall plugin)

Core plugin now only handles: branding, master user, emergency access,
site aliases, Atum branding, language overrides, health API, Grafana
heartbeat, demo banner, plugin protection, and content sync.

Settings migration handled by migrateFeatureParams() in script.php.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:31:42 -05:00
Jonathan Miller 1e6a255fab fix: duplicate Core card on dashboard - webservices plugin matching core metadata [skip ci]
The webservices plugin (element=mokowaas, folder=webservices) was
matching the PLUGIN_META entry for the core system plugin since the
lookup key was just the element name. Fixed by only matching system
and task plugins against PLUGIN_META; webservices/other folders use
folder_element as the key and fall through to guessPluginMeta.

Also includes auto-remove retired monitor plugin on package update.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:21:03 -05:00
Jonathan Miller a78178b5dd feat: auto-remove retired monitor plugin on package update
Added removeRetiredExtensions() to package postflight script.
On every install/update, it checks for retired extensions and
removes them (DB record + files). Currently retires:
- plg_system_mokowaas_monitor (merged into core in 02.32.00)

Future retired extensions can be added to the $retired array.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:17:21 -05:00
gitea-actions[bot] 79c3cfc1f0 chore: update development channel 02.32.26 [skip ci] 2026-06-02 19:14:30 +00:00
gitea-actions[bot] dac5c6c052 chore(version): auto-bump 02.32.26 [skip ci] 2026-06-02 19:14:28 +00:00
Jonathan Miller b4beaf5bc9 Merge branches 'dev' and 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 14s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 30s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 14:14:14 -05:00
Jonathan Miller d563e2eac8 refactor: remove Health Monitor plugin - heartbeat built into core (#115)
The health monitor/Grafana heartbeat is mandatory infrastructure that
must always be active. The core system plugin (plg_system_mokowaas)
already has handleGrafanaProvisioning() and sendHeartbeat(), making
the separate monitor plugin redundant.

- Removed plg_system_mokowaas_monitor from package manifest
- Removed from script.php enable/protect lists
- Removed from cascade enable/disable logic
- Removed from dashboard plugin metadata
- Excluded from dashboard plugin discovery query
- Disabled on dev server (can be uninstalled manually)

Health monitoring is now entirely handled by the core plugin which
is protected and cannot be disabled.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:14:07 -05:00
Jonathan Miller 267beea8f9 chore: update MokoOnyx submodule to 02.17.00 stable
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:13:02 -05:00
gitea-actions[bot] 5a1a2f98b0 chore: update development channel 02.32.25 [skip ci] 2026-06-02 19:09:24 +00:00
gitea-actions[bot] ed4b06d330 chore(version): auto-bump 02.32.25 [skip ci] 2026-06-02 19:09:22 +00:00
Jonathan Miller 23dc30b5f9 Merge branches 'dev' and 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Successful in 11s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 14:09:10 -05:00
Jonathan Miller af841ace19 fix: dashboard plugin cards - correct protected/enabled/configure-only states
- Task plugins (demo reset, content sync) show Enabled/Disabled badge
  instead of Protected — DB protected flag is for uninstall protection,
  not dashboard display
- DevTools shows Enabled badge only (configure-only, no toggle) since
  features are controlled inside the plugin settings
- Health Monitor marked as protected (cannot be disabled)
- Added explicit metadata for task plugins instead of relying on
  guessPluginMeta

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:09:01 -05:00
gitea-actions[bot] 8ce3452125 chore: update development channel 02.32.24 [skip ci] 2026-06-02 19:05:59 +00:00
gitea-actions[bot] 12e9115a6a chore(version): auto-bump 02.32.24 [skip ci] 2026-06-02 19:05:56 +00:00
Jonathan Miller eeb4822b37 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 9s
Update Server / Update Server (push) Successful in 27s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 14:05:31 -05:00
Jonathan Miller 0632981d88 feat: .htaccess / NginX configuration maker (#123)
GUI-based server config generator with toggle switches:

Security: directory listing, sensitive files, PHP in uploads,
server signature, clickjacking, MIME sniffing, XSS, TRACE/TRACK,
referrer policy, HSTS, CSP, permissions policy

Performance: GZip compression, browser caching with configurable
expiry times, ETag control

SEO: www/non-www redirect, index.php redirect, trailing slash

Features:
- Live preview updates as toggles change (AJAX)
- Save to disk with automatic .htaccess.mokowaas.bak backup
- Download generated config
- NginX equivalent tab
- Current file viewer tab
- Options persist in component params
- Added to sidebar submenu

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 14:05:25 -05:00
gitea-actions[bot] a013755ce4 chore(version): auto-bump 02.32.23 [skip ci] 2026-06-02 18:57:17 +00:00
Jonathan Miller 8240e693fb Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Update Server / Update Server (push) Failing after 10s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 23s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 13:57:04 -05:00
Jonathan Miller 6a02a2b4e5 feat: add all quick action buttons to dashboard + Community Builder detection
8 action buttons in a 4-column grid:
- Clear Cache, Check Updates, Moko Extensions (primary)
- Global Check-in, View Logs, Scheduled Tasks, User Manager, Redirects (secondary)

User Manager auto-detects Community Builder — links to com_comprofiler
if installed, otherwise com_users.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 13:56:58 -05:00
gitea-actions[bot] 5d94419d9f chore: update development channel 02.32.22 [skip ci] 2026-06-02 18:54:54 +00:00
gitea-actions[bot] 3d3c918848 chore(version): auto-bump 02.32.22 [skip ci] 2026-06-02 18:54:53 +00:00
Jonathan Miller d0a3b5d6a4 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS into dev
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
Update Server / Update Server (push) Successful in 9s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 21s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
2026-06-02 13:54:40 -05:00
Jonathan Miller 4f2aea75f5 feat: add admin submenus under MokoWaaS sidebar menu
Submenus:
- Dashboard (control panel)
- Moko Extensions (install packages)
- Feature Plugins (filtered plugin manager)
- Joomla Updates (com_installer update view)
- Global Check-in (com_checkin)
- Cache Management (com_cache)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-02 13:54:30 -05:00
jmiller 7be52a964e chore: sync updates.xml 02.33.00 from main [skip ci] 2026-06-02 18:53:37 +00:00
236 changed files with 16706 additions and 10094 deletions
+3 -2
View File
@@ -107,7 +107,7 @@ replit.md
*.tar.gz *.tar.gz
*.tgz *.tgz
*.zip *.zip
!src/payload/*.zip !source/payload/*.zip
artifacts/ artifacts/
release/ release/
releases/ releases/
@@ -122,6 +122,7 @@ build/
dist/ dist/
out/ out/
site/ site/
!source/packages/*/site/
*.map *.map
*.css.map *.css.map
*.js.map *.js.map
@@ -160,7 +161,7 @@ package-lock.json
# PHP / Composer tooling # PHP / Composer tooling
# ============================================================ # ============================================================
vendor/ vendor/
!src/media/vendor/ !source/media/vendor/
composer.lock composer.lock
*.phar *.phar
codeception.phar codeception.phar
-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
+61 -295
View File
@@ -1,316 +1,82 @@
<!-- # MokoWaaS
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. Joomla 5/6 admin tools suite — heartbeat health monitoring, extension management, security firewall, tenant restrictions, and site administration.
SPDX-License-Identifier: GPL-3.0-or-later ## Quick Reference
# FILE INFORMATION | Field | Value |
DEFGROUP: MokoStandards.Templates.GitHub |---|---|
INGROUP: MokoStandards.Templates | **Package** | `pkg_mokowaas` |
REPO: https://github.com/mokoconsulting-tech/MokoStandards | **Language** | PHP 8.1+ |
PATH: /templates/github/copilot-instructions.joomla.md.template | **Branch** | develop on `dev`, merge to `main` (protected) |
VERSION: XX.YY.ZZ | **Wiki** | [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) |
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
Tokens replaced at sync time: MokoWaaS, https://github.com/mokoconsulting-tech/MokoWaaS, {{EXTENSION_NAME}},
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
-->
> [!IMPORTANT] ## Commands
> **🔧 AI Self-Update Required on First Use**
>
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
> this repository — or this is a brand-new repo.
>
> **On your very first interaction in this repository, before answering any other question:**
>
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
> 2. If they do, locate the values as described in the table below and replace every occurrence
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
> through the closing `> ---` line) in both files.
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
> values — all set."*
>
> | Placeholder | Where to find the value |
> |---|---|
> | `MokoWaaS` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
> | `https://github.com/mokoconsulting-tech/MokoWaaS` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
>
> ---
# MokoWaaS — GitHub Copilot Custom Instructions ```bash
composer install # Install PHP dependencies
## What This Repo Is
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
Repository URL: https://github.com/mokoconsulting-tech/MokoWaaS
Extension name: **{{EXTENSION_NAME}}**
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
Platform: **Joomla 4.x / MokoWaaS**
---
## Primary Language
**PHP** (≥ 7.4) is the primary language for this Joomla extension. JavaScript may be used for frontend enhancements. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
---
## File Header — Always Required on New Files
Every new file needs a copyright header as its first content.
**PHP:**
```php
<?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoWaaS.{{EXTENSION_TYPE}}
* INGROUP: MokoWaaS
* REPO: https://github.com/mokoconsulting-tech/MokoWaaS
* PATH: /path/to/file.php
* VERSION: XX.YY.ZZ
* BRIEF: One-line description of purpose
*/
defined('_JEXEC') or die;
``` ```
**Markdown:** ## Architecture
```markdown
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. Joomla **package** (`pkg_mokowaas`) with 17 sub-extensions:
SPDX-License-Identifier: GPL-3.0-or-later ### Core Plugin (`plg_system_mokowaas`)
- Heartbeat health endpoint (`/?mokowaas=health`) with 16 diagnostic checks
- Grafana provisioning and heartbeat sender
- Site alias / domain management
- Extension cascade (enable/disable coordination)
- Download key preservation across Joomla updates
- Namespace: `Moko\Plugin\System\MokoWaaS`
# FILE INFORMATION ### Feature Plugins
DEFGROUP: MokoWaaS.Documentation - `plg_system_mokowaas_firewall` — WAF, IP blocklist, security headers, password policy
INGROUP: MokoWaaS - `plg_system_mokowaas_tenant` — admin restrictions for non-master users
REPO: https://github.com/mokoconsulting-tech/MokoWaaS - `plg_system_mokowaas_devtools` — dev mode, hit reset, version cleanup, download key reset
PATH: /docs/file.md - `plg_system_mokowaas_offline` — offline mode bypass for legal pages
VERSION: XX.YY.ZZ - `plg_system_mokowaas_monitor` — Grafana heartbeat registration
BRIEF: One-line description
-->
```
**YAML / Shell / XML:** Use the appropriate comment syntax with the same fields. JSON files are exempt. ### Component (`com_mokowaas`)
- Admin dashboard with plugin management, WAF charts, extension catalog
- Helpdesk ticketing system
- REST API controllers
--- ### Modules
- `mod_mokowaas_cpanel` — admin dashboard widget
- `mod_mokowaas_menu` — admin sidebar menu
- `mod_mokowaas_cache` — status bar cache/temp cleaner
- `mod_mokowaas_categories` — auto-category tree menu
## Version Management ### Task Plugins
- `plg_task_mokowaasdemo` — scheduled demo site reset
- `plg_task_mokowaassync` — scheduled content sync
- `plg_task_mokowaas_tickets` — ticket automation
**`README.md` is the single source of truth for the repository version.** ### Update Server
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03``01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`. MokoGitea generates update feeds dynamically from releases — no static `updates.xml` needed.
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
### Joomla Version Alignment ## Source Directory
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically. Source lives in `source/` (not `src/`):
- `source/pkg_mokowaas.xml` — package manifest
- `source/script.php` — install script
- `source/packages/` — all sub-extensions
```xml ## Rules
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In updates.xml — prepend a new <update> block for every release. - **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
Note: the backslash in version="4\.[0-9]+" is a literal backslash character - **Attribution**: `Authored-by: Moko Consulting`
in the XML attribute value. Joomla's update server treats the value as a - **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
regular expression, so \. matches a literal dot. --> - **Minification**: handled at build time (CI)
<updates> - **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
<update> - **Standards**: [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
<name>{{EXTENSION_NAME}}</name>
<version>01.02.04</version>
<downloads>
<downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/MokoWaaS/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="4\.[0-9]+" />
</update>
<!-- … older entries preserved below … -->
</updates>
```
--- ## Coding Standards
## Joomla Extension Structure - PHP 8.1+ minimum
- Joomla 5/6 DI container pattern: `services/provider.php` → Extension class
``` - `SubscriberInterface` for event subscription
MokoWaaS/ - Joomla 5/6 dual-compat for events: check `is_object($event)` with `getArgument()` fallback
├── manifest.xml # Joomla installer manifest (root — required) - SPDX license headers on all PHP files
├── (no updates.xml) # Update XML is generated dynamically by MokoGitea - `defined('_JEXEC') or die;` on all web-accessible PHP files
├── site/ # Frontend (site) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ └── views/
├── admin/ # Backend (admin) code
│ ├── controller.php
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── sql/
├── language/ # Language INI files
├── media/ # CSS, JS, images (deployed to /media/{{EXTENSION_ELEMENT}}/)
├── docs/ # Technical documentation
├── tests/ # Test suite
├── .github/
│ ├── workflows/
│ ├── copilot-instructions.md # This file
│ └── CLAUDE.md
├── README.md # Version source of truth
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE # GPL-3.0-or-later
└── Makefile # Build automation
```
---
## Update Server — MokoGitea Dynamic Endpoint
`updates.xml` is **NOT** stored in the repo. MokoGitea generates the update XML dynamically from git releases at:
```
https://git.mokoconsulting.tech/{Owner}/{Repo}/updates.xml
```
The package manifest (`pkg_mokowaas.xml`) references it via:
```xml
<updateservers>
<server type="extension" priority="1" name="MokoWaaS Update Server">
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml
</server>
</updateservers>
```
**License Key (Download Key):**
- MokoGitea's endpoint validates license keys passed as `?dlid=MOKO-XXXX-XXXX-XXXX-XXXX`
- The generated XML includes `<downloadkey prefix="dlid=" suffix="" />` to tell Joomla a key is required
- Users enter the download key via Joomla's native **System → Update Sites** interface
- Joomla stores the key in `#__update_sites.extra_query` and appends it to all update/download requests
- Invalid/expired keys receive an empty `<updates></updates>` response
**Rules:**
- Do NOT create or commit a static `updates.xml` — MokoGitea generates it from releases
- The `<version>` in release tags must match `<version>` in the manifest and `README.md`
- Release assets (ZIPs) must be attached to git releases — MokoGitea uses them for `<downloadurl>`
- `<targetplatform name="joomla" version="(5|6)\..*">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression
---
## manifest.xml Rules
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
- Must include `<files folder="site">` and `<administration>` sections.
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
---
## GitHub Actions — Token Usage
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
```yaml
# ✅ Correct
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
```
```yaml
# ❌ Wrong — never use these in workflows
token: ${{ github.token }}
token: ${{ secrets.GITHUB_TOKEN }}
```
---
## MokoStandards Reference
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies:
| Document | Purpose |
|----------|---------|
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
---
## Naming Conventions
| Context | Convention | Example |
|---------|-----------|---------|
| PHP class | `PascalCase` | `MyController` |
| PHP method / function | `camelCase` | `getItems()` |
| PHP variable | `$snake_case` | `$item_id` |
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
| PHP class file | `PascalCase.php` | `ItemModel.php` |
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
---
## Commit Messages
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
---
## Branch Naming
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
---
## Keeping Documentation Current
| Change type | Documentation to update |
|-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Bump README.md version |
| New release | Create git release with ZIP asset; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
---
## Key Constraints
- Never commit directly to `main` — all changes go via PR, squash-merged
- Never skip the FILE INFORMATION block on a new file
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
- Never let `manifest.xml` version and `README.md` version go out of sync
- Never commit a static `updates.xml` — the update feed is generated dynamically by MokoGitea
+2 -2
View File
@@ -9,7 +9,7 @@
<display-name>Package - MokoWaaS</display-name> <display-name>Package - MokoWaaS</display-name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description> <description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
<version>02.33.00</version> <version>02.34.16</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
<governance> <governance>
@@ -21,6 +21,6 @@
<build> <build>
<language>PHP</language> <language>PHP</language>
<package-type>package</package-type> <package-type>package</package-type>
<entry-point>src/</entry-point> <entry-point>source/</entry-point>
</build> </build>
</moko-platform> </moko-platform>
+1 -4
View File
@@ -48,15 +48,12 @@ jobs:
if ! command -v composer &> /dev/null; then 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 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 fi
if [ -d "/opt/moko-platform/cli" ]; then rm -rf /tmp/moko-platform-api
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \ "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api /tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Bump version - name: Bump version
run: | run: |
+43 -12
View File
@@ -74,17 +74,17 @@ jobs:
if ! command -v composer &> /dev/null; then 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 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 fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api /tmp/moko-platform-api
cd /tmp/moko-platform-api cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Rename branch to rc - name: Rename branch to rc
run: | run: |
php /tmp/moko-platform-api/cli/branch_rename.php \ php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ --from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ --api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
@@ -100,7 +100,7 @@ jobs:
- name: Publish RC release - name: Publish RC release
run: | run: |
php /tmp/moko-platform-api/cli/release_publish.php \ php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \ --path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream --skip-update-stream
@@ -136,7 +136,7 @@ jobs:
run: | 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) 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 if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found aborting release" echo "::error::Merge conflict markers found - aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
@@ -151,26 +151,57 @@ jobs:
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: | run: |
# Ensure PHP + Composer are available
if ! command -v composer &> /dev/null; then 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 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 fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api /tmp/moko-platform-api
cd /tmp/moko-platform-api cd /tmp/moko-platform-api
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: "Publish stable release" - name: "Publish stable release"
run: | run: |
php /tmp/moko-platform-api/cli/release_publish.php \ php ${MOKO_CLI}/release_publish.php \
--path . --stability stable --bump minor --branch main \ --path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream --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) -------------------------------- # -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub" - name: "Step 9: Mirror release to GitHub"
if: >- if: >-
@@ -182,7 +213,7 @@ jobs:
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/release_mirror.php \ php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \ --version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \ --gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
@@ -213,7 +244,7 @@ jobs:
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}" TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral created by promote-rc) # Delete rc branch (ephemeral - created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \ "${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found" && echo "Deleted rc branch" || echo "rc branch not found"
@@ -256,7 +287,7 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php /tmp/moko-platform-api/cli/version_reset_dev.php \ php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true --branch dev --path . 2>&1 || true
@@ -270,7 +301,7 @@ jobs:
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released ${VERSION}" >> $GITHUB_STEP_SUMMARY echo "## Already Released - ${VERSION}" >> $GITHUB_STEP_SUMMARY
else else
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation # INGROUP: moko-platform.Automation
# VERSION: 02.33.00 # VERSION: 02.34.16
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+24
View File
@@ -295,6 +295,30 @@ jobs:
;; ;;
esac esac
- name: Check changelog has unreleased entries (PRs to main)
if: github.base_ref == 'main'
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::error::CHANGELOG.md not found — required for releases"
exit 1
fi
# Extract content between [Unreleased] and next ## heading
ENTRIES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found && /^- /{count++} END{print count+0}' CHANGELOG.md)
if [ "$ENTRIES" -eq 0 ]; then
echo "::error::CHANGELOG.md has no entries under [Unreleased]. Add changelog entries before releasing."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "No entries found under \`[Unreleased]\` in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
echo "Add entries describing what changed before merging to main." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${ENTRIES} unreleased entries found"
echo "## Changelog Check: Passed" >> $GITHUB_STEP_SUMMARY
echo "${ENTRIES} entries under [Unreleased]" >> $GITHUB_STEP_SUMMARY
- name: Validate Joomla language files - name: Validate Joomla language files
if: steps.platform.outputs.platform == 'joomla' if: steps.platform.outputs.platform == 'joomla'
run: | run: |
+50 -68
View File
@@ -63,16 +63,22 @@ jobs:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: | run: |
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
if [ -f “/opt/moko-platform/cli/version_bump.php” ] && [ -f “/opt/moko-platform/vendor/autoload.php” ]; then
echo “Using pre-installed /opt/moko-platform”
echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV”
else
echo “Falling back to fresh clone”
if ! command -v composer &> /dev/null; then 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 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 fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \ git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \ https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git \
/tmp/moko-platform-api /tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV" echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
fi
- name: Detect platform - name: Detect platform
id: platform id: platform
@@ -96,20 +102,23 @@ jobs:
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac esac
# Read current version (bump already handled by push workflow) # Bump version via CLI: patch for dev/alpha/beta, minor for RC
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null) case "$STABILITY" in
[ -z "$VERSION" ] && VERSION="00.00.01" release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
# Strip any existing suffix from version before applying stability 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\)$//') VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \ php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true --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 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 if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}" VERSION="${VERSION}${SUFFIX}"
fi fi
@@ -155,19 +164,39 @@ jobs:
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease --repo "${GITEA_REPO}" --branch dev --prerelease
- name: Ensure prerelease flag - name: Update release notes from CHANGELOG.md
run: | run: |
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Get release ID by tag and force prerelease=true
RELEASE_ID=$(curl -s "${API_BASE}/releases/tags/${TAG}" \ # Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" | jq -r '.id // empty') if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then if [ -n "$RELEASE_ID" ]; then
curl -s -X PATCH "${API_BASE}/releases/${RELEASE_ID}" \ python3 -c "
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \ import json, urllib.request
-H "Content-Type: application/json" \ body = open('/dev/stdin').read()
-d '{"prerelease": true}' payload = json.dumps({'body': body}).encode()
echo "Marked release ${TAG} (id=${RELEASE_ID}) as prerelease" 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 fi
- name: Build package and upload - name: Build package and upload
@@ -181,55 +210,8 @@ jobs:
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \ --token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true --repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml # updates.xml is generated dynamically by MokoGitea license server
if: steps.platform.outputs.platform == 'joomla' # No need to build, commit, or sync updates.xml from workflows
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
- name: "Delete lesser pre-release channels (cascade)" - name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true continue-on-error: true
-302
View File
@@ -1,302 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/update-server.yml
# VERSION: 09.23.00
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
#
# Thin wrapper around moko-platform CLI tools.
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
#
# Joomla filters update entries by the user's "Minimum Stability" setting.
name: "Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update Server
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: |
BRANCH="${{ github.ref_name }}"
# Configure git for bot pushes
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
else
STABILITY="development"
fi
# Gitea release tag per stability
case "$STABILITY" in
development) TAG="development" ;;
alpha) TAG="alpha" ;;
beta) TAG="beta" ;;
rc) TAG="release-candidate" ;;
*) TAG="stable" ;;
esac
# Bump patch, set platform suffix, fix consistency — version_bump preserves suffix
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
--branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Read final version (includes suffix, e.g. 01.02.15-dev)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
# Commit version bump if changed
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
# Build package and upload
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
python3 -c "
import base64, json, urllib.request, sys
with open('updates.xml', 'rb') as f:
content = base64.b64encode(f.read()).decode()
payload = json.dumps({
'content': content,
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main'
}).encode()
req = urllib.request.Request(
'${API_BASE}/contents/updates.xml',
data=payload, method='PUT',
headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# Permission check: admin or maintain role required
ACTOR="${{ github.actor }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${VERSION}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
+54 -8
View File
@@ -14,11 +14,58 @@
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./CHANGELOG.md PATH: ./CHANGELOG.md
VERSION: 02.33.00 VERSION: 02.34.16
BRIEF: Version history using `Keep a Changelog` BRIEF: Version history using `Keep a Changelog`
--> -->
# Changelog## [02.32.00] - 2026-06-02 # Changelog
## [Unreleased]
### Added
- Database Tools view — table status, optimize, repair, session purge (#127)
- Cache Cleanup view — directory size reporting and one-click cleanup (#128)
- mod_mokowaas_cache — one-click cache cleaner button in admin status bar (replaces Regular Labs Cache Cleaner)
- mod_mokowaas_menu — collapsible admin sidebar menu using native MetisMenu classes (like Community Builder)
- SSL certificate expiry monitoring in cpanel module (#148)
- MokoWaaS-specific update badge (blue) separate from other updates in cpanel module
- migrateUpdateServerUrls() — rewrites all Moko extension update server URLs to clean /updates.xml on install/update
- fixMenuIcons() — sets menu_icon params on submenu items (Joomla only renders img on level 1)
- setupCacheModule() — registers cache cleaner module in status bar position on install
- Component config.xml for Joomla Options modal (#149)
- preflight() ALTER for #__extensions.element default (MySQL strict mode fix)
- Retire MokoJoomTOS, MokoATS-Automation, MokoDPCalendarAPI, MokoGalleryCalendar on install
- MokoJoomTOS settings auto-migrate to mokowaas_offline before removal
- dev-release and pre-release workflows with changelog extraction into release notes
- RC pre-release consolidates dev patches into clean minor version bump
### Changed
- Move security hardening methods (protectPlugin, ensureProtectedFlag, isOurExtension) from core plugin to firewall plugin (#155)
- Admin menu module uses native Joomla MetisMenu CSS classes
- Helpdesk icon changed to fa-handshake-angle, .htaccess to fa-solid fa-file-code
- clearCache purges all cache files recursively (replaces Regular Labs Cache Cleaner behavior)
- License key warning moved from every-page onAfterRoute to package postflight only
- Update server URL changed to dynamic MokoGitea feed
- Component manifest adds `<languages>` for global language dir deployment
- Privacy and WAF Log added to component manifest submenu
- MokoOnyx template removed from package manifest (separate repo/release)
### Removed
- Static updates.xml — MokoGitea generates update feed dynamically from releases
- update-server.yml workflow — replaced by pre-release.yml
### Fixed
- Tickets list showing raw `<em>Unassigned</em>` HTML instead of italic text
- Cache cleaner CSRF failure — token now sent as POST FormData
- Admin menu icons missing for Helpdesk and .htaccess Maker
- Firewall install error "Field 'element' doesn't have a default value" (MySQL strict mode)
## [02.32] - 2026-06-02
### Added ### Added
- Admin control panel dashboard in com_mokowaas with site info bar, feature plugin grid, and quick actions - Admin control panel dashboard in com_mokowaas with site info bar, feature plugin grid, and quick actions
- Feature plugin architecture — MokoWaaS features split into toggleable plugins managed from the dashboard - Feature plugin architecture — MokoWaaS features split into toggleable plugins managed from the dashboard
@@ -42,7 +89,8 @@
- License key validation (licensing system not ready — will return in future release) - License key validation (licensing system not ready — will return in future release)
- Dynamic MokoGitea update feed dependency (replaced with static updates.xml) - Dynamic MokoGitea update feed dependency (replaced with static updates.xml)
## [02.31.00] - 2026-06-01 ## [02.31] - 2026-06-01
### Added ### Added
- License key support via Joomla's native Update Sites download key system (dlid) - License key support via Joomla's native Update Sites download key system (dlid)
- Update server URL migrated from static XML to MokoGitea's dynamic update feed endpoint - Update server URL migrated from static XML to MokoGitea's dynamic update feed endpoint
@@ -75,7 +123,8 @@
- Site Aliases config tab (hardcoded to dev.{primary_domain}) - Site Aliases config tab (hardcoded to dev.{primary_domain})
- File sync (images/, files/, media/) — sync is API/DB content only - File sync (images/, files/, media/) — sync is API/DB content only
## [02.29.03] - 2026-05-31 ## [02.29] - 2026-05-31
### Added ### Added
- `allow_extension_updates` param — separate update rights from installer restrictions; tenants can update extensions by default even when the installer is restricted - `allow_extension_updates` param — separate update rights from installer restrictions; tenants can update extensions by default even when the installer is restricted
- Hardcoded master usernames — multiple privileged users supported with identical access - Hardcoded master usernames — multiple privileged users supported with identical access
@@ -89,7 +138,6 @@
- Demo Mode with configurable warning banner on frontend when enabled - Demo Mode with configurable warning banner on frontend when enabled
### Fixed
- Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours - Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours
- `DemoResetService` — baseline snapshot and restore for DB tables + media files - `DemoResetService` — baseline snapshot and restore for DB tables + media files
- API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string) - API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string)
@@ -104,6 +152,4 @@
- Package installer: clean up legacy `mokowaasbrand` extension entries and files on install/update - Package installer: clean up legacy `mokowaasbrand` extension entries and files on install/update
- API endpoint `GET /?mokowaas=extensions` and `GET /api/v1/mokowaas/extensions` — list installed extensions with version, status, and update server info - API endpoint `GET /?mokowaas=extensions` and `GET /api/v1/mokowaas/extensions` — list installed extensions with version, status, and update server info
## [02.20.00] --- 2026-05-28 ## [02.20] --- 2026-05-28
## [02.20.00] --- 2026-05-28
-42
View File
@@ -1,42 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with this repository.
## Project Overview
**MokoWaaS** -- MokoWaaS is a Joomla 5.x / 6.x system plugin that provides a configurable white-label identity layer for the MokoWaaS platform.
| Field | Value |
|---|---|
| **Platform** | joomla |
| **Language** | PHP |
| **Default branch** | main |
| **License** | GPL-3.0-or-later |
| **Wiki** | [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) |
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
## Common Commands
```bash
composer install # Install PHP dependencies
```
## Architecture
This is a Joomla extension. Key directories:
- `src/` -- extension source (deployed to Joomla)
- `src/*.xml` -- manifest file (version, files, params)
- `src/src/` or `src/services/` -- PHP classes
- `src/language/` -- translation strings
- `src/media/` -- CSS/JS/images
## Rules
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
- **Attribution**: use `Authored-by: Moko Consulting` in commits
- **Branch strategy**: develop on `dev`, merge to `main` for release
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: ./CODE_OF_CONDUCT.md PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
--> -->
+24
View File
@@ -127,6 +127,30 @@ The version tools update all files containing version stamps:
Files synced from other repos (with a `# REPO:` header) are not touched. Files synced from other repos (with a `# REPO:` header) are not touched.
## Changelog
We use [Keep a Changelog](https://keepachangelog.com/) with an `[Unreleased]` staging section.
### Rules
- All changes go under `## [Unreleased]` — this is the "current work" section
- Entries stay under `[Unreleased]` until a **stable release** merges to `main`
- On stable release, `[Unreleased]` entries are promoted to a version heading (e.g., `## [02.34] - 2026-06-10`)
- Only **minor versions** get changelog headings — patch numbers from dev are never shown
- Dev/alpha/beta/RC pre-release descriptions pull from `[Unreleased]` automatically
- **CI will block PRs to main** if `[Unreleased]` has no entries
### Categories
Use these headings under each version:
- `### Added` — new features
- `### Changed` — changes to existing functionality
- `### Deprecated` — features that will be removed
- `### Removed` — features that were removed
- `### Fixed` — bug fixes
- `### Security` — vulnerability fixes
## Code Standards ## Code Standards
- **PHP**: PSR-12, tabs for indentation - **PHP**: PSR-12, tabs for indentation
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
INGROUP: MokoStandards.Governance INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /GOVERNANCE.md PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
--> -->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: ./LICENSE.md PATH: ./LICENSE.md
VERSION: 02.33.00 VERSION: 02.34.16
BRIEF: Project license (GPL-3.0-or-later) BRIEF: Project license (GPL-3.0-or-later)
--> -->
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /README.md PATH: /README.md
BRIEF: MokoWaaS platform plugin for Joomla BRIEF: MokoWaaS platform plugin for Joomla
--> -->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL] REPO: [REPOSITORY_URL]
PATH: /SECURITY.md PATH: /SECURITY.md
VERSION: 02.33.00 VERSION: 02.34.16
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
+4 -4
View File
@@ -11,13 +11,13 @@
INGROUP: MokoWaaS.Build INGROUP: MokoWaaS.Build
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
FILE: build-guide.md FILE: build-guide.md
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/ PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoWaaS system plugin BRIEF: Build and packaging guide for the MokoWaaS system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
--> -->
# MokoWaaS Build Guide (VERSION: 02.33.00) # MokoWaaS Build Guide (VERSION: 02.34.16)
## 1. Purpose ## 1. Purpose
@@ -44,7 +44,7 @@ The repository should maintain a clean, predictable, and modular structure suita
```text ```text
mokowaas/ mokowaas/
├── src/ ├── source/
│ ├── mokowaas.php (main plugin file) │ ├── mokowaas.php (main plugin file)
│ ├── mokowaas.xml (plugin manifest) │ ├── mokowaas.xml (plugin manifest)
│ ├── services/ (service providers for DI) │ ├── services/ (service providers for DI)
@@ -192,7 +192,7 @@ jobs:
- name: Lint PHP and syntax check - name: Lint PHP and syntax check
run: | run: |
echo "[INFO] Run php -l over src/ and any additional linting as needed." echo "[INFO] Run php -l over source/ and any additional linting as needed."
- name: Create build artifact - name: Create build artifact
run: | run: |
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/configuration-guide.md PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoWaaS system plugin BRIEF: Configuration guide for the MokoWaaS system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
--> -->
# MokoWaaS Configuration Guide (VERSION: 02.33.00) # MokoWaaS Configuration Guide (VERSION: 02.34.16)
## 1. Objective ## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/installation-guide.md PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoWaaS system plugin BRIEF: Installation guide for the MokoWaaS system plugin
NOTE: First document in the guide set NOTE: First document in the guide set
--> -->
# MokoWaaS Installation Guide (VERSION: 02.33.00) # MokoWaaS Installation Guide (VERSION: 02.34.16)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/operations-guide.md PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors NOTE: Defines lifecycle, responsibilities, and operational behaviors
--> -->
# MokoWaaS Operations Guide (VERSION: 02.33.00) # MokoWaaS Operations Guide (VERSION: 02.34.16)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/rollback-and-recovery-guide.md PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for WaaS plugin governance NOTE: Completes the core guide set for WaaS plugin governance
--> -->
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.33.00) # MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.16)
## Introduction ## Introduction
+8 -8
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/testing-guide.md PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoWaaS v02.01.08 BRIEF: Testing guide for MokoWaaS v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
--> -->
# MokoWaaS Testing Guide (VERSION: 02.33.00) # MokoWaaS Testing Guide (VERSION: 02.34.16)
## 1. Prerequisites ## 1. Prerequisites
@@ -27,7 +27,7 @@
1. Clean Joomla 5.x installation OR existing site with custom language overrides. 1. Clean Joomla 5.x installation OR existing site with custom language overrides.
2. Admin account with Super User access. 2. Admin account with Super User access.
3. Build the plugin package: `make package` or zip the `src/` directory. 3. Build the plugin package: `make package` or zip the `source/` directory.
## 2. Test Suites ## 2. Test Suites
@@ -278,19 +278,19 @@ Run from the project root:
```bash ```bash
# Lint all PHP files # Lint all PHP files
php -l src/script.php php -l source/script.php
php -l src/Extension/MokoWaaS.php php -l source/Extension/MokoWaaS.php
# Verify all override files have placeholders (no hardcoded "MokoWaaS" in values) # Verify all override files have placeholders (no hardcoded "MokoWaaS" in values)
grep -r '"MokoWaaS' src/language/overrides/ src/administrator/language/overrides/ grep -r '"MokoWaaS' source/language/overrides/ source/administrator/language/overrides/
# Expected: no output (all values should use {{BRAND_NAME}}) # Expected: no output (all values should use {{BRAND_NAME}})
# Verify sentinel constants match # Verify sentinel constants match
grep -c 'BLOCK_START\|BLOCK_END' src/script.php grep -c 'BLOCK_START\|BLOCK_END' source/script.php
# Expected: 6+ references # Expected: 6+ references
# Verify all .ini files have version 02.01.08 # Verify all .ini files have version 02.01.08
grep -r 'Version:' src/**/*.ini | grep -v '02.01.08' grep -r 'Version:' source/**/*.ini | grep -v '02.01.08'
# Expected: no output # Expected: no output
``` ```
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/troubleshooting-guide.md PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
NOTE: Designed for administrators and WaaS operations teams NOTE: Designed for administrators and WaaS operations teams
--> -->
# MokoWaaS Troubleshooting Guide (VERSION: 02.33.00) # MokoWaaS Troubleshooting Guide (VERSION: 02.34.16)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Guides INGROUP: MokoWaaS.Guides
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/guides/upgrade-and-versioning-guide.md PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
NOTE: Defines release flow, version rules, and upgrade validation NOTE: Defines release flow, version rules, and upgrade validation
--> -->
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.33.00) # MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.16)
## Introduction ## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS.Documentation INGROUP: MokoWaaS.Documentation
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.33.00 VERSION: 02.34.16
PATH: /docs/index.md PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoWaaS plugin BRIEF: Master index of all documentation for the MokoWaaS plugin
NOTE: Automatically maintained index for all guide canvases NOTE: Automatically maintained index for all guide canvases
--> -->
# MokoWaaS Documentation Index (VERSION: 02.33.00) # MokoWaaS Documentation Index (VERSION: 02.34.16)
## Introduction ## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoWaaS INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas REPO: https://github.com/mokoconsulting-tech/mokowaas
PATH: /docs/plugin-basic.md PATH: /docs/plugin-basic.md
VERSION: 02.33.00 VERSION: 02.34.16
BRIEF: Baseline documentation for the MokoWaaS system plugin BRIEF: Baseline documentation for the MokoWaaS system plugin
NOTE: Foundational reference for internal and external stakeholders NOTE: Foundational reference for internal and external stakeholders
--> -->
# MokoWaaS Plugin Overview (VERSION: 02.33.00) # MokoWaaS Plugin Overview (VERSION: 02.34.16)
## Introduction ## Introduction
+3 -3
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
INGROUP: MokoStandards.Templates INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoWaaS REPO: https://github.com/mokoconsulting-tech/MokoWaaS
PATH: /docs/update-server.md PATH: /docs/update-server.md
VERSION: 02.33.00 VERSION: 02.34.16
BRIEF: How this extension's Joomla update server file (update.xml) is managed BRIEF: How this extension's Joomla update server file (update.xml) is managed
--> -->
@@ -84,7 +84,7 @@ Since Joomla sites read `updates.xml` from the `main` branch, the `update-server
### Metadata Source ### Metadata Source
All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time: All metadata is extracted from the extension's XML manifest (`source/*.xml`) at build time:
| XML Element | Source | Notes | | XML Element | Source | Notes |
|-------------|--------|-------| |-------------|--------|-------|
@@ -136,7 +136,7 @@ The `repo_health.yml` workflow verifies on every commit:
- `<version>`, `<name>`, `<author>`, `<namespace>` tags present - `<version>`, `<name>`, `<author>`, `<namespace>` tags present
- Extension `type` attribute is valid - Extension `type` attribute is valid
- Language `.ini` files exist - Language `.ini` files exist
- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/` - `index.html` directory listing protection in `source/`, `source/admin/`, `source/site/`
--- ---
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<access component="com_mokowaas">
<section name="component">
<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
<action name="mokowaas.dashboard" title="COM_MOKOWAAS_ACL_DASHBOARD" description="COM_MOKOWAAS_ACL_DASHBOARD_DESC" />
<action name="mokowaas.extensions" title="COM_MOKOWAAS_ACL_EXTENSIONS" description="COM_MOKOWAAS_ACL_EXTENSIONS_DESC" />
<action name="mokowaas.htaccess" title="COM_MOKOWAAS_ACL_HTACCESS" description="COM_MOKOWAAS_ACL_HTACCESS_DESC" />
<action name="mokowaas.tickets" title="COM_MOKOWAAS_ACL_TICKETS" description="COM_MOKOWAAS_ACL_TICKETS_DESC" />
<action name="mokowaas.tickets.create" title="COM_MOKOWAAS_ACL_TICKETS_CREATE" description="COM_MOKOWAAS_ACL_TICKETS_CREATE_DESC" />
<action name="mokowaas.tickets.assign" title="COM_MOKOWAAS_ACL_TICKETS_ASSIGN" description="COM_MOKOWAAS_ACL_TICKETS_ASSIGN_DESC" />
<action name="mokowaas.plugins.toggle" title="COM_MOKOWAAS_ACL_PLUGINS_TOGGLE" description="COM_MOKOWAAS_ACL_PLUGINS_TOGGLE_DESC" />
<action name="mokowaas.cache" title="COM_MOKOWAAS_ACL_CACHE" description="COM_MOKOWAAS_ACL_CACHE_DESC" />
</section>
</access>
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Extension catalog for MokoWaaS Extension Manager.
Each entry points to the extension's own updates.xml — the installer
resolves the latest version and download URL at runtime.
To add an extension: copy an <extension> block and fill in the fields.
The updateserver URL must point to a valid Joomla updates.xml file.
-->
<catalog>
<extension>
<name>MokoWaaS</name>
<element>pkg_mokowaas</element>
<type>package</type>
<description>Admin dashboard, security firewall, tenant restrictions, health monitoring, and REST API.</description>
<icon>icon-shield-alt</icon>
<category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokowaas-platform</article>
<protected>true</protected>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoOnyx</name>
<element>mokoonyx</element>
<type>template</type>
<description>Modern Joomla site template with dark mode, custom layouts, and MokoWaaS integration.</description>
<icon>icon-paint-brush</icon>
<category>Templates</category>
<article>https://mokoconsulting.tech/support/products/mokoonyx-template</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomTOS</name>
<element>com_mokojoomtos</element>
<type>component</type>
<description>Terms of Service and privacy policy component with consent tracking.</description>
<icon>icon-file-contract</icon>
<category>Components</category>
<article>https://mokoconsulting.tech/support/products/mokojoomtos</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomHero</name>
<element>mod_mokojoomhero</element>
<type>module</type>
<description>Random hero image module from a configurable folder.</description>
<icon>icon-image</icon>
<category>Modules</category>
<article>https://mokoconsulting.tech/support/products/mokojoomhero</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoWaaS Announce</name>
<element>mod_mokowaas_announce</element>
<type>module</type>
<description>Centralized announcement system via admin module.</description>
<icon>icon-bullhorn</icon>
<category>Modules</category>
<article>https://mokoconsulting.tech/support/products/mokowaas-announce</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaSAnnounce/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>DPCalendar API</name>
<element>mokodpcalendarapi</element>
<type>plugin</type>
<description>Web Services plugin exposing DPCalendar events and calendars via REST API.</description>
<icon>icon-calendar</icon>
<category>Plugins</category>
<article>https://mokoconsulting.tech/support/products/mokodpcalendarapi</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoDPCalendarAPI/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>Gallery Calendar</name>
<element>mokogallerycalendar</element>
<type>plugin</type>
<description>JoomGallery and DPCalendar integration — link galleries to events.</description>
<icon>icon-images</icon>
<category>Plugins</category>
<article>https://mokoconsulting.tech/support/products/mokogallerycalendar</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoGalleryCalendar/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomOpenGraph</name>
<element>pkg_mokoog</element>
<type>package</type>
<description>Open Graph, Twitter Card, and social sharing meta tags for articles, categories, and pages.</description>
<icon>icon-share-alt</icon>
<category>Components</category>
<article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
</extension>
</catalog>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<fieldset name="notifications" label="Email Notifications" description="Configure email recipients for ticket and security notifications.">
<field name="admin_emails" type="text" default=""
label="Admin Email Addresses"
description="Comma-separated email addresses to receive all notifications."
hint="admin@example.com, support@example.com" />
<field name="admin_user_ids" type="text" default=""
label="Admin User IDs"
description="Comma-separated Joomla user IDs to receive notifications."
hint="320, 321" />
<field name="security_alerts" type="radio" default="1"
label="Security Alerts"
description="Send email alerts for WAF blocks and admin logins."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="helpdesk" label="Helpdesk Settings" description="Default helpdesk behavior.">
<field name="default_category" type="sql" default=""
label="Default Ticket Category"
description="Category assigned to tickets without a selection."
query="SELECT id AS value, title AS text FROM #__mokowaas_ticket_categories WHERE published = 1 ORDER BY ordering" />
<field name="autoclose_days" type="number" default="7"
label="Auto-Close After (days)"
description="Resolved tickets are auto-closed after this many days. 0 = disabled." />
<field name="kb_search_enabled" type="radio" default="1"
label="KB Search on Ticket Forms"
description="Show knowledge base search before ticket submission."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="permissions" label="COM_MOKOWAAS_ACL_TITLE"
description="COM_MOKOWAAS_ACL_DESC">
<field name="rules" type="rules"
label="COM_MOKOWAAS_ACL_TITLE"
validate="rules"
filter="rules"
component="com_mokowaas"
section="component" />
</fieldset>
</config>
@@ -0,0 +1,41 @@
; MokoWaaS Admin Dashboard - Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOWAAS_DASHBOARD_TITLE="MokoWaaS Control Panel"
COM_MOKOWAAS_SITE="Site"
COM_MOKOWAAS_DATABASE="Database"
COM_MOKOWAAS_DEBUG_ON="Debug ON"
COM_MOKOWAAS_OFFLINE="Offline"
COM_MOKOWAAS_CLEAR_CACHE="Clear Cache"
COM_MOKOWAAS_CHECK_UPDATES="Check Updates"
COM_MOKOWAAS_ENABLED="Enabled"
COM_MOKOWAAS_DISABLED="Disabled"
COM_MOKOWAAS_PROTECTED="Protected"
COM_MOKOWAAS_CONFIGURE="Configure"
COM_MOKOWAAS_TOGGLE_SUCCESS="Plugin state updated."
COM_MOKOWAAS_TOGGLE_FAIL="Failed to update plugin state."
COM_MOKOWAAS_CACHE_CLEARED="Cache cleared successfully."
COM_MOKOWAAS_EXTENSIONS_TITLE="Moko Extensions"
COM_MOKOWAAS_EXTENSIONS_INFO="Install Moko Consulting Joomla packages from the official release server. Updates are handled through Joomla's native System > Update mechanism — each package registers its own update server."
COM_MOKOWAAS_EXTENSIONS_LINK="Moko Extensions"
COM_MOKOWAAS_HTACCESS_TITLE=".htaccess Maker"
COM_MOKOWAAS_TICKETS_TITLE="Helpdesk"
; ACL
COM_MOKOWAAS_ACL_DASHBOARD="View Dashboard"
COM_MOKOWAAS_ACL_DASHBOARD_DESC="Allow viewing the MokoWaaS control panel dashboard."
COM_MOKOWAAS_ACL_EXTENSIONS="Manage Extensions"
COM_MOKOWAAS_ACL_EXTENSIONS_DESC="Allow installing and uninstalling Moko extensions."
COM_MOKOWAAS_ACL_HTACCESS="Manage .htaccess"
COM_MOKOWAAS_ACL_HTACCESS_DESC="Allow editing and saving the .htaccess configuration."
COM_MOKOWAAS_ACL_TICKETS="View Tickets"
COM_MOKOWAAS_ACL_TICKETS_DESC="Allow viewing helpdesk tickets."
COM_MOKOWAAS_ACL_TICKETS_CREATE="Create Tickets"
COM_MOKOWAAS_ACL_TICKETS_CREATE_DESC="Allow creating new helpdesk tickets."
COM_MOKOWAAS_ACL_TICKETS_ASSIGN="Assign Tickets"
COM_MOKOWAAS_ACL_TICKETS_ASSIGN_DESC="Allow assigning tickets to other users."
COM_MOKOWAAS_ACL_PLUGINS_TOGGLE="Toggle Plugins"
COM_MOKOWAAS_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoWaaS feature plugins."
COM_MOKOWAAS_ACL_CACHE="Clear Cache"
COM_MOKOWAAS_ACL_CACHE_DESC="Allow clearing the Joomla cache from the dashboard."
@@ -0,0 +1,19 @@
; MokoWaaS Admin Dashboard - System Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOWAAS="MokoWaaS"
COM_MOKOWAAS_DESCRIPTION="MokoWaaS admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management."
COM_MOKOWAAS_DASHBOARD_TITLE="MokoWaaS Control Panel"
COM_MOKOWAAS_MENU_DASHBOARD="Dashboard"
COM_MOKOWAAS_MENU_EXTENSIONS="Moko Extensions"
COM_MOKOWAAS_MENU_PLUGINS="Feature Plugins"
COM_MOKOWAAS_MENU_UPDATES="Joomla Updates"
COM_MOKOWAAS_MENU_CHECKIN="Global Check-in"
COM_MOKOWAAS_MENU_TICKETS="Helpdesk"
COM_MOKOWAAS_MENU_HTACCESS=".htaccess Maker"
COM_MOKOWAAS_MENU_PRIVACY="Privacy Guard"
COM_MOKOWAAS_MENU_WAFLOG="WAF Log"
COM_MOKOWAAS_MENU_DATABASE="Database Tools"
COM_MOKOWAAS_MENU_CLEANUP="Cache Cleanup"
COM_MOKOWAAS_MENU_CACHE="Cache Management"
@@ -0,0 +1,135 @@
--
-- MokoWaaS Helpdesk Tables
--
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_categories` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`alias` VARCHAR(255) NOT NULL DEFAULT '',
`description` TEXT,
`auto_assign_user` INT DEFAULT NULL,
`sla_response_minutes` INT UNSIGNED NOT NULL DEFAULT 480,
`sla_resolution_minutes` INT UNSIGNED NOT NULL DEFAULT 2880,
`ordering` INT NOT NULL DEFAULT 0,
`published` TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_tickets` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`subject` VARCHAR(512) NOT NULL,
`body` TEXT NOT NULL,
`status` ENUM('open','in_progress','waiting','resolved','closed') NOT NULL DEFAULT 'open',
`priority` ENUM('low','normal','high','urgent') NOT NULL DEFAULT 'normal',
`category_id` INT UNSIGNED DEFAULT NULL,
`created_by` INT NOT NULL DEFAULT 0,
`assigned_to` INT DEFAULT NULL,
`created` DATETIME NOT NULL,
`modified` DATETIME DEFAULT NULL,
`resolved` DATETIME DEFAULT NULL,
`closed` DATETIME DEFAULT NULL,
`sla_response_due` DATETIME DEFAULT NULL,
`sla_resolution_due` DATETIME DEFAULT NULL,
`sla_responded` TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_priority` (`priority`),
KEY `idx_assigned` (`assigned_to`),
KEY `idx_category` (`category_id`),
KEY `idx_created` (`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_replies` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`ticket_id` INT UNSIGNED NOT NULL,
`user_id` INT NOT NULL DEFAULT 0,
`body` TEXT NOT NULL,
`is_internal` TINYINT NOT NULL DEFAULT 0,
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_ticket` (`ticket_id`),
KEY `idx_created` (`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_canned` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`body` TEXT NOT NULL,
`category_id` INT UNSIGNED DEFAULT NULL,
`ordering` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_automation` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`trigger_event` VARCHAR(50) NOT NULL DEFAULT 'ticket_created',
`conditions` TEXT NOT NULL DEFAULT '[]',
`actions` TEXT NOT NULL DEFAULT '[]',
`enabled` TINYINT NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default automation rules
INSERT IGNORE INTO `#__mokowaas_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES
(1, 'Auto-close resolved tickets after 7 days', 'scheduled', '[{"field":"status","op":"eq","value":"resolved"},{"field":"age_hours","op":"gt","value":"168"}]', '[{"type":"set_status","value":"closed"},{"type":"add_note","value":"Auto-closed after 7 days with no response."}]', 1, 1),
(2, 'Escalate urgent tickets with no response in 1 hour', 'scheduled', '[{"field":"priority","op":"eq","value":"urgent"},{"field":"sla_responded","op":"eq","value":"0"},{"field":"age_hours","op":"gt","value":"1"}]', '[{"type":"add_note","value":"SLA BREACH: Urgent ticket has no staff response after 1 hour."}]', 1, 2),
(3, 'Notify on high priority ticket creation', 'ticket_created', '[{"field":"priority","op":"in","value":"high,urgent"}]', '[{"type":"add_note","value":"High/urgent ticket created — requires immediate attention."}]', 1, 3);
-- Default categories
INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `description`, `sla_response_minutes`, `sla_resolution_minutes`, `ordering`) VALUES
(1, 'General Support', 'general-support', 'General questions and assistance', 480, 2880, 1),
(2, 'Bug Report', 'bug-report', 'Report a software bug or issue', 240, 1440, 2),
(3, 'Feature Request', 'feature-request', 'Request a new feature or enhancement', 1440, 10080, 3),
(4, 'Billing', 'billing', 'Billing, invoicing, and payment questions', 240, 1440, 4),
(5, 'Urgent / Outage', 'urgent-outage', 'Site down or critical issue', 60, 240, 5);
--
-- Privacy Guard Tables
--
CREATE TABLE IF NOT EXISTS `#__mokowaas_consent_log` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`category` VARCHAR(50) NOT NULL,
`action` ENUM('granted','revoked') NOT NULL,
`ip_address` VARCHAR(45) NOT NULL DEFAULT '',
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_data_requests` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT NOT NULL,
`type` ENUM('export','delete','anonymize') NOT NULL,
`status` ENUM('pending','processing','completed','denied') NOT NULL DEFAULT 'pending',
`notes` TEXT,
`processed_by` INT DEFAULT NULL,
`created` DATETIME NOT NULL,
`processed` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `#__mokowaas_retention_policies` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`content_type` VARCHAR(100) NOT NULL,
`retention_days` INT UNSIGNED NOT NULL DEFAULT 365,
`action` ENUM('anonymize','delete','archive') NOT NULL DEFAULT 'anonymize',
`enabled` TINYINT NOT NULL DEFAULT 1,
`description` VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default retention policies
INSERT IGNORE INTO `#__mokowaas_retention_policies` (`id`, `content_type`, `retention_days`, `action`, `enabled`, `description`) VALUES
(1, 'action_logs', 90, 'delete', 1, 'Delete action log entries older than 90 days'),
(2, 'waf_logs', 30, 'delete', 1, 'Delete WAF block logs older than 30 days'),
(3, 'sessions', 7, 'delete', 1, 'Purge expired sessions older than 7 days'),
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
@@ -0,0 +1,719 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
class DisplayController extends BaseController
{
protected $default_view = 'dashboard';
/**
* ACL map: view name => required permission.
*/
private const VIEW_ACL = [
'dashboard' => 'mokowaas.dashboard',
'extensions' => 'mokowaas.extensions',
'htaccess' => 'mokowaas.htaccess',
'tickets' => 'mokowaas.tickets',
'ticket' => 'mokowaas.tickets',
'privacy' => 'core.admin',
'waflog' => 'core.admin',
'categories' => 'mokowaas.tickets',
'canned' => 'mokowaas.tickets',
'automation' => 'core.admin',
'database' => 'core.admin',
'cleanup' => 'mokowaas.cache',
];
public function display($cachable = false, $urlparams = [])
{
$view = $this->input->get('view', $this->default_view);
$acl = self::VIEW_ACL[$view] ?? 'core.manage';
if (!$this->checkAcl($acl))
{
Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
Factory::getApplication()->redirect(Route::_('index.php', false));
return;
}
return parent::display($cachable, $urlparams);
}
// ==================================================================
// Plugin toggle
// ==================================================================
public function togglePlugin()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.plugins.toggle'))
{
$this->jsonForbidden();
return;
}
$app = Factory::getApplication();
$model = $this->getModel('Dashboard');
$result = $model->togglePlugin(
$app->getInput()->getInt('extension_id', 0),
$app->getInput()->getInt('enabled', 0)
);
$this->jsonResponse($result);
}
// ==================================================================
// Cache
// ==================================================================
public function clearCache()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.cache'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Dashboard')->clearCache());
}
public function clearTemp()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.cache'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Dashboard')->clearTemp());
}
// ==================================================================
// Extensions
// ==================================================================
public function installExtension()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.extensions'))
{
$this->jsonForbidden();
return;
}
$downloadUrl = Factory::getApplication()->getInput()->getString('download_url', '');
if (empty($downloadUrl))
{
$this->jsonResponse(['success' => false, 'message' => 'Missing download URL.']);
return;
}
$this->jsonResponse($this->getModel('Extensions')->installFromUrl($downloadUrl));
}
// ==================================================================
// .htaccess
// ==================================================================
public function saveHtaccess()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.htaccess'))
{
$this->jsonForbidden();
return;
}
$app = Factory::getApplication();
$input = $app->getInput();
$model = $this->getModel('Htaccess');
$options = [];
foreach ($input->getArray() as $key => $value)
{
if (str_starts_with($key, 'opt_'))
{
$options[substr($key, 4)] = $value;
}
}
if (!empty($options))
{
$model->saveOptions($options);
}
$this->jsonResponse($model->saveHtaccess($input->getRaw('content', '')));
}
public function generateHtaccess()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.htaccess'))
{
$this->jsonForbidden();
return;
}
$model = $this->getModel('Htaccess');
$options = Factory::getApplication()->getInput()->getArray();
$model->saveOptions($options);
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json');
echo json_encode([
'htaccess' => $model->generateHtaccess($options),
'nginx' => $model->generateNginx($options),
]);
$app->close();
}
// ==================================================================
// Tickets
// ==================================================================
public function createTicket()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets.create'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->createTicket([
'subject' => $input->getString('subject', ''),
'body' => $input->getRaw('body', ''),
'priority' => $input->getString('priority', 'normal'),
'category_id' => $input->getInt('category_id', 0),
]));
}
public function addTicketReply()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->addReply(
$input->getInt('ticket_id', 0),
$input->getRaw('body', ''),
(bool) $input->getInt('is_internal', 0)
));
}
public function updateTicketStatus()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->updateStatus(
$input->getInt('ticket_id', 0),
$input->getString('status', '')
));
}
// ==================================================================
// KB Search
// ==================================================================
public function searchKb()
{
$query = Factory::getApplication()->getInput()->getString('q', '');
if (strlen($query) < 3)
{
$this->jsonResponse(['results' => []]);
}
try
{
$db = Factory::getDbo();
$escaped = $db->quote('%' . $db->escape($query, true) . '%');
$results = $db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('l.title'), $db->quoteName('l.url'), $db->quoteName('l.description')])
->from($db->quoteName('#__finder_links', 'l'))
->where($db->quoteName('l.published') . ' = 1')
->where('(' . $db->quoteName('l.title') . ' LIKE ' . $escaped
. ' OR ' . $db->quoteName('l.description') . ' LIKE ' . $escaped . ')')
->order($db->quoteName('l.title') . ' ASC')
->setLimit(8)
)->loadObjectList() ?: [];
foreach ($results as $r)
{
$r->description = mb_substr(strip_tags($r->description ?? ''), 0, 150);
}
$this->jsonResponse(['results' => $results]);
}
catch (\Throwable $e)
{
$this->jsonResponse(['results' => []]);
}
}
// ==================================================================
// Maintenance (#127, #128)
// ==================================================================
public function optimizeDb()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->optimizeTables());
}
public function repairDb()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->repairTables());
}
public function purgeSessions()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->purgeSessions());
}
public function cleanDirectory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.cache')) { $this->jsonForbidden(); return; }
$dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->jsonResponse($model->cleanDirectory($dirKey));
}
// ==================================================================
// Helpdesk CRUD (#137, #138, #139)
// ==================================================================
public function saveCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$id = $input->getInt('id', 0);
$data = (object) [
'title' => $input->getString('title', ''),
'alias' => \Joomla\CMS\Filter\OutputFilter::stringURLSafe($input->getString('title', '')),
'sla_response_minutes' => $input->getInt('sla_response_minutes', 480),
'sla_resolution_minutes' => $input->getInt('sla_resolution_minutes', 2880),
'auto_assign_user' => $input->getInt('auto_assign_user', 0) ?: null,
'published' => $input->getInt('published', 1),
];
if ($id) {
$data->id = $id;
$db->updateObject('#__mokowaas_ticket_categories', $data, 'id');
} else {
$data->ordering = 0;
$db->insertObject('#__mokowaas_ticket_categories', $data, 'id');
}
$this->jsonResponse(['success' => true, 'message' => 'Category saved.', 'id' => (int) $data->id]);
}
public function deleteCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
}
public function saveCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$data = (object) [
'title' => $input->getString('title', ''),
'body' => $input->getRaw('body', ''),
'category_id' => $input->getInt('category_id', 0) ?: null,
'ordering' => 0,
];
$id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_canned', $data, 'id'); }
else { $db->insertObject('#__mokowaas_ticket_canned', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Canned response saved.', 'id' => (int) $data->id]);
}
public function deleteCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
}
public function saveAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$data = (object) [
'title' => $input->getString('title', ''),
'trigger_event' => $input->getString('trigger_event', 'ticket_created'),
'conditions' => $input->getRaw('conditions', '[]'),
'actions' => $input->getRaw('actions', '[]'),
'enabled' => 1,
'ordering' => 0,
];
$id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_automation', $data, 'id'); }
else { $db->insertObject('#__mokowaas_ticket_automation', $data, 'id'); }
$this->jsonResponse(['success' => true, 'message' => 'Rule saved.', 'id' => (int) $data->id]);
}
public function deleteAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']);
}
public function toggleAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->update('#__mokowaas_ticket_automation')
->set('enabled = ' . $input->getInt('enabled', 0))
->where('id = ' . $input->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule updated.']);
}
// ==================================================================
// Settings Import/Export (#132)
// ==================================================================
public function exportSettings()
{
Session::checkToken('get') or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$db = Factory::getDbo();
$settings = [];
// Export all MokoWaaS plugin params
$plugins = ['mokowaas', 'mokowaas_firewall', 'mokowaas_tenant', 'mokowaas_devtools', 'mokowaas_offline'];
foreach ($plugins as $element)
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
);
$settings['plugins'][$element] = json_decode($db->loadResult() ?? '{}', true);
}
// Export component params
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$settings['component'] = json_decode($db->loadResult() ?? '{}', true);
$settings['exported'] = gmdate('Y-m-d\TH:i:s\Z');
$settings['site'] = Factory::getConfig()->get('sitename', '');
$this->jsonResponse(['success' => true, 'settings' => $settings]);
}
public function importSettings()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$json = Factory::getApplication()->getInput()->getRaw('settings_json', '');
$data = json_decode($json, true);
if (empty($data) || empty($data['plugins']))
{
$this->jsonResponse(['success' => false, 'message' => 'Invalid settings JSON.']);
return;
}
$db = Factory::getDbo();
$count = 0;
foreach ($data['plugins'] ?? [] as $element => $params)
{
if (!is_array($params))
{
continue;
}
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute();
$count++;
}
if (!empty($data['component']) && is_array($data['component']))
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component'])))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute();
$count++;
}
$this->jsonResponse(['success' => true, 'message' => "Imported settings for {$count} extensions."]);
}
// ==================================================================
// WAF Log
// ==================================================================
public function purgeWafLog()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$days = Factory::getApplication()->getInput()->getInt('days', 30);
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
$this->jsonResponse($model->purgeLogs($days));
}
public function banIpFromLog()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$ip = Factory::getApplication()->getInput()->getString('ip', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
$this->jsonResponse($model->banIp($ip));
}
// ==================================================================
// Privacy Guard
// ==================================================================
public function processDataRequest()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$action = $input->getString('action', 'deny');
if ($action === 'create')
{
$result = $model->createRequest(
$input->getInt('user_id', 0),
$input->getString('type', 'export')
);
$this->jsonResponse($result);
return;
}
if ($action === 'approve' && !$input->getInt('request_id', 0) && $input->getInt('user_id', 0))
{
// Auto-process: create then immediately approve
$result = $model->createRequest(
$input->getInt('user_id', 0),
$input->getString('type', 'export')
);
if ($result['success'] && !empty($result['id']))
{
$result = $model->processRequest((int) $result['id'], 'approve');
}
$this->jsonResponse($result);
return;
}
$this->jsonResponse($model->processRequest(
$input->getInt('request_id', 0),
$action
));
}
public function exportUserData()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$this->jsonResponse($model->exportUserData(
Factory::getApplication()->getInput()->getInt('user_id', 0)
));
}
// ==================================================================
// Importers
// ==================================================================
public function importAts()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokowaas.tickets'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Import')->importAts());
}
public function importAdminTools()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Import')->importAdminTools());
}
// ==================================================================
// Helpers
// ==================================================================
/**
* Check a MokoWaaS ACL permission for the current user.
*/
private function checkAcl(string $action): bool
{
$user = Factory::getApplication()->getIdentity();
// Super admins always pass
if ($user->authorise('core.admin', 'com_mokowaas'))
{
return true;
}
return $user->authorise($action, 'com_mokowaas');
}
/**
* Send a JSON response and close.
*/
private function jsonResponse(array $data): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json');
echo json_encode($data);
$app->close();
}
/**
* Send a 403 JSON response and close.
*/
private function jsonForbidden(): void
{
$this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
return;
}
}
@@ -24,9 +24,10 @@ class DashboardModel extends BaseDatabaseModel
'mokowaas' => [ 'mokowaas' => [
'icon' => 'icon-shield-alt', 'icon' => 'icon-shield-alt',
'category' => 'core', 'category' => 'core',
'label' => 'Core — Branding & Identity', 'label' => 'Core',
'description' => 'White-label branding, master user enforcement, emergency access, and plugin protection.', 'description' => 'Heartbeat, health monitoring, site aliases, extension coordination, and download key preservation.',
'protected' => true, 'protected' => true,
'configure_only' => false,
], ],
'mokowaas_firewall' => [ 'mokowaas_firewall' => [
'icon' => 'icon-lock', 'icon' => 'icon-lock',
@@ -34,6 +35,7 @@ class DashboardModel extends BaseDatabaseModel
'label' => 'Firewall', 'label' => 'Firewall',
'description' => 'Web Application Firewall — SQLi, XSS, RFI, DFI shields, IP blocklist, admin secret URL, file protection.', 'description' => 'Web Application Firewall — SQLi, XSS, RFI, DFI shields, IP blocklist, admin secret URL, file protection.',
'protected' => false, 'protected' => false,
'configure_only' => false,
], ],
'mokowaas_tenant' => [ 'mokowaas_tenant' => [
'icon' => 'icon-users', 'icon' => 'icon-users',
@@ -41,20 +43,39 @@ class DashboardModel extends BaseDatabaseModel
'label' => 'Tenant Restrictions', 'label' => 'Tenant Restrictions',
'description' => 'Installer, sysinfo, config, and template access restrictions for non-master users.', 'description' => 'Installer, sysinfo, config, and template access restrictions for non-master users.',
'protected' => false, 'protected' => false,
'configure_only' => false,
],
'mokowaas_offline' => [
'icon' => 'icon-globe',
'category' => 'security',
'label' => 'Offline Bypass',
'description' => 'Keep selected pages (TOS, Privacy Policy) accessible during offline mode.',
'protected' => false,
'configure_only' => true,
], ],
'mokowaas_devtools' => [ 'mokowaas_devtools' => [
'icon' => 'icon-wrench', 'icon' => 'icon-wrench',
'category' => 'tools', 'category' => 'tools',
'label' => 'Developer Tools', 'label' => 'Developer Tools',
'description' => 'Dev mode, hit counter reset, content version cleanup.', 'description' => 'Dev mode, hit counter reset, content version cleanup. Features are controlled inside the plugin settings.',
'protected' => false, 'protected' => false,
'configure_only' => true,
], ],
'mokowaas_monitor' => [ 'mokowaasdemo' => [
'icon' => 'icon-heartbeat', 'icon' => 'icon-undo',
'category' => 'monitoring', 'category' => 'content',
'label' => 'Health Monitor', 'label' => 'Demo Reset Task',
'description' => 'Site health checks, Grafana heartbeat integration, and diagnostics.', 'description' => 'Scheduled demo site reset with content snapshots.',
'protected' => false, 'protected' => false,
'configure_only' => true,
],
'mokowaassync' => [
'icon' => 'icon-sync',
'category' => 'content',
'label' => 'Content Sync Task',
'description' => 'Scheduled content synchronisation to remote MokoWaaS sites.',
'protected' => false,
'configure_only' => true,
], ],
]; ];
@@ -97,7 +118,8 @@ class DashboardModel extends BaseDatabaseModel
'(' . $db->quoteName('type') . ' = ' . $db->quote('plugin') '(' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system')
. ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\_%') . '))' . ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\_%') . ')'
. ' AND ' . $db->quoteName('element') . ' != ' . $db->quote('mokowaas_monitor') . ')'
// Webservices plugins // Webservices plugins
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin') . ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices') . ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices')
@@ -120,8 +142,10 @@ class DashboardModel extends BaseDatabaseModel
$manifest = json_decode($row->manifest_cache ?? '{}'); $manifest = json_decode($row->manifest_cache ?? '{}');
$version = $manifest->version ?? ''; $version = $manifest->version ?? '';
// Build a lookup key: system plugins use element, others use folder_element // Only system plugins and task plugins match PLUGIN_META by element
$metaKey = $row->element; $metaKey = ($row->folder === 'system' || $row->folder === 'task')
? $row->element
: $row->folder . '_' . $row->element;
$meta = self::PLUGIN_META[$metaKey] ?? null; $meta = self::PLUGIN_META[$metaKey] ?? null;
@@ -141,7 +165,8 @@ class DashboardModel extends BaseDatabaseModel
'folder' => $row->folder, 'folder' => $row->folder,
'type' => $row->type, 'type' => $row->type,
'enabled' => (int) $row->enabled, 'enabled' => (int) $row->enabled,
'protected' => (int) $row->protected || ($meta['protected'] ?? false), 'protected' => (bool) ($meta['protected'] ?? false),
'configure_only' => (bool) ($meta['configure_only'] ?? false),
'version' => $version, 'version' => $version,
'icon' => $meta['icon'] ?? 'icon-puzzle-piece', 'icon' => $meta['icon'] ?? 'icon-puzzle-piece',
'category' => $categoryKey, 'category' => $categoryKey,
@@ -187,6 +212,54 @@ class DashboardModel extends BaseDatabaseModel
]; ];
} }
/**
* Get installed MokoWaaS component and modules with versions.
*
* @return array Array of extension objects with name, element, type, version.
*/
public function getMokoExtensions(): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
$db->quoteName('element'),
$db->quoteName('name'),
$db->quoteName('type'),
$db->quoteName('enabled'),
$db->quoteName('manifest_cache'),
])
->from($db->quoteName('#__extensions'))
->where('('
// The component
. '(' . $db->quoteName('type') . ' = ' . $db->quote('component')
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokowaas') . ')'
// Admin modules
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module')
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokowaas%') . ')'
. ')')
->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC');
$db->setQuery($query);
$rows = $db->loadObjectList() ?: [];
$extensions = [];
foreach ($rows as $row)
{
$manifest = json_decode($row->manifest_cache ?? '{}');
$extensions[] = (object) [
'element' => $row->element,
'name' => $manifest->name ?? $row->name,
'type' => $row->type,
'version' => $manifest->version ?? '',
'enabled' => (int) $row->enabled,
];
}
return $extensions;
}
/** /**
* Toggle a plugin's enabled state. * Toggle a plugin's enabled state.
* *
@@ -242,13 +315,18 @@ class DashboardModel extends BaseDatabaseModel
{ {
try try
{ {
$app = Factory::getApplication(); // Use Joomla's native cache API — same as com_cache
$app->get('cache_handler', 'file');
// Clear site and admin caches
$cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class); $cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class);
Factory::getCache('', '')->gc(); $cache->createCacheController('', ['defaultgroup' => ''])->cache->clean('');
Factory::getCache('', '', 'administrator')->gc();
// Also clean admin cache
$conf = Factory::getApplication()->get('cache_handler', 'file');
$options = [
'defaultgroup' => '',
'cachebase' => JPATH_ADMINISTRATOR . '/cache',
'storage' => $conf,
];
$cache->createCacheController('', $options)->cache->clean('');
// Clear opcache if available // Clear opcache if available
if (\function_exists('opcache_reset')) if (\function_exists('opcache_reset'))
@@ -256,7 +334,7 @@ class DashboardModel extends BaseDatabaseModel
\opcache_reset(); \opcache_reset();
} }
return ['success' => true, 'message' => 'Cache cleared successfully.']; return ['success' => true, 'message' => 'All cache cleared successfully.'];
} }
catch (\Throwable $e) catch (\Throwable $e)
{ {
@@ -264,6 +342,62 @@ class DashboardModel extends BaseDatabaseModel
} }
} }
/**
* Clear the Joomla tmp directory.
*
* Removes all files and subdirectories from the configured tmp_path,
* preserving the directory itself and any .htaccess / web.config files.
*
* @return array Result with success and message keys.
*/
public function clearTemp(): array
{
try
{
$tmpPath = Factory::getApplication()->get('tmp_path', JPATH_ROOT . '/tmp');
if (!is_dir($tmpPath))
{
return ['success' => false, 'message' => 'Temp directory does not exist: ' . $tmpPath];
}
$count = 0;
$protected = ['.htaccess', 'web.config', 'index.html', '.gitkeep'];
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($tmpPath, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item)
{
$basename = $item->getBasename();
// Skip protected files in the root tmp directory
if ($item->getPath() === $tmpPath && \in_array($basename, $protected, true))
{
continue;
}
if ($item->isDir())
{
@rmdir($item->getPathname());
}
else
{
@unlink($item->getPathname());
$count++;
}
}
return ['success' => true, 'message' => sprintf('Temp directory cleaned (%d files removed).', $count)];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Temp clear failed: ' . $e->getMessage()];
}
}
/** /**
* Auto-generate dashboard metadata for plugins not in the static map. * Auto-generate dashboard metadata for plugins not in the static map.
*/ */
@@ -422,4 +556,84 @@ class DashboardModel extends BaseDatabaseModel
return []; return [];
} }
} }
/**
* WAF blocks per day for the last 14 days.
*/
public function getWafBlocksByDay(int $days = 14): array
{
try
{
$db = $this->getDatabase();
$db->setQuery(
"SELECT DATE(" . $db->quoteName('created') . ") AS day, COUNT(*) AS total"
. " FROM " . $db->quoteName('#__mokowaas_waf_log')
. " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
. " GROUP BY day ORDER BY day"
);
$rows = $db->loadObjectList() ?: [];
// Fill in missing days with zero
$result = [];
$date = new \DateTime("-{$days} days");
$now = new \DateTime('now');
$map = [];
foreach ($rows as $r)
{
$map[$r->day] = (int) $r->total;
}
while ($date <= $now)
{
$key = $date->format('Y-m-d');
$result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0];
$date->modify('+1 day');
}
return $result;
}
catch (\Throwable $e)
{
return [];
}
}
/**
* Admin logins per day for the last 14 days.
*/
public function getLoginsByDay(int $days = 14): array
{
try
{
$db = $this->getDatabase();
$db->setQuery(
"SELECT DATE(" . $db->quoteName('log_date') . ") AS day, COUNT(*) AS total"
. " FROM " . $db->quoteName('#__action_logs')
. " WHERE " . $db->quoteName('message_language_key') . " = 'PLG_ACTIONLOG_JOOMLA_USER_LOGGED_IN'"
. " AND " . $db->quoteName('log_date') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
. " GROUP BY day ORDER BY day"
);
$rows = $db->loadObjectList() ?: [];
$result = [];
$date = new \DateTime("-{$days} days");
$now = new \DateTime('now');
$map = [];
foreach ($rows as $r)
{
$map[$r->day] = (int) $r->total;
}
while ($date <= $now)
{
$key = $date->format('Y-m-d');
$result[] = (object) ['day' => $date->format('M d'), 'total' => $map[$key] ?? 0];
$date->modify('+1 day');
}
return $result;
}
catch (\Throwable $e)
{
return [];
}
}
} }
@@ -0,0 +1,321 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
/**
* Extension catalog model — reads catalog.xml, fetches each extension's
* updates.xml to resolve latest version and download URL, and checks
* local install status.
*
* @since 02.32.00
*/
class ExtensionsModel extends BaseDatabaseModel
{
/**
* Parsed catalog entries (cached per request).
*
* @var array|null
*/
private ?array $catalogCache = null;
/**
* Get the full catalog with install status and release info.
*
* @return array Array of catalog entry objects
*/
public function getCatalog(): array
{
$catalog = $this->loadCatalog();
$installed = $this->getInstalledVersions($catalog);
$packages = [];
foreach ($catalog as $entry)
{
$release = $this->fetchFromUpdateServer($entry['updateserver'] ?? '');
$localVersion = $installed[$entry['element']] ?? null;
$remoteVersion = $release['version'] ?? '';
$downloadUrl = $release['download_url'] ?? '';
$status = 'not_installed';
if ($localVersion !== null)
{
$status = 'installed';
if ($remoteVersion !== '' && version_compare($remoteVersion, $localVersion, '>'))
{
$status = 'update_available';
}
}
$extensionId = $this->getExtensionId($entry['element']);
$packages[] = (object) [
'label' => $entry['name'],
'description' => $entry['description'],
'element' => $entry['element'],
'type' => $entry['type'],
'icon' => $entry['icon'],
'category' => $entry['category'],
'local_version' => $localVersion ?? '',
'remote_version' => $remoteVersion,
'download_url' => $downloadUrl,
'status' => $status,
'article_url' => $entry['article'] ?? '',
'protected' => ($entry['protected'] ?? 'false') === 'true',
'extension_id' => $extensionId,
];
}
return $packages;
}
/**
* Install an extension from a remote ZIP URL.
*
* @param string $url The download URL
*
* @return array Result with success, message, and extension info
*/
public function installFromUrl(string $url): array
{
$tmpPath = Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp');
$tmpFile = $tmpPath . '/mokowaas_install_' . md5($url) . '.zip';
try
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$data = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error || $code !== 200 || empty($data))
{
return ['success' => false, 'message' => 'Download failed: ' . ($error ?: "HTTP {$code}")];
}
file_put_contents($tmpFile, $data);
$installer = new \Joomla\CMS\Installer\Installer();
$result = $installer->install($tmpFile);
@unlink($tmpFile);
if (!$result)
{
return ['success' => false, 'message' => 'Installation failed.'];
}
return [
'success' => true,
'message' => 'Installed successfully.',
];
}
catch (\Throwable $e)
{
@unlink($tmpFile);
return ['success' => false, 'message' => 'Error: ' . $e->getMessage()];
}
}
/**
* Load and parse the catalog.xml file.
*
* @return array Array of associative arrays, one per extension
*/
private function loadCatalog(): array
{
if ($this->catalogCache !== null)
{
return $this->catalogCache;
}
$catalogFile = JPATH_ADMINISTRATOR . '/components/com_mokowaas/catalog.xml';
if (!file_exists($catalogFile))
{
$this->catalogCache = [];
return [];
}
$xml = @simplexml_load_file($catalogFile);
if (!$xml)
{
$this->catalogCache = [];
return [];
}
$entries = [];
foreach ($xml->extension as $ext)
{
$entries[] = [
'name' => (string) $ext->name,
'element' => (string) $ext->element,
'type' => (string) $ext->type,
'description' => (string) $ext->description,
'icon' => (string) $ext->icon,
'category' => (string) $ext->category,
'article' => (string) $ext->article,
'protected' => (string) $ext->protected,
'updateserver' => (string) $ext->updateserver,
];
}
$this->catalogCache = $entries;
return $entries;
}
/**
* Fetch the latest version and download URL from an extension's updates.xml.
*
* Parses the standard Joomla update server XML format and returns
* the highest version entry with its download URL.
*
* @param string $updateServerUrl URL to the updates.xml file
*
* @return array [version, download_url] or empty array
*/
private function fetchFromUpdateServer(string $updateServerUrl): array
{
if (empty($updateServerUrl))
{
return [];
}
$ch = curl_init($updateServerUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200 || empty($response))
{
return [];
}
$xml = @simplexml_load_string($response);
if (!$xml)
{
return [];
}
// Find the highest version entry
$bestVersion = '0.0.0';
$downloadUrl = '';
foreach ($xml->update as $update)
{
$ver = (string) ($update->version ?? '');
if ($ver === '' || version_compare($ver, $bestVersion, '<='))
{
continue;
}
$bestVersion = $ver;
// Get download URL from <downloads><downloadurl>
if (isset($update->downloads->downloadurl))
{
$downloadUrl = (string) $update->downloads->downloadurl;
}
}
if ($bestVersion === '0.0.0')
{
return [];
}
return [
'version' => $bestVersion,
'download_url' => $downloadUrl,
];
}
/**
* Get installed versions of catalog extensions.
*
* @param array $catalog The parsed catalog entries
*
* @return array element => version
*/
private function getInstalledVersions(array $catalog): array
{
if (empty($catalog))
{
return [];
}
$db = $this->getDatabase();
$elements = [];
foreach ($catalog as $entry)
{
$elements[] = $db->quote($entry['element']);
}
$query = $db->getQuery(true)
->select([$db->quoteName('element'), $db->quoteName('manifest_cache')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' IN (' . implode(',', $elements) . ')');
$db->setQuery($query);
$rows = $db->loadObjectList() ?: [];
$versions = [];
foreach ($rows as $row)
{
$mc = json_decode($row->manifest_cache ?? '{}');
$versions[$row->element] = $mc->version ?? '0.0.0';
}
return $versions;
}
/**
* Get the extension_id for an element (for uninstall links).
*
* @param string $element Extension element name
*
* @return int
*/
private function getExtensionId(string $element): int
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('extension_id'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->setLimit(1);
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -0,0 +1,522 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Registry\Registry;
/**
* .htaccess / NginX configuration generator.
*
* @since 02.32.00
*/
class HtaccessModel extends BaseDatabaseModel
{
private const DEFAULTS = [
// Security
'disable_directory_listing' => 1,
'block_sensitive_files' => 1,
'block_php_in_uploads' => 1,
'disable_server_signature' => 1,
'prevent_clickjacking' => 1,
'prevent_mime_sniffing' => 1,
'xss_protection' => 1,
'disable_trace_track' => 1,
'referrer_policy' => 'strict-origin-when-cross-origin',
'hsts_enabled' => 0,
'hsts_max_age' => 31536000,
'hsts_subdomains' => 0,
'csp_enabled' => 0,
'csp_value' => '',
'permissions_policy' => 0,
'permissions_value' => '',
// Performance
'enable_gzip' => 1,
'enable_expires' => 1,
'expires_html' => 3600,
'expires_css_js' => 2592000,
'expires_images' => 31536000,
'etag_control' => 0,
// SEO
'www_redirect' => 'off',
'redirect_index_php' => 1,
'force_trailing_slash' => 0,
// Custom
'custom_rules' => '',
];
/**
* Get saved options or defaults.
*/
public function getOptions(): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query);
$params = new Registry($db->loadResult() ?? '{}');
$htaccess = $params->get('htaccess', null);
if ($htaccess)
{
return array_merge(self::DEFAULTS, (array) json_decode(json_encode($htaccess), true));
}
return self::DEFAULTS;
}
/**
* Save options to component params.
*/
public function saveOptions(array $options): array
{
try
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query);
$params = new Registry($db->loadResult() ?? '{}');
$clean = [];
foreach (self::DEFAULTS as $key => $default)
{
$clean[$key] = $options[$key] ?? $default;
}
$params->set('htaccess', $clean);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute();
return ['success' => true, 'message' => 'Options saved.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Save failed: ' . $e->getMessage()];
}
}
/**
* Read the current .htaccess file.
*/
public function readCurrentHtaccess(): string
{
$path = JPATH_ROOT . '/.htaccess';
return file_exists($path) ? file_get_contents($path) : '';
}
/**
* Write .htaccess to disk with backup.
*/
public function saveHtaccess(string $content): array
{
$path = JPATH_ROOT . '/.htaccess';
$backup = JPATH_ROOT . '/.htaccess.mokowaas.bak';
try
{
// Backup existing
if (file_exists($path))
{
copy($path, $backup);
}
$result = file_put_contents($path, $content);
if ($result === false)
{
// Restore backup
if (file_exists($backup))
{
copy($backup, $path);
}
return ['success' => false, 'message' => '.htaccess is not writable.'];
}
return ['success' => true, 'message' => '.htaccess saved. Backup at .htaccess.mokowaas.bak'];
}
catch (\Throwable $e)
{
if (file_exists($backup))
{
@copy($backup, $path);
}
return ['success' => false, 'message' => 'Write failed: ' . $e->getMessage()];
}
}
/**
* Generate .htaccess content from options.
*/
public function generateHtaccess(array $opts): string
{
$lines = [];
$lines[] = '##';
$lines[] = '## MokoWaaS Generated .htaccess';
$lines[] = '## Generated: ' . gmdate('Y-m-d H:i:s') . ' UTC';
$lines[] = '## DO NOT EDIT — regenerate from MokoWaaS > .htaccess Maker';
$lines[] = '##';
$lines[] = '';
// --- Security ---
if (!empty($opts['disable_directory_listing']))
{
$lines[] = '## Disable directory listing';
$lines[] = 'Options -Indexes';
$lines[] = '';
}
if (!empty($opts['disable_server_signature']))
{
$lines[] = '## Hide server signature';
$lines[] = 'ServerSignature Off';
$lines[] = '<IfModule mod_headers.c>';
$lines[] = ' Header unset X-Powered-By';
$lines[] = ' Header unset Server';
$lines[] = '</IfModule>';
$lines[] = '';
}
if (!empty($opts['block_sensitive_files']))
{
$lines[] = '## Block access to sensitive files';
$lines[] = '<FilesMatch "^(htaccess\.txt|web\.config\.txt|configuration\.php-dist|README\.txt|LICENSE\.txt|joomla\.xml|robots\.txt\.dist)$">';
$lines[] = ' <IfModule mod_authz_core.c>';
$lines[] = ' Require all denied';
$lines[] = ' </IfModule>';
$lines[] = '</FilesMatch>';
$lines[] = '';
}
if (!empty($opts['block_php_in_uploads']))
{
$lines[] = '## Block PHP execution in upload directories';
$dirs = ['images', 'media', 'tmp', 'cache', 'logs'];
foreach ($dirs as $dir)
{
$lines[] = '<Directory "' . $dir . '">';
$lines[] = ' <FilesMatch "\.php$">';
$lines[] = ' <IfModule mod_authz_core.c>';
$lines[] = ' Require all denied';
$lines[] = ' </IfModule>';
$lines[] = ' </FilesMatch>';
$lines[] = '</Directory>';
}
$lines[] = '';
}
if (!empty($opts['disable_trace_track']))
{
$lines[] = '## Disable TRACE and TRACK methods';
$lines[] = '<IfModule mod_rewrite.c>';
$lines[] = ' RewriteEngine On';
$lines[] = ' RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)';
$lines[] = ' RewriteRule .* - [F]';
$lines[] = '</IfModule>';
$lines[] = '';
}
// Security headers
$headers = [];
if (!empty($opts['prevent_clickjacking']))
{
$headers[] = ' Header always set X-Frame-Options "SAMEORIGIN"';
}
if (!empty($opts['prevent_mime_sniffing']))
{
$headers[] = ' Header always set X-Content-Type-Options "nosniff"';
}
if (!empty($opts['xss_protection']))
{
$headers[] = ' Header always set X-XSS-Protection "1; mode=block"';
}
$referrer = $opts['referrer_policy'] ?? '';
if (!empty($referrer) && $referrer !== 'off')
{
$headers[] = ' Header always set Referrer-Policy "' . $referrer . '"';
}
if (!empty($opts['hsts_enabled']))
{
$maxAge = (int) ($opts['hsts_max_age'] ?? 31536000);
$hsts = 'max-age=' . $maxAge;
if (!empty($opts['hsts_subdomains']))
{
$hsts .= '; includeSubDomains';
}
$headers[] = ' Header always set Strict-Transport-Security "' . $hsts . '"';
}
if (!empty($opts['csp_enabled']) && !empty($opts['csp_value']))
{
$headers[] = ' Header always set Content-Security-Policy "' . str_replace('"', '', $opts['csp_value']) . '"';
}
if (!empty($opts['permissions_policy']) && !empty($opts['permissions_value']))
{
$headers[] = ' Header always set Permissions-Policy "' . str_replace('"', '', $opts['permissions_value']) . '"';
}
if (!empty($headers))
{
$lines[] = '## Security headers';
$lines[] = '<IfModule mod_headers.c>';
$lines = array_merge($lines, $headers);
$lines[] = '</IfModule>';
$lines[] = '';
}
// --- Performance ---
if (!empty($opts['enable_gzip']))
{
$lines[] = '## GZip compression';
$lines[] = '<IfModule mod_deflate.c>';
$lines[] = ' AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css';
$lines[] = ' AddOutputFilterByType DEFLATE text/javascript application/javascript application/x-javascript';
$lines[] = ' AddOutputFilterByType DEFLATE application/json application/xml application/rss+xml';
$lines[] = ' AddOutputFilterByType DEFLATE image/svg+xml application/font-woff application/font-woff2';
$lines[] = '</IfModule>';
$lines[] = '';
}
if (!empty($opts['enable_expires']))
{
$html = (int) ($opts['expires_html'] ?? 3600);
$cssJs = (int) ($opts['expires_css_js'] ?? 2592000);
$images = (int) ($opts['expires_images'] ?? 31536000);
$lines[] = '## Browser caching';
$lines[] = '<IfModule mod_expires.c>';
$lines[] = ' ExpiresActive On';
$lines[] = ' ExpiresDefault "access plus ' . $html . ' seconds"';
$lines[] = ' ExpiresByType text/html "access plus ' . $html . ' seconds"';
$lines[] = ' ExpiresByType text/css "access plus ' . $cssJs . ' seconds"';
$lines[] = ' ExpiresByType text/javascript "access plus ' . $cssJs . ' seconds"';
$lines[] = ' ExpiresByType application/javascript "access plus ' . $cssJs . ' seconds"';
$lines[] = ' ExpiresByType image/jpeg "access plus ' . $images . ' seconds"';
$lines[] = ' ExpiresByType image/png "access plus ' . $images . ' seconds"';
$lines[] = ' ExpiresByType image/gif "access plus ' . $images . ' seconds"';
$lines[] = ' ExpiresByType image/webp "access plus ' . $images . ' seconds"';
$lines[] = ' ExpiresByType image/svg+xml "access plus ' . $images . ' seconds"';
$lines[] = ' ExpiresByType font/woff2 "access plus ' . $images . ' seconds"';
$lines[] = '</IfModule>';
$lines[] = '';
}
if (!empty($opts['etag_control']))
{
$lines[] = '## Disable ETags (for load-balanced environments)';
$lines[] = '<IfModule mod_headers.c>';
$lines[] = ' Header unset ETag';
$lines[] = '</IfModule>';
$lines[] = 'FileETag None';
$lines[] = '';
}
// --- SEO / Redirects ---
$wwwRedirect = $opts['www_redirect'] ?? 'off';
if ($wwwRedirect !== 'off' || !empty($opts['redirect_index_php']) || !empty($opts['force_trailing_slash']))
{
$lines[] = '## SEO redirects';
$lines[] = '<IfModule mod_rewrite.c>';
$lines[] = ' RewriteEngine On';
if ($wwwRedirect === 'www')
{
$lines[] = '';
$lines[] = ' ## Force www';
$lines[] = ' RewriteCond %{HTTP_HOST} !^www\. [NC]';
$lines[] = ' RewriteRule ^(.*)$ https://www.%{HTTP_HOST}/$1 [R=301,L]';
}
elseif ($wwwRedirect === 'non-www')
{
$lines[] = '';
$lines[] = ' ## Force non-www';
$lines[] = ' RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]';
$lines[] = ' RewriteRule ^(.*)$ https://%1/$1 [R=301,L]';
}
if (!empty($opts['redirect_index_php']))
{
$lines[] = '';
$lines[] = ' ## Redirect /index.php to root';
$lines[] = ' RewriteCond %{THE_REQUEST} ^[A-Z]{3,}\s/+index\.php\s [NC]';
$lines[] = ' RewriteRule ^index\.php/?(.*)$ /$1 [R=301,L]';
}
if (!empty($opts['force_trailing_slash']))
{
$lines[] = '';
$lines[] = ' ## Force trailing slash';
$lines[] = ' RewriteCond %{REQUEST_FILENAME} !-f';
$lines[] = ' RewriteCond %{REQUEST_URI} !(.*)/$';
$lines[] = ' RewriteRule ^(.*)$ /$1/ [R=301,L]';
}
$lines[] = '</IfModule>';
$lines[] = '';
}
// --- Custom rules ---
$custom = trim($opts['custom_rules'] ?? '');
if (!empty($custom))
{
$lines[] = '## Custom rules';
$lines[] = $custom;
$lines[] = '';
}
return implode("\n", $lines);
}
/**
* Generate equivalent NginX configuration snippet.
*/
public function generateNginx(array $opts): string
{
$lines = [];
$lines[] = '## MokoWaaS Generated NginX Configuration';
$lines[] = '## Add these directives inside your server { } block';
$lines[] = '';
if (!empty($opts['disable_directory_listing']))
{
$lines[] = '# Disable directory listing';
$lines[] = 'autoindex off;';
$lines[] = '';
}
if (!empty($opts['disable_server_signature']))
{
$lines[] = '# Hide server version';
$lines[] = 'server_tokens off;';
$lines[] = '';
}
if (!empty($opts['block_sensitive_files']))
{
$lines[] = '# Block sensitive files';
$lines[] = 'location ~* (htaccess\.txt|web\.config\.txt|configuration\.php-dist|README\.txt|LICENSE\.txt)$ {';
$lines[] = ' deny all;';
$lines[] = '}';
$lines[] = '';
}
if (!empty($opts['block_php_in_uploads']))
{
$lines[] = '# Block PHP in upload directories';
$lines[] = 'location ~* ^/(images|media|tmp|cache|logs)/.*\.php$ {';
$lines[] = ' deny all;';
$lines[] = '}';
$lines[] = '';
}
// Headers
$hdrs = [];
if (!empty($opts['prevent_clickjacking']))
{
$hdrs[] = 'add_header X-Frame-Options "SAMEORIGIN" always;';
}
if (!empty($opts['prevent_mime_sniffing']))
{
$hdrs[] = 'add_header X-Content-Type-Options "nosniff" always;';
}
if (!empty($opts['xss_protection']))
{
$hdrs[] = 'add_header X-XSS-Protection "1; mode=block" always;';
}
$referrer = $opts['referrer_policy'] ?? '';
if (!empty($referrer) && $referrer !== 'off')
{
$hdrs[] = 'add_header Referrer-Policy "' . $referrer . '" always;';
}
if (!empty($opts['hsts_enabled']))
{
$maxAge = (int) ($opts['hsts_max_age'] ?? 31536000);
$hsts = 'max-age=' . $maxAge;
if (!empty($opts['hsts_subdomains']))
{
$hsts .= '; includeSubDomains';
}
$hdrs[] = 'add_header Strict-Transport-Security "' . $hsts . '" always;';
}
if (!empty($hdrs))
{
$lines[] = '# Security headers';
$lines = array_merge($lines, $hdrs);
$lines[] = '';
}
if (!empty($opts['enable_gzip']))
{
$lines[] = '# GZip compression';
$lines[] = 'gzip on;';
$lines[] = 'gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;';
$lines[] = 'gzip_min_length 256;';
$lines[] = '';
}
if (!empty($opts['enable_expires']))
{
$cssJs = (int) ($opts['expires_css_js'] ?? 2592000);
$images = (int) ($opts['expires_images'] ?? 31536000);
$lines[] = '# Browser caching';
$lines[] = 'location ~* \.(css|js)$ {';
$lines[] = ' expires ' . round($cssJs / 86400) . 'd;';
$lines[] = '}';
$lines[] = 'location ~* \.(jpg|jpeg|png|gif|webp|svg|ico|woff2)$ {';
$lines[] = ' expires ' . round($images / 86400) . 'd;';
$lines[] = '}';
$lines[] = '';
}
return implode("\n", $lines);
}
}
@@ -0,0 +1,688 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Registry\Registry;
/**
* Importer for migrating from Akeeba Admin Tools to MokoWaaS.
*
* Reads Admin Tools WAF config, htaccess settings, IP blocklists,
* and security headers — maps them to MokoWaaS firewall plugin params
* and htaccess maker options.
*
* @since 02.32.00
*/
class ImportModel extends BaseDatabaseModel
{
/**
* Check if Admin Tools data is available for import.
* Returns null if already imported or no data found.
*/
public function checkAdminToolsAvailable(): ?object
{
if ($this->wasImported('admintools'))
{
return null;
}
$db = $this->getDatabase();
try
{
$result = (object) [
'component' => false,
'waf_config' => false,
'storage' => false,
'ip_blocks' => 0,
];
// Check component
$db->setQuery("SELECT COUNT(*) FROM #__extensions WHERE element = 'com_admintools' AND type = 'component'");
$result->component = (int) $db->loadResult() > 0;
// Check WAF config table
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%admintools_wafconfig%'));
if ($db->loadResult())
{
$result->waf_config = true;
$db->setQuery('SELECT COUNT(*) FROM #__admintools_wafconfig');
$result->waf_settings = (int) $db->loadResult();
}
// Check storage table
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%admintools_storage%'));
if ($db->loadResult())
{
$result->storage = true;
}
// Check IP blocklist
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%admintools_ipblock%'));
if ($db->loadResult())
{
$db->setQuery('SELECT COUNT(*) FROM #__admintools_ipblock');
$result->ip_blocks = (int) $db->loadResult();
}
// Only available if at least one data source exists
if (!$result->component && !$result->waf_config && !$result->storage)
{
return null;
}
return $result;
}
catch (\Throwable $e)
{
return null;
}
}
/**
* Import Admin Tools settings into MokoWaaS.
*/
public function importAdminTools(): array
{
$db = $this->getDatabase();
$results = ['firewall' => 0, 'htaccess' => 0, 'ip_blocks' => 0, 'disabled' => false];
try
{
// ============================================================
// 1. Import WAF Config → Firewall plugin params
// ============================================================
$wafSettings = $this->readWafConfig($db);
$firewallParams = $this->mapWafToFirewall($wafSettings);
if (!empty($firewallParams))
{
$this->mergePluginParams('mokowaas_firewall', 'system', $firewallParams);
$results['firewall'] = \count($firewallParams);
}
// ============================================================
// 2. Import htaccess settings → component htaccess options
// ============================================================
$htaccessSettings = $this->readHtaccessConfig($db);
$htaccessOptions = $this->mapToHtaccess($htaccessSettings, $wafSettings);
if (!empty($htaccessOptions))
{
$this->mergeComponentHtaccessOptions($htaccessOptions);
$results['htaccess'] = \count($htaccessOptions);
}
// ============================================================
// 3. Import IP blocklist → Firewall IP deny list
// ============================================================
$ipBlocks = $this->readIpBlocklist($db);
if (!empty($ipBlocks))
{
$this->mergeIpBlocklist($ipBlocks);
$results['ip_blocks'] = \count($ipBlocks);
}
// ============================================================
// 4. Disable Admin Tools
// ============================================================
$this->disableAdminTools($db);
$results['disabled'] = true;
$this->markImported('admintools');
return [
'success' => true,
'message' => \sprintf(
'Imported %d firewall settings, %d htaccess options, %d blocked IPs from Admin Tools. Admin Tools has been disabled.',
$results['firewall'], $results['htaccess'], $results['ip_blocks']
),
'counts' => $results,
];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Import failed: ' . $e->getMessage()];
}
}
/**
* Read WAF config from #__admintools_wafconfig.
*/
private function readWafConfig($db): array
{
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%admintools_wafconfig%'));
if (!$db->loadResult())
{
return [];
}
$db->setQuery('SELECT * FROM #__admintools_wafconfig');
$rows = $db->loadObjectList() ?: [];
$config = [];
foreach ($rows as $row)
{
$key = $row->key ?? $row->option ?? '';
if (!empty($key))
{
$config[$key] = $row->value ?? '';
}
}
return $config;
}
catch (\Throwable $e)
{
return [];
}
}
/**
* Read htaccess/server config from #__admintools_storage.
*/
private function readHtaccessConfig($db): array
{
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%admintools_storage%'));
if (!$db->loadResult())
{
return [];
}
$db->setQuery('SELECT * FROM #__admintools_storage');
$rows = $db->loadObjectList() ?: [];
$config = [];
foreach ($rows as $row)
{
$key = $row->key ?? '';
if (!empty($key))
{
$config[$key] = $row->value ?? '';
}
}
return $config;
}
catch (\Throwable $e)
{
return [];
}
}
/**
* Read IP blocklist from #__admintools_ipblock.
*/
private function readIpBlocklist($db): array
{
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%admintools_ipblock%'));
if (!$db->loadResult())
{
return [];
}
$db->setQuery('SELECT ip FROM #__admintools_ipblock');
return $db->loadColumn() ?: [];
}
catch (\Throwable $e)
{
return [];
}
}
/**
* Map Admin Tools WAF config to MokoWaaS firewall plugin params.
*/
private function mapWafToFirewall(array $waf): array
{
$params = [];
// WAF shields
if (isset($waf['sqlishield']))
{
$params['waf_sqli'] = (int) $waf['sqlishield'] ? 1 : 0;
}
if (isset($waf['antispam']))
{
$params['waf_xss'] = (int) $waf['antispam'] ? 1 : 0;
}
if (isset($waf['muashield']))
{
$params['waf_mua'] = (int) $waf['muashield'] ? 1 : 0;
}
if (isset($waf['rfishield']))
{
$params['waf_rfi'] = (int) $waf['rfishield'] ? 1 : 0;
}
if (isset($waf['dfishield']))
{
$params['waf_dfi'] = (int) $waf['dfishield'] ? 1 : 0;
}
if (isset($waf['uploadshield']))
{
// Map to our block_direct_php
$params['block_direct_php'] = (int) $waf['uploadshield'] ? 1 : 0;
}
// Admin secret URL
if (!empty($waf['adminpw']))
{
$params['admin_secret'] = $waf['adminpw'];
}
// Block frontend super user login
if (isset($waf['nofesalogin']))
{
$params['block_frontend_superuser'] = (int) $waf['nofesalogin'] ? 1 : 0;
}
// Session timeout
if (!empty($waf['sessionshield']) && !empty($waf['session_timeout']))
{
$params['admin_session_timeout'] = (int) $waf['session_timeout'];
}
// Template switch blocking
if (isset($waf['tmpl']))
{
$params['block_template_switch'] = (int) $waf['tmpl'] ? 1 : 0;
}
// Blocked sensitive files
if (isset($waf['hogfiles']))
{
$params['block_sensitive_files'] = (int) $waf['hogfiles'] ? 1 : 0;
}
return $params;
}
/**
* Map Admin Tools config to MokoWaaS htaccess maker options.
*/
private function mapToHtaccess(array $storage, array $waf): array
{
$opts = [];
// Server signature
if (isset($waf['serversignature']) || isset($storage['serversignature']))
{
$opts['disable_server_signature'] = 1;
}
// Clickjacking
if (isset($waf['clickjacking']) || isset($storage['xframeoptions']))
{
$opts['prevent_clickjacking'] = 1;
}
// HSTS
if (!empty($storage['hstsheader']) || !empty($waf['hstsheader']))
{
$opts['hsts_enabled'] = 1;
if (!empty($storage['hstsmaxage']))
{
$opts['hsts_max_age'] = (int) $storage['hstsmaxage'];
}
}
// GZip
if (isset($storage['gzipcompression']))
{
$opts['enable_gzip'] = (int) $storage['gzipcompression'] ? 1 : 0;
}
// Expiration
if (isset($storage['exptime']))
{
$opts['enable_expires'] = (int) $storage['exptime'] ? 1 : 0;
}
// ETag
if (isset($storage['etagtype']))
{
$opts['etag_control'] = ($storage['etagtype'] === 'none') ? 1 : 0;
}
// Redirect www / non-www
if (!empty($storage['wwwredir']))
{
$map = ['www' => 'www', 'nowww' => 'non-www'];
$opts['www_redirect'] = $map[$storage['wwwredir']] ?? 'off';
}
// Directory listing
if (isset($storage['nodirlisting']))
{
$opts['disable_directory_listing'] = (int) $storage['nodirlisting'] ? 1 : 0;
}
// Block PHP in uploads
if (isset($storage['phpuploadexec']))
{
$opts['block_php_in_uploads'] = (int) $storage['phpuploadexec'] ? 1 : 0;
}
// Sensitive files
if (isset($storage['hogfiles']))
{
$opts['block_sensitive_files'] = (int) $storage['hogfiles'] ? 1 : 0;
}
return $opts;
}
/**
* Merge params into a plugin's existing params.
*/
private function mergePluginParams(string $element, string $folder, array $newParams): void
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote($folder));
$db->setQuery($query);
$current = new Registry($db->loadResult() ?? '{}');
foreach ($newParams as $key => $value)
{
$current->set($key, $value);
}
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($current->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote($element))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote($folder))
)->execute();
}
/**
* Merge htaccess options into the component params.
*/
private function mergeComponentHtaccessOptions(array $options): void
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
$db->setQuery($query);
$params = new Registry($db->loadResult() ?? '{}');
$htaccess = (array) json_decode(json_encode($params->get('htaccess', new \stdClass())), true);
foreach ($options as $key => $value)
{
$htaccess[$key] = $value;
}
$params->set('htaccess', $htaccess);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute();
}
/**
* Merge imported IPs into the firewall IP blocklist.
*/
private function mergeIpBlocklist(array $ips): void
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
$db->setQuery($query);
$params = new Registry($db->loadResult() ?? '{}');
$blocklist = json_decode($params->get('ip_blocklist', '[]'), true) ?: [];
$existingIps = array_column($blocklist, 'ip');
foreach ($ips as $ip)
{
$ip = trim($ip);
if (empty($ip) || \in_array($ip, $existingIps, true))
{
continue;
}
$blocklist[] = [
'ip' => $ip,
'enabled' => '1',
'label' => 'Imported from Admin Tools',
];
}
$params->set('ip_blocklist', json_encode($blocklist));
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute();
}
/**
* Disable Admin Tools component and plugins.
*/
private function disableAdminTools($db): void
{
// Disable component
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 0')
->where($db->quoteName('element') . ' = ' . $db->quote('com_admintools'))
)->execute();
// Disable all Admin Tools plugins
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 0')
->where($db->quoteName('element') . ' LIKE ' . $db->quote('admintools%'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
)->execute();
Log::add('Admin Tools component and plugins disabled after MokoWaaS import', Log::INFO, 'mokowaas');
}
// ==================================================================
// Akeeba Ticket System Import
// ==================================================================
/**
* Check if ATS tables exist.
* Returns null if already imported or no data found.
*/
public function checkAtsAvailable(): ?object
{
if ($this->wasImported('ats'))
{
return null;
}
$db = $this->getDatabase();
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%ats_tickets%'));
if (!$db->loadResult())
{
return null;
}
$db->setQuery('SELECT COUNT(*) FROM #__ats_tickets');
$tickets = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__ats_posts');
$posts = (int) $db->loadResult();
return (object) ['tickets' => $tickets, 'posts' => $posts];
}
catch (\Throwable $e)
{
return null;
}
}
/**
* Import from Akeeba Ticket System and disable it.
*/
public function importAts(): array
{
// Delegate to TicketsModel for the actual import
$ticketsModel = new TicketsModel();
$result = $ticketsModel->importFromAts();
if (!$result['success'])
{
return $result;
}
// Disable ATS after successful import
try
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 0')
->where($db->quoteName('element') . ' = ' . $db->quote('com_ats'))
)->execute();
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 0')
->where($db->quoteName('element') . ' LIKE ' . $db->quote('ats%'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
)->execute();
$result['message'] .= ' Akeeba Ticket System has been disabled.';
Log::add('Akeeba Ticket System disabled after MokoWaaS import', Log::INFO, 'mokowaas');
}
catch (\Throwable $e)
{
$result['message'] .= ' Warning: could not disable ATS: ' . $e->getMessage();
}
$this->markImported('ats');
return $result;
}
// ==================================================================
// Import markers (stored in component params)
// ==================================================================
private function wasImported(string $key): bool
{
try
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = new Registry($db->loadResult() ?? '{}');
return (bool) $params->get('imported_' . $key, false);
}
catch (\Throwable $e)
{
return false;
}
}
private function markImported(string $key): void
{
try
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = new Registry($db->loadResult() ?? '{}');
$params->set('imported_' . $key, 1);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
)->execute();
}
catch (\Throwable $e)
{
Log::add('Import marker error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
@@ -0,0 +1,251 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class MaintenanceModel extends BaseDatabaseModel
{
/**
* Get database table status (size, rows, engine, overhead).
*/
public function getTableStatus(): array
{
$db = $this->getDatabase();
$prefix = $db->getPrefix();
$db->setQuery('SHOW TABLE STATUS');
$tables = $db->loadObjectList() ?: [];
$results = [];
$totalSize = 0;
$totalOverhead = 0;
foreach ($tables as $t)
{
$sizeMb = round(($t->Data_length + $t->Index_length) / 1048576, 2);
$overheadKb = round(($t->Data_free ?? 0) / 1024, 1);
$totalSize += $sizeMb;
$totalOverhead += $overheadKb;
$results[] = (object) [
'name' => $t->Name,
'rows' => (int) $t->Rows,
'engine' => $t->Engine,
'size_mb' => $sizeMb,
'overhead_kb' => $overheadKb,
'is_moko' => str_contains($t->Name, 'mokowaas'),
];
}
usort($results, fn($a, $b) => $b->size_mb <=> $a->size_mb);
return ['tables' => $results, 'total_size_mb' => round($totalSize, 2), 'total_overhead_kb' => round($totalOverhead, 1), 'count' => \count($results)];
}
/**
* Optimize all tables or specific ones.
*/
public function optimizeTables(array $tableNames = []): array
{
$db = $this->getDatabase();
$count = 0;
try
{
if (empty($tableNames))
{
$db->setQuery('SHOW TABLE STATUS WHERE Data_free > 0');
$tables = $db->loadObjectList() ?: [];
$tableNames = array_column($tables, 'Name');
}
foreach ($tableNames as $name)
{
$db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($name));
$db->execute();
$count++;
}
return ['success' => true, 'message' => "Optimized {$count} tables."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Optimize failed: ' . $e->getMessage()];
}
}
/**
* Repair all tables.
*/
public function repairTables(): array
{
$db = $this->getDatabase();
try
{
$db->setQuery('SHOW TABLE STATUS');
$tables = $db->loadObjectList() ?: [];
$count = 0;
foreach ($tables as $t)
{
if ($t->Engine === 'InnoDB' || $t->Engine === 'MyISAM')
{
$db->setQuery('REPAIR TABLE ' . $db->quoteName($t->Name));
$db->execute();
$count++;
}
}
return ['success' => true, 'message' => "Repaired {$count} tables."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Repair failed: ' . $e->getMessage()];
}
}
/**
* Purge expired sessions.
*/
public function purgeSessions(): array
{
try
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__session'))
->where($db->quoteName('time') . ' < ' . (time() - 86400))
)->execute();
return ['success' => true, 'message' => 'Expired sessions purged. ' . $db->getAffectedRows() . ' removed.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => $e->getMessage()];
}
}
// ==================================================================
// Temp/Cache Cleanup (#128)
// ==================================================================
/**
* Get directory sizes for cleanup.
*/
public function getCleanupInfo(): array
{
$dirs = [
['path' => JPATH_ROOT . '/cache', 'label' => 'Site Cache'],
['path' => JPATH_ADMINISTRATOR . '/cache', 'label' => 'Admin Cache'],
['path' => JPATH_ROOT . '/tmp', 'label' => 'Temp Directory'],
['path' => JPATH_ADMINISTRATOR . '/logs', 'label' => 'Log Files'],
];
$results = [];
foreach ($dirs as $dir)
{
$size = 0;
$files = 0;
if (is_dir($dir['path']))
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir['path'], \RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file)
{
if ($file->isFile())
{
$size += $file->getSize();
$files++;
}
}
}
$results[] = (object) [
'label' => $dir['label'],
'path' => $dir['path'],
'size_mb' => round($size / 1048576, 2),
'files' => $files,
'writable' => is_writable($dir['path']),
];
}
return $results;
}
/**
* Clean a specific directory.
*/
public function cleanDirectory(string $dirKey): array
{
$allowed = [
'site_cache' => JPATH_ROOT . '/cache',
'admin_cache' => JPATH_ADMINISTRATOR . '/cache',
'tmp' => JPATH_ROOT . '/tmp',
'logs' => JPATH_ADMINISTRATOR . '/logs',
];
if (!isset($allowed[$dirKey]))
{
return ['success' => false, 'message' => 'Invalid directory.'];
}
$dir = $allowed[$dirKey];
if (!is_dir($dir))
{
return ['success' => false, 'message' => 'Directory not found.'];
}
$count = 0;
try
{
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $item)
{
// Keep index.html and .htaccess files
$name = $item->getFilename();
if ($name === 'index.html' || $name === '.htaccess')
{
continue;
}
if ($item->isDir())
{
@rmdir($item->getPathname());
}
else
{
@unlink($item->getPathname());
$count++;
}
}
// Also clear opcache
if (\function_exists('opcache_reset'))
{
\opcache_reset();
}
return ['success' => true, 'message' => "Cleaned {$count} files from {$dirKey}."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Cleanup failed: ' . $e->getMessage()];
}
}
}
@@ -0,0 +1,612 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class PrivacyModel extends BaseDatabaseModel
{
/**
* Get all pending data requests.
*/
public function getDataRequests(string $filterStatus = ''): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
$db->quoteName('r') . '.*',
$db->quoteName('u.name', 'user_name'),
$db->quoteName('u.email', 'user_email'),
$db->quoteName('u.username'),
$db->quoteName('p.name', 'processed_by_name'),
])
->from($db->quoteName('#__mokowaas_data_requests', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->leftJoin($db->quoteName('#__users', 'p') . ' ON p.id = r.processed_by');
if ($filterStatus)
{
$query->where($db->quoteName('r.status') . ' = ' . $db->quote($filterStatus));
}
$query->order($db->quoteName('r.created') . ' DESC')->setLimit(50);
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
/**
* Create a data request (from admin or user self-service).
*/
public function createRequest(int $userId, string $type, string $notes = ''): array
{
$validTypes = ['export', 'delete', 'anonymize'];
if (!\in_array($type, $validTypes, true))
{
return ['success' => false, 'message' => 'Invalid request type.'];
}
try
{
$db = $this->getDatabase();
$row = (object) [
'user_id' => $userId,
'type' => $type,
'status' => 'pending',
'notes' => $notes,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokowaas_data_requests', $row, 'id');
return ['success' => true, 'message' => ucfirst($type) . ' request #' . $row->id . ' created.', 'id' => (int) $row->id];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
}
}
/**
* Process a data request (approve and execute).
*/
public function processRequest(int $requestId, string $action): array
{
$db = $this->getDatabase();
try
{
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_data_requests'))
->where($db->quoteName('id') . ' = ' . $requestId)
);
$request = $db->loadObject();
if (!$request)
{
return ['success' => false, 'message' => 'Request not found.'];
}
if ($action === 'deny')
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('denied'))
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
->where($db->quoteName('id') . ' = ' . $requestId)
)->execute();
return ['success' => true, 'message' => 'Request denied.'];
}
// Mark as processing
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('processing'))
->where($db->quoteName('id') . ' = ' . $requestId)
)->execute();
// Execute the request
$result = null;
switch ($request->type)
{
case 'export':
$result = $this->exportUserData((int) $request->user_id);
break;
case 'delete':
$result = $this->deleteUserData((int) $request->user_id);
break;
case 'anonymize':
$result = $this->anonymizeUserData((int) $request->user_id);
break;
}
// Mark completed
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_data_requests'))
->set($db->quoteName('status') . ' = ' . $db->quote('completed'))
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
->where($db->quoteName('id') . ' = ' . $requestId)
)->execute();
return $result ?? ['success' => true, 'message' => 'Request processed.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Processing failed: ' . $e->getMessage()];
}
}
/**
* Export all data for a user as a structured array.
*/
public function exportUserData(int $userId): array
{
$db = $this->getDatabase();
$data = ['user_id' => $userId, 'exported' => gmdate('Y-m-d\TH:i:s\Z')];
try
{
// User profile
$db->setQuery(
$db->getQuery(true)
->select(['id', 'name', 'username', 'email', 'registerDate', 'lastvisitDate', 'params'])
->from($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . $userId)
);
$data['profile'] = $db->loadObject();
// Content (articles)
$db->setQuery(
$db->getQuery(true)
->select(['id', 'title', 'alias', 'created', 'modified', 'hits'])
->from($db->quoteName('#__content'))
->where($db->quoteName('created_by') . ' = ' . $userId)
);
$data['articles'] = $db->loadObjectList() ?: [];
// Action logs
$db->setQuery(
$db->getQuery(true)
->select(['message', 'log_date', 'ip_address'])
->from($db->quoteName('#__action_logs'))
->where($db->quoteName('user_id') . ' = ' . $userId)
->order('log_date DESC')
->setLimit(100)
);
$data['action_logs'] = $db->loadObjectList() ?: [];
// Support tickets
$db->setQuery(
$db->getQuery(true)
->select(['id', 'subject', 'body', 'status', 'priority', 'created'])
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
);
$data['tickets'] = $db->loadObjectList() ?: [];
// Ticket replies
$db->setQuery(
$db->getQuery(true)
->select(['r.id', 'r.ticket_id', 'r.body', 'r.created'])
->from($db->quoteName('#__mokowaas_ticket_replies', 'r'))
->where($db->quoteName('r.user_id') . ' = ' . $userId)
);
$data['ticket_replies'] = $db->loadObjectList() ?: [];
// Consent log
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId)
->order('created ASC')
);
$data['consent_history'] = $db->loadObjectList() ?: [];
// Community Builder profile (if table exists)
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%comprofiler%'));
if ($db->loadResult())
{
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__comprofiler'))
->where($db->quoteName('user_id') . ' = ' . $userId)
);
$data['community_builder'] = $db->loadObject();
}
}
catch (\Throwable $e) {}
return ['success' => true, 'message' => 'Data exported.', 'data' => $data];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Export failed: ' . $e->getMessage()];
}
}
/**
* Anonymize a user's data (GDPR right to be forgotten — soft).
*/
public function anonymizeUserData(int $userId): array
{
$db = $this->getDatabase();
$now = Factory::getDate()->toSql();
$anon = 'Anonymous User #' . $userId;
try
{
// Anonymize user record
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__users'))
->set([
$db->quoteName('name') . ' = ' . $db->quote($anon),
$db->quoteName('username') . ' = ' . $db->quote('anon_' . $userId),
$db->quoteName('email') . ' = ' . $db->quote('anon_' . $userId . '@deleted.local'),
$db->quoteName('password') . ' = ' . $db->quote(''),
$db->quoteName('block') . ' = 1',
$db->quoteName('params') . ' = ' . $db->quote('{}'),
])
->where($db->quoteName('id') . ' = ' . $userId)
)->execute();
// Anonymize article authorship
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('created_by_alias') . ' = ' . $db->quote($anon))
->where($db->quoteName('created_by') . ' = ' . $userId)
)->execute();
// Delete action logs
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__action_logs'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
// Anonymize ticket replies
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_ticket_replies'))
->set($db->quoteName('body') . ' = ' . $db->quote('[Content removed per data request]'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
// Community Builder
try
{
$db->setQuery('SHOW TABLES LIKE ' . $db->quote('%comprofiler%'));
if ($db->loadResult())
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__comprofiler'))
->set([
$db->quoteName('firstname') . ' = ' . $db->quote('Anonymous'),
$db->quoteName('lastname') . ' = ' . $db->quote('User'),
$db->quoteName('middlename') . ' = ' . $db->quote(''),
])
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
}
}
catch (\Throwable $e) {}
// Clear Joomla user profile fields (#7)
try
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__user_profiles'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
}
catch (\Throwable $e) {}
// Clear contact details if linked
try
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__contact_details'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
}
catch (\Throwable $e) {}
// Log the anonymization
$this->logConsent($userId, 'account_anonymized', 'granted');
return ['success' => true, 'message' => 'User #' . $userId . ' data anonymized.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Anonymization failed: ' . $e->getMessage()];
}
}
/**
* Delete a user's data completely (hard delete).
*/
public function deleteUserData(int $userId): array
{
$result = $this->anonymizeUserData($userId);
if (!$result['success'])
{
return $result;
}
$db = $this->getDatabase();
try
{
// Delete tickets and replies
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
);
$ticketIds = $db->loadColumn() ?: [];
if (!empty($ticketIds))
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_ticket_replies'))
->where($db->quoteName('ticket_id') . ' IN (' . implode(',', $ticketIds) . ')')
)->execute();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
)->execute();
}
// Delete consent log
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId)
)->execute();
// Delete user record
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . $userId)
)->execute();
return ['success' => true, 'message' => 'User #' . $userId . ' data permanently deleted.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Deletion failed: ' . $e->getMessage()];
}
}
// ==================================================================
// Consent Management
// ==================================================================
/**
* Get consent status for a user.
*/
public function getUserConsent(int $userId): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . $userId)
->order($db->quoteName('created') . ' DESC')
);
return $db->loadObjectList() ?: [];
}
/**
* Record a consent action.
*/
public function logConsent(int $userId, string $category, string $action): void
{
$db = $this->getDatabase();
$row = (object) [
'user_id' => $userId,
'category' => $category,
'action' => $action === 'revoked' ? 'revoked' : 'granted',
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokowaas_consent_log', $row, 'id');
}
// ==================================================================
// Retention Policy Enforcement
// ==================================================================
/**
* Get all retention policies.
*/
public function getRetentionPolicies(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_retention_policies'))
->order($db->quoteName('id') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Run retention policy enforcement (called by scheduled task).
*/
public function enforceRetentionPolicies(): array
{
$db = $this->getDatabase();
$results = ['policies_run' => 0, 'items_affected' => 0];
$policies = $this->getRetentionPolicies();
foreach ($policies as $policy)
{
if (!(int) $policy->enabled)
{
continue;
}
$cutoff = Factory::getDate('-' . (int) $policy->retention_days . ' days')->toSql();
$count = 0;
try
{
switch ($policy->content_type)
{
case 'action_logs':
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__action_logs'))
->where($db->quoteName('log_date') . ' < ' . $db->quote($cutoff))
)->execute();
$count = $db->getAffectedRows();
break;
case 'waf_logs':
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_waf_log'))
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
)->execute();
$count = $db->getAffectedRows();
break;
case 'sessions':
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__session'))
->where($db->quoteName('time') . ' < ' . (int) strtotime($cutoff))
)->execute();
$count = $db->getAffectedRows();
break;
case 'closed_tickets':
if ($policy->action === 'anonymize')
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('body') . ' = ' . $db->quote('[Removed per retention policy]'))
->where($db->quoteName('status') . ' = ' . $db->quote('closed'))
->where($db->quoteName('closed') . ' < ' . $db->quote($cutoff))
->where($db->quoteName('body') . ' != ' . $db->quote('[Removed per retention policy]'))
)->execute();
$count = $db->getAffectedRows();
}
break;
case 'inactive_users':
if ($policy->action === 'anonymize')
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__users'))
->where($db->quoteName('lastvisitDate') . ' < ' . $db->quote($cutoff))
->where($db->quoteName('lastvisitDate') . ' != ' . $db->quote('0000-00-00 00:00:00'))
->where($db->quoteName('block') . ' = 0')
->where($db->quoteName('username') . ' NOT LIKE ' . $db->quote('anon_%'))
);
$userIds = $db->loadColumn() ?: [];
foreach ($userIds as $uid)
{
$this->anonymizeUserData((int) $uid);
$count++;
}
}
break;
}
if ($count > 0)
{
$results['policies_run']++;
$results['items_affected'] += $count;
Log::add(\sprintf('Retention: %s — %d items affected', $policy->content_type, $count), Log::INFO, 'mokowaas');
}
}
catch (\Throwable $e)
{
Log::add('Retention policy error (' . $policy->content_type . '): ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
return $results;
}
/**
* Get privacy dashboard summary counts.
*/
public function getDashboardSummary(): object
{
$db = $this->getDatabase();
$summary = (object) [
'pending_requests' => 0,
'total_requests' => 0,
'consent_entries' => 0,
'policies_active' => 0,
];
try
{
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests WHERE status = ' . $db->quote('pending'));
$summary->pending_requests = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests');
$summary->total_requests = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_consent_log');
$summary->consent_entries = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_retention_policies WHERE enabled = 1');
$summary->policies_active = (int) $db->loadResult();
}
catch (\Throwable $e) {}
return $summary;
}
}
@@ -0,0 +1,945 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Moko\Component\MokoWaaS\Administrator\Service\NotificationService;
class TicketsModel extends BaseDatabaseModel
{
/**
* Get ticket list with filters.
*/
public function getTickets(array $filters = []): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
$db->quoteName('t.id'),
$db->quoteName('t.subject'),
$db->quoteName('t.status'),
$db->quoteName('t.priority'),
$db->quoteName('t.created'),
$db->quoteName('t.modified'),
$db->quoteName('t.sla_response_due'),
$db->quoteName('t.sla_resolution_due'),
$db->quoteName('t.sla_responded'),
$db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'),
$db->quoteName('a.name', 'assigned_to_name'),
])
->from($db->quoteName('#__mokowaas_tickets', 't'))
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to');
if (!empty($filters['status']))
{
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($filters['status']));
}
if (!empty($filters['priority']))
{
$query->where($db->quoteName('t.priority') . ' = ' . $db->quote($filters['priority']));
}
if (!empty($filters['assigned_to']))
{
$query->where($db->quoteName('t.assigned_to') . ' = ' . (int) $filters['assigned_to']);
}
if (!empty($filters['category_id']))
{
$query->where($db->quoteName('t.category_id') . ' = ' . (int) $filters['category_id']);
}
$query->order($db->quoteName('t.created') . ' DESC');
$query->setLimit(50);
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
/**
* Get a single ticket with all replies.
*/
public function getTicket(int $id): ?object
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select([
$db->quoteName('t') . '.*',
$db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'),
$db->quoteName('u.email', 'created_by_email'),
$db->quoteName('a.name', 'assigned_to_name'),
])
->from($db->quoteName('#__mokowaas_tickets', 't'))
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to')
->where($db->quoteName('t.id') . ' = ' . $id);
$db->setQuery($query);
$ticket = $db->loadObject();
if (!$ticket)
{
return null;
}
// Load replies
$query = $db->getQuery(true)
->select([
$db->quoteName('r') . '.*',
$db->quoteName('u.name', 'user_name'),
])
->from($db->quoteName('#__mokowaas_ticket_replies', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->where($db->quoteName('r.ticket_id') . ' = ' . $id)
->order($db->quoteName('r.created') . ' ASC');
$db->setQuery($query);
$ticket->replies = $db->loadObjectList() ?: [];
// Reply count
$ticket->reply_count = \count($ticket->replies);
return $ticket;
}
/**
* Create a new ticket.
*/
public function createTicket(array $data): array
{
try
{
$db = $this->getDatabase();
$user = Factory::getApplication()->getIdentity();
$now = Factory::getDate()->toSql();
$ticket = (object) [
'subject' => $data['subject'] ?? '',
'body' => $data['body'] ?? '',
'status' => 'open',
'priority' => $data['priority'] ?? 'normal',
'category_id' => (int) ($data['category_id'] ?? 0) ?: null,
'created_by' => $user->id,
'assigned_to' => (int) ($data['assigned_to'] ?? 0) ?: null,
'created' => $now,
'modified' => $now,
];
// Auto-assign from category
if (!$ticket->assigned_to && $ticket->category_id)
{
$query = $db->getQuery(true)
->select($db->quoteName('auto_assign_user'))
->from($db->quoteName('#__mokowaas_ticket_categories'))
->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id);
$db->setQuery($query);
$autoAssign = (int) $db->loadResult();
if ($autoAssign)
{
$ticket->assigned_to = $autoAssign;
}
}
// SLA deadlines from category
if ($ticket->category_id)
{
$query = $db->getQuery(true)
->select([$db->quoteName('sla_response_minutes'), $db->quoteName('sla_resolution_minutes')])
->from($db->quoteName('#__mokowaas_ticket_categories'))
->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id);
$db->setQuery($query);
$sla = $db->loadObject();
if ($sla)
{
$ticket->sla_response_due = Factory::getDate($now)->modify('+' . (int) $sla->sla_response_minutes . ' minutes')->toSql();
$ticket->sla_resolution_due = Factory::getDate($now)->modify('+' . (int) $sla->sla_resolution_minutes . ' minutes')->toSql();
}
}
$db->insertObject('#__mokowaas_tickets', $ticket, 'id');
// Run automation + notifications
$this->runAutomation('ticket_created', (int) $ticket->id);
NotificationService::notify('ticket_created', $this->getTicket((int) $ticket->id));
return ['success' => true, 'message' => 'Ticket #' . $ticket->id . ' created.', 'id' => (int) $ticket->id];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
}
}
/**
* Add a reply to a ticket.
*/
public function addReply(int $ticketId, string $body, bool $isInternal = false): array
{
try
{
$db = $this->getDatabase();
$user = Factory::getApplication()->getIdentity();
$now = Factory::getDate()->toSql();
$reply = (object) [
'ticket_id' => $ticketId,
'user_id' => $user->id,
'body' => $body,
'is_internal' => $isInternal ? 1 : 0,
'created' => $now,
];
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
// Mark SLA as responded only for staff replies (not customer self-replies)
$ticket = $this->getTicket($ticketId);
$isStaffReply = $ticket && (int) $user->id !== (int) $ticket->created_by;
$updateQuery = $db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $ticketId);
if ($isStaffReply)
{
$updateQuery->set($db->quoteName('sla_responded') . ' = 1')
->where($db->quoteName('sla_responded') . ' = 0');
}
$db->setQuery($updateQuery)->execute();
// Run automation + notifications (skip internal notes)
$this->runAutomation('ticket_replied', $ticketId);
if (!$isInternal)
{
NotificationService::notify('ticket_replied', $this->getTicket($ticketId), ['reply_body' => $body]);
}
return ['success' => true, 'message' => 'Reply added.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
}
}
/**
* Update ticket status.
*/
public function updateStatus(int $ticketId, string $status): array
{
$valid = ['open', 'in_progress', 'waiting', 'resolved', 'closed'];
if (!\in_array($status, $valid, true))
{
return ['success' => false, 'message' => 'Invalid status.'];
}
try
{
$db = $this->getDatabase();
$now = Factory::getDate()->toSql();
// Capture old status for notification
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('status'))
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('id') . ' = ' . $ticketId)
);
$oldStatus = $db->loadResult() ?? '';
$sets = [
$db->quoteName('status') . ' = ' . $db->quote($status),
$db->quoteName('modified') . ' = ' . $db->quote($now),
];
if ($status === 'resolved')
{
$sets[] = $db->quoteName('resolved') . ' = ' . $db->quote($now);
}
if ($status === 'closed')
{
$sets[] = $db->quoteName('closed') . ' = ' . $db->quote($now);
}
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($sets)
->where($db->quoteName('id') . ' = ' . $ticketId)
)->execute();
// Run automation + notifications
$this->runAutomation('status_changed', $ticketId);
NotificationService::notify('status_changed', $this->getTicket($ticketId), ['old_status' => $oldStatus]);
return ['success' => true, 'message' => 'Status updated to ' . $status . '.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
}
}
/**
* Get all ticket categories.
*/
public function getCategories(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_categories'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get canned responses, optionally filtered by category.
*/
public function getCannedResponses(int $categoryId = 0): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_canned'))
->order($db->quoteName('ordering') . ' ASC');
if ($categoryId)
{
$query->where('(' . $db->quoteName('category_id') . ' = ' . $categoryId
. ' OR ' . $db->quoteName('category_id') . ' IS NULL)');
}
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
/**
* Get ticket counts by status for dashboard.
*/
public function getStatusCounts(): object
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('status'), 'COUNT(*) AS ' . $db->quoteName('cnt')])
->from($db->quoteName('#__mokowaas_tickets'))
->group($db->quoteName('status'))
);
$rows = $db->loadObjectList('status') ?: [];
return (object) [
'open' => (int) ($rows['open']->cnt ?? 0),
'in_progress' => (int) ($rows['in_progress']->cnt ?? 0),
'waiting' => (int) ($rows['waiting']->cnt ?? 0),
'resolved' => (int) ($rows['resolved']->cnt ?? 0),
'closed' => (int) ($rows['closed']->cnt ?? 0),
'total' => array_sum(array_map(fn($r) => (int) $r->cnt, $rows)),
];
}
/**
* Get overdue tickets (SLA breached).
*/
public function getOverdueTickets(): array
{
$db = $this->getDatabase();
$now = Factory::getDate()->toSql();
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('subject'), $db->quoteName('priority'),
$db->quoteName('sla_response_due'), $db->quoteName('sla_resolution_due'), $db->quoteName('sla_responded')])
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
->where('((' . $db->quoteName('sla_response_due') . ' < ' . $db->quote($now) . ' AND ' . $db->quoteName('sla_responded') . ' = 0)'
. ' OR ' . $db->quoteName('sla_resolution_due') . ' < ' . $db->quote($now) . ')')
->order($db->quoteName('sla_resolution_due') . ' ASC');
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
// ==================================================================
// Automation Engine
// ==================================================================
/**
* Run automation rules for a specific trigger event against a ticket.
*
* @param string $event trigger_event: ticket_created, ticket_replied, status_changed, scheduled
* @param int $ticketId The ticket to evaluate
*/
public function runAutomation(string $event, int $ticketId): void
{
try
{
$db = $this->getDatabase();
// Load enabled rules for this event
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$rules = $db->loadObjectList() ?: [];
if (empty($rules))
{
return;
}
// Load the ticket
$ticket = $this->getTicket($ticketId);
if (!$ticket)
{
return;
}
// Calculate age in hours
$ticket->age_hours = (time() - strtotime($ticket->created)) / 3600;
foreach ($rules as $rule)
{
$conditions = json_decode($rule->conditions, true) ?: [];
$actions = json_decode($rule->actions, true) ?: [];
if ($this->evaluateConditions($conditions, $ticket))
{
$this->executeActions($actions, $ticketId, $ticket);
}
}
}
catch (\Throwable $e)
{
\Joomla\CMS\Log\Log::add('Automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
}
}
/**
* Run all scheduled automation rules against all open tickets.
*/
public function runScheduledAutomation(): array
{
$db = $this->getDatabase();
$results = ['evaluated' => 0, 'acted' => 0];
// Load scheduled rules
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote('scheduled'))
->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$rules = $db->loadObjectList() ?: [];
if (empty($rules))
{
return $results;
}
// Load all non-closed tickets
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('status') . ' != ' . $db->quote('closed'));
$db->setQuery($query);
$tickets = $db->loadObjectList() ?: [];
foreach ($tickets as $ticket)
{
$ticket->age_hours = (time() - strtotime($ticket->created)) / 3600;
$ticket->replies = [];
$results['evaluated']++;
foreach ($rules as $rule)
{
$conditions = json_decode($rule->conditions, true) ?: [];
$actions = json_decode($rule->actions, true) ?: [];
if ($this->evaluateConditions($conditions, $ticket))
{
$this->executeActions($actions, (int) $ticket->id, $ticket);
$results['acted']++;
}
}
}
return $results;
}
/**
* Evaluate a set of conditions against a ticket (all must match).
*/
private function evaluateConditions(array $conditions, object $ticket): bool
{
foreach ($conditions as $cond)
{
$field = $cond['field'] ?? '';
$op = $cond['op'] ?? 'eq';
$value = $cond['value'] ?? '';
$ticketValue = $ticket->{$field} ?? null;
if ($ticketValue === null)
{
return false;
}
switch ($op)
{
case 'eq':
if ((string) $ticketValue !== (string) $value) return false;
break;
case 'neq':
if ((string) $ticketValue === (string) $value) return false;
break;
case 'gt':
if ((float) $ticketValue <= (float) $value) return false;
break;
case 'lt':
if ((float) $ticketValue >= (float) $value) return false;
break;
case 'in':
$list = array_map('trim', explode(',', $value));
if (!\in_array((string) $ticketValue, $list, true)) return false;
break;
case 'not_in':
$list = array_map('trim', explode(',', $value));
if (\in_array((string) $ticketValue, $list, true)) return false;
break;
default:
return false;
}
}
return true;
}
/**
* Execute a set of actions on a ticket.
*/
private function executeActions(array $actions, int $ticketId, object $ticket): void
{
$db = $this->getDatabase();
$now = Factory::getDate()->toSql();
foreach ($actions as $action)
{
$type = $action['type'] ?? '';
$value = $action['value'] ?? '';
switch ($type)
{
case 'set_status':
$this->updateStatus($ticketId, $value);
break;
case 'set_priority':
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('priority') . ' = ' . $db->quote($value))
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $ticketId)
)->execute();
break;
case 'assign':
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('assigned_to') . ' = ' . (int) $value)
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $ticketId)
)->execute();
break;
case 'add_note':
$reply = (object) [
'ticket_id' => $ticketId,
'user_id' => 0,
'body' => $value,
'is_internal' => 1,
'created' => $now,
];
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
break;
case 'send_email':
// value = email address or comma-separated list
$emails = array_filter(array_map('trim', explode(',', $value)));
foreach ($emails as $email)
{
try
{
$mailer = Factory::getMailer();
$mailer->addRecipient($email);
$mailer->setSubject('[Ticket #' . $ticketId . '] Automation Alert');
$mailer->setBody('Automation rule triggered for ticket #' . $ticketId . ': ' . ($ticket->subject ?? ''));
$mailer->isHtml(false);
$mailer->Send();
}
catch (\Throwable $e)
{
\Joomla\CMS\Log\Log::add('Automation email failed: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
}
}
break;
case 'create_ticket':
// value = JSON: {"subject":"...","body":"...","category_id":1,"priority":"normal","behavior":"append"}
$ticketData = json_decode($value, true) ?: [];
$behavior = $ticketData['behavior'] ?? 'append';
$userId = (int) ($ticket->created_by ?? 0);
$catId = (int) ($ticketData['category_id'] ?? 0);
if ($behavior === 'append' && $userId > 0)
{
// Check for existing open ticket from this user in this category
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1')
->order($db->quoteName('created') . ' DESC')
->setLimit(1)
);
$existingId = (int) $db->loadResult();
if ($existingId)
{
$this->addReply($existingId, $ticketData['body'] ?? 'Automation event', true);
break;
}
}
elseif ($behavior === 'skip_if_open' && $userId > 0)
{
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_tickets'))
->where($db->quoteName('created_by') . ' = ' . $userId)
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
);
if ((int) $db->loadResult() > 0)
{
break;
}
}
// Create new ticket
$this->createTicket([
'subject' => $ticketData['subject'] ?? 'Automation: ' . ($ticket->subject ?? 'System event'),
'body' => $ticketData['body'] ?? '',
'priority' => $ticketData['priority'] ?? 'normal',
'category_id' => $catId,
]);
break;
}
}
}
/**
* Run automation for a system event (not tied to a specific ticket).
* Creates a virtual ticket context from event data.
*/
public function runSystemEventAutomation(string $event, array $eventData = []): void
{
try
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation'))
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$rules = $db->loadObjectList() ?: [];
if (empty($rules))
{
return;
}
// Build a virtual ticket-like object from event data
$context = (object) array_merge([
'id' => 0,
'subject' => $eventData['subject'] ?? $event,
'body' => $eventData['body'] ?? '',
'status' => 'open',
'priority' => $eventData['priority'] ?? 'normal',
'created_by' => $eventData['user_id'] ?? 0,
'created' => gmdate('Y-m-d H:i:s'),
'age_hours' => 0,
], $eventData);
foreach ($rules as $rule)
{
$conditions = json_decode($rule->conditions, true) ?: [];
$actions = json_decode($rule->actions, true) ?: [];
if (empty($conditions) || $this->evaluateConditions($conditions, $context))
{
$this->executeActions($actions, 0, $context);
}
}
}
catch (\Throwable $e)
{
\Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
}
}
/**
* Get all automation rules.
*/
public function getAutomationRules(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_ticket_automation'))
->order($db->quoteName('ordering') . ' ASC')
);
return $db->loadObjectList() ?: [];
}
// ==================================================================
// Akeeba Ticket System Importer
// ==================================================================
/**
* Check if ATS tables exist and return counts.
*/
public function checkAtsAvailable(): ?object
{
$db = $this->getDatabase();
try
{
$db->setQuery('SELECT COUNT(*) FROM #__ats_tickets');
$tickets = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__ats_posts');
$posts = (int) $db->loadResult();
$db->setQuery('SELECT COUNT(*) FROM #__ats_cannedreplies');
$canned = (int) $db->loadResult();
return (object) ['tickets' => $tickets, 'posts' => $posts, 'canned' => $canned];
}
catch (\Throwable $e)
{
return null;
}
}
/**
* Import tickets, replies, and canned responses from Akeeba Ticket System.
*/
public function importFromAts(): array
{
$db = $this->getDatabase();
$results = ['tickets' => 0, 'replies' => 0, 'canned' => 0, 'errors' => []];
try
{
// Status mapping: ATS → MokoWaaS
$statusMap = [
'O' => 'open', // Open
'P' => 'in_progress', // Pending (staff action needed)
'C' => 'closed', // Closed
];
// Numeric statuses 1-99 are custom — map to open
for ($i = 1; $i <= 99; $i++)
{
$statusMap[(string) $i] = 'open';
}
// Priority mapping: ATS uses 1-5, we use enum
$priorityMap = [
1 => 'low',
2 => 'low',
3 => 'normal',
4 => 'high',
5 => 'urgent',
];
// Category mapping: ATS uses Joomla categories, map catid to our category
// Default all to General Support (1) — admin can reassign later
$defaultCategory = 1;
// Import canned replies first
$db->setQuery('SELECT * FROM #__ats_cannedreplies WHERE enabled = 1 ORDER BY ordering');
$atsCanned = $db->loadObjectList() ?: [];
foreach ($atsCanned as $c)
{
$exists = $db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from('#__mokowaas_ticket_canned')
->where($db->quoteName('title') . ' = ' . $db->quote($c->title))
)->loadResult();
if ((int) $exists > 0)
{
continue;
}
$row = (object) [
'title' => $c->title,
'body' => strip_tags($c->reply ?? ''),
'category_id' => null,
'ordering' => (int) ($c->ordering ?? 0),
];
$db->insertObject('#__mokowaas_ticket_canned', $row, 'id');
$results['canned']++;
}
// Import tickets
$db->setQuery('SELECT * FROM #__ats_tickets ORDER BY id');
$atsTickets = $db->loadObjectList() ?: [];
$ticketIdMap = []; // ATS id → MokoWaaS id
foreach ($atsTickets as $t)
{
// Skip if already imported (check by subject + created_by + created)
$exists = $db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from('#__mokowaas_tickets')
->where($db->quoteName('subject') . ' = ' . $db->quote($t->title))
->where($db->quoteName('created_by') . ' = ' . (int) $t->created_by)
)->loadResult();
if ((int) $exists > 0)
{
continue;
}
$status = $statusMap[$t->status] ?? 'open';
$priority = $priorityMap[(int) $t->priority] ?? 'normal';
$row = (object) [
'subject' => $t->title,
'body' => '',
'status' => $status,
'priority' => $priority,
'category_id' => $defaultCategory,
'created_by' => (int) $t->created_by,
'assigned_to' => (int) $t->assigned_to ?: null,
'created' => $t->created ?: Factory::getDate()->toSql(),
'modified' => $t->modified,
'resolved' => $status === 'closed' ? ($t->modified ?: $t->created) : null,
'closed' => $status === 'closed' ? ($t->modified ?: $t->created) : null,
'sla_responded' => 1,
];
$db->insertObject('#__mokowaas_tickets', $row, 'id');
$ticketIdMap[(int) $t->id] = (int) $row->id;
$results['tickets']++;
}
// Import posts (replies)
$db->setQuery('SELECT * FROM #__ats_posts ORDER BY id');
$atsPosts = $db->loadObjectList() ?: [];
foreach ($atsPosts as $p)
{
$newTicketId = $ticketIdMap[(int) $p->ticket_id] ?? null;
if (!$newTicketId)
{
continue;
}
// First post of a ticket is usually the ticket body — update the ticket
if (empty($results['first_post_' . $p->ticket_id]))
{
$results['first_post_' . $p->ticket_id] = true;
$body = strip_tags($p->content_html ?? '');
$db->setQuery(
$db->getQuery(true)
->update('#__mokowaas_tickets')
->set($db->quoteName('body') . ' = ' . $db->quote($body))
->where($db->quoteName('id') . ' = ' . $newTicketId)
)->execute();
continue;
}
$row = (object) [
'ticket_id' => $newTicketId,
'user_id' => (int) $p->created_by,
'body' => strip_tags($p->content_html ?? ''),
'is_internal' => 0,
'created' => $p->created ?: Factory::getDate()->toSql(),
];
$db->insertObject('#__mokowaas_ticket_replies', $row, 'id');
$results['replies']++;
}
// Clean up temp tracking keys
foreach (array_keys($results) as $k)
{
if (str_starts_with($k, 'first_post_'))
{
unset($results[$k]);
}
}
return [
'success' => true,
'message' => sprintf(
'Imported %d tickets, %d replies, %d canned responses from Akeeba Ticket System.',
$results['tickets'], $results['replies'], $results['canned']
),
'counts' => $results,
];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Import failed: ' . $e->getMessage()];
}
}
}
@@ -0,0 +1,215 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
class WaflogModel extends BaseDatabaseModel
{
/**
* Get WAF log entries with filters and pagination.
*/
public function getLogs(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_waf_log'));
if (!empty($filters['rule']))
{
$query->where($db->quoteName('rule') . ' = ' . $db->quote($filters['rule']));
}
if (!empty($filters['ip']))
{
$query->where($db->quoteName('ip') . ' LIKE ' . $db->quote('%' . $db->escape($filters['ip'], true) . '%'));
}
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('uri') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('detail') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('user_agent') . ' LIKE ' . $search . ')');
}
if (!empty($filters['date_from']))
{
$query->where($db->quoteName('created') . ' >= ' . $db->quote($filters['date_from'] . ' 00:00:00'));
}
if (!empty($filters['date_to']))
{
$query->where($db->quoteName('created') . ' <= ' . $db->quote($filters['date_to'] . ' 23:59:59'));
}
$query->order($db->quoteName('created') . ' DESC');
$query->setLimit($limit, $offset);
$db->setQuery($query);
return $db->loadObjectList() ?: [];
}
/**
* Get total count for pagination.
*/
public function getTotal(array $filters = []): int
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokowaas_waf_log'));
if (!empty($filters['rule']))
{
$query->where($db->quoteName('rule') . ' = ' . $db->quote($filters['rule']));
}
if (!empty($filters['ip']))
{
$query->where($db->quoteName('ip') . ' LIKE ' . $db->quote('%' . $db->escape($filters['ip'], true) . '%'));
}
$db->setQuery($query);
return (int) $db->loadResult();
}
/**
* Get block counts grouped by rule for the summary bar.
*/
public function getRuleCounts(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('rule'), 'COUNT(*) AS ' . $db->quoteName('cnt')])
->from($db->quoteName('#__mokowaas_waf_log'))
->group($db->quoteName('rule'))
->order($db->quoteName('cnt') . ' DESC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get top blocked IPs.
*/
public function getTopIps(int $limit = 10): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('ip'), 'COUNT(*) AS ' . $db->quoteName('cnt'),
'MAX(' . $db->quoteName('created') . ') AS ' . $db->quoteName('last_seen')])
->from($db->quoteName('#__mokowaas_waf_log'))
->group($db->quoteName('ip'))
->order($db->quoteName('cnt') . ' DESC')
->setLimit($limit)
);
return $db->loadObjectList() ?: [];
}
/**
* Get distinct rule names for the filter dropdown.
*/
public function getRuleNames(): array
{
$db = $this->getDatabase();
$db->setQuery(
$db->getQuery(true)
->select('DISTINCT ' . $db->quoteName('rule'))
->from($db->quoteName('#__mokowaas_waf_log'))
->order($db->quoteName('rule') . ' ASC')
);
return $db->loadColumn() ?: [];
}
/**
* Delete logs older than N days.
*/
public function purgeLogs(int $days): array
{
try
{
$db = $this->getDatabase();
$cutoff = Factory::getDate('-' . $days . ' days')->toSql();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__mokowaas_waf_log'))
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
)->execute();
$count = $db->getAffectedRows();
return ['success' => true, 'message' => "Purged {$count} log entries older than {$days} days."];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Purge failed: ' . $e->getMessage()];
}
}
/**
* Add an IP to the firewall blocklist.
*/
public function banIp(string $ip, string $reason = 'Banned from WAF log'): array
{
try
{
$db = $this->getDatabase();
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
$db->setQuery($query);
$params = new \Joomla\Registry\Registry($db->loadResult() ?? '{}');
$blocklist = json_decode($params->get('ip_blocklist', '[]'), true) ?: [];
// Check if already blocked
foreach ($blocklist as $entry)
{
if (($entry['ip'] ?? '') === $ip)
{
return ['success' => false, 'message' => $ip . ' is already blocked.'];
}
}
$blocklist[] = ['ip' => $ip, 'enabled' => '1', 'label' => $reason];
$params->set('ip_blocklist', json_encode($blocklist));
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute();
return ['success' => true, 'message' => $ip . ' has been added to the IP blocklist.'];
}
catch (\Throwable $e)
{
return ['success' => false, 'message' => 'Ban failed: ' . $e->getMessage()];
}
}
}
@@ -0,0 +1,416 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Uri\Uri;
/**
* Helpdesk email notification service.
*
* Sends emails for ticket events to Joomla users (by ID) and/or
* raw email addresses. Uses Joomla's configured mailer.
*
* @since 02.32.00
*/
class NotificationService
{
/**
* Send a ticket notification email.
*
* @param string $event Event name (ticket_created, ticket_replied, status_changed, ticket_assigned)
* @param object $ticket Ticket object with id, subject, status, priority, created_by, assigned_to
* @param array $extra Extra context (reply body, old status, etc.)
*/
public static function notify(string $event, object $ticket, array $extra = []): void
{
try
{
$recipients = self::getRecipients($event, $ticket);
if (empty($recipients))
{
return;
}
$subject = self::buildSubject($event, $ticket);
$body = self::buildBody($event, $ticket, $extra);
$mailer = Factory::getMailer();
$mailer->isHtml(false);
$mailer->setSubject($subject);
$mailer->setBody($body);
foreach ($recipients as $email)
{
$email = trim($email);
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL))
{
continue;
}
try
{
$mailer->clearAddresses();
$mailer->addRecipient($email);
$mailer->Send();
}
catch (\Throwable $e)
{
Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
catch (\Throwable $e)
{
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
/**
* Determine recipients based on event type and ticket data.
*/
private static function getRecipients(string $event, object $ticket): array
{
$emails = [];
// Get notification config from component params
$config = self::getNotificationConfig();
// Always notify configured admin emails
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
$emails = array_merge($emails, $adminEmails);
// Always notify configured admin user IDs
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
foreach ($adminUserIds as $uid)
{
$email = self::getUserEmail($uid);
if ($email)
{
$emails[] = $email;
}
}
switch ($event)
{
case 'ticket_created':
// Notify assigned user if any
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'ticket_replied':
// Notify ticket creator (customer gets notified of staff reply)
if (!empty($ticket->created_by))
{
$email = self::getUserEmail((int) $ticket->created_by);
if ($email)
{
$emails[] = $email;
}
}
// Notify assigned user
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'status_changed':
// Notify ticket creator
if (!empty($ticket->created_by))
{
$email = self::getUserEmail((int) $ticket->created_by);
if ($email)
{
$emails[] = $email;
}
}
break;
case 'ticket_assigned':
// Notify newly assigned user
if (!empty($ticket->assigned_to))
{
$email = self::getUserEmail((int) $ticket->assigned_to);
if ($email)
{
$emails[] = $email;
}
}
break;
}
return array_unique($emails);
}
/**
* Build email subject line.
*/
private static function buildSubject(string $event, object $ticket): string
{
$siteName = Factory::getConfig()->get('sitename', 'Support');
$prefix = '[' . $siteName . ' #' . $ticket->id . '] ';
switch ($event)
{
case 'ticket_created':
return $prefix . 'New Ticket: ' . ($ticket->subject ?? '');
case 'ticket_replied':
return $prefix . 'Reply: ' . ($ticket->subject ?? '');
case 'status_changed':
return $prefix . 'Status Changed: ' . ($ticket->subject ?? '');
case 'ticket_assigned':
return $prefix . 'Assigned: ' . ($ticket->subject ?? '');
default:
return $prefix . ($ticket->subject ?? '');
}
}
/**
* Build email body.
*/
private static function buildBody(string $event, object $ticket, array $extra): string
{
$siteName = Factory::getConfig()->get('sitename', 'Support');
$siteUrl = rtrim(Uri::root(), '/');
$ticketUrl = $siteUrl . '/index.php?option=com_mokowaas&view=ticket&id=' . $ticket->id;
$lines = [];
$lines[] = $siteName . ' Support';
$lines[] = str_repeat('-', 40);
$lines[] = '';
switch ($event)
{
case 'ticket_created':
$lines[] = 'A new support ticket has been created.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
$lines[] = 'Category: ' . ($ticket->category_title ?? 'General');
$lines[] = '';
if (!empty($ticket->body))
{
$lines[] = 'Description:';
$lines[] = strip_tags($ticket->body);
$lines[] = '';
}
break;
case 'ticket_replied':
$lines[] = 'A new reply has been added to your ticket.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
$lines[] = '';
if (!empty($extra['reply_body']))
{
$lines[] = 'Reply:';
$lines[] = strip_tags($extra['reply_body']);
$lines[] = '';
}
break;
case 'status_changed':
$lines[] = 'Your ticket status has been updated.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'New Status: ' . ucwords(str_replace('_', ' ', $ticket->status ?? ''));
if (!empty($extra['old_status']))
{
$lines[] = 'Old Status: ' . ucwords(str_replace('_', ' ', $extra['old_status']));
}
$lines[] = '';
break;
case 'ticket_assigned':
$lines[] = 'A ticket has been assigned to you.';
$lines[] = '';
$lines[] = 'Subject: ' . ($ticket->subject ?? '');
$lines[] = 'Priority: ' . ucfirst($ticket->priority ?? 'normal');
$lines[] = '';
break;
}
$lines[] = 'View ticket: ' . $ticketUrl;
$lines[] = '';
$lines[] = '-- ';
$lines[] = $siteName . ' | Powered by MokoWaaS';
return implode("\n", $lines);
}
/**
* Get email address for a Joomla user ID.
*/
private static function getUserEmail(int $userId): ?string
{
if ($userId <= 0)
{
return null;
}
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('email'))
->from($db->quoteName('#__users'))
->where($db->quoteName('id') . ' = ' . $userId)
);
return $db->loadResult() ?: null;
}
catch (\Throwable $e)
{
return null;
}
}
/**
* Get notification configuration from component params.
*/
private static function getNotificationConfig(): array
{
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = json_decode($db->loadResult() ?? '{}', true);
return $params['notifications'] ?? [];
}
catch (\Throwable $e)
{
return [];
}
}
// ==================================================================
// Security Event Notifications (#131)
// ==================================================================
/**
* Send a security alert to admin emails.
*/
public static function securityAlert(string $event, string $subject, string $body): void
{
try
{
$config = self::getNotificationConfig();
$enabled = $config['security_alerts'] ?? '1';
if (!$enabled)
{
return;
}
$adminEmails = array_filter(array_map('trim', explode(',', $config['admin_emails'] ?? '')));
$adminUserIds = array_filter(array_map('intval', explode(',', $config['admin_user_ids'] ?? '')));
$recipients = $adminEmails;
foreach ($adminUserIds as $uid)
{
$email = self::getUserEmail($uid);
if ($email)
{
$recipients[] = $email;
}
}
$recipients = array_unique($recipients);
if (empty($recipients))
{
return;
}
$siteName = Factory::getConfig()->get('sitename', 'Site');
$fullSubject = '[' . $siteName . ' Security] ' . $subject;
$lines = [
$siteName . ' Security Alert',
str_repeat('-', 40),
'',
'Event: ' . $event,
'Time: ' . gmdate('Y-m-d H:i:s') . ' UTC',
'',
$body,
'',
'-- ',
$siteName . ' | MokoWaaS Security',
];
$mailer = Factory::getMailer();
$mailer->isHtml(false);
$mailer->setSubject($fullSubject);
$mailer->setBody(implode("\n", $lines));
foreach ($recipients as $email)
{
try
{
$mailer->clearAddresses();
$mailer->addRecipient(trim($email));
$mailer->Send();
}
catch (\Throwable $e)
{
Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
catch (\Throwable $e)
{
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
}
}
}
@@ -0,0 +1,27 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Automation;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $rules = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\TicketsModel();
$this->rules = $model->getAutomationRules();
ToolbarHelper::title('Automation Rules', 'cogs');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,33 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Canned;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $responses = [];
protected $categories = [];
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT * FROM #__mokowaas_ticket_canned ORDER BY ordering ASC');
$this->responses = $db->loadObjectList() ?: [];
$db->setQuery('SELECT id, title FROM #__mokowaas_ticket_categories WHERE published = 1 ORDER BY ordering');
$this->categories = $db->loadObjectList() ?: [];
ToolbarHelper::title('Canned Responses', 'comment');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,41 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Categories;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $categories = [];
protected $users = [];
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT * FROM #__mokowaas_ticket_categories ORDER BY ordering ASC');
$this->categories = $db->loadObjectList() ?: [];
// Get admin users for auto-assign dropdown
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('name')])
->from($db->quoteName('#__users'))
->where($db->quoteName('block') . ' = 0')
->order($db->quoteName('name') . ' ASC')
->setLimit(100)
);
$this->users = $db->loadObjectList() ?: [];
ToolbarHelper::title('Ticket Categories', 'folder');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,27 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Cleanup;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $dirs = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->dirs = $model->getCleanupInfo();
ToolbarHelper::title('Cache &amp; Temp Cleanup', 'trash');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -23,6 +23,9 @@ class HtmlView extends BaseHtmlView
protected $pendingUpdates = []; protected $pendingUpdates = [];
protected $checkedOutItems = []; protected $checkedOutItems = [];
protected $wafBlocks = []; protected $wafBlocks = [];
protected $wafChartData = [];
protected $loginChartData = [];
protected $mokoExtensions = [];
public function display($tpl = null) public function display($tpl = null)
{ {
@@ -34,6 +37,22 @@ class HtmlView extends BaseHtmlView
$this->pendingUpdates = $model->getPendingUpdates(); $this->pendingUpdates = $model->getPendingUpdates();
$this->checkedOutItems = $model->getCheckedOutItems(); $this->checkedOutItems = $model->getCheckedOutItems();
$this->wafBlocks = $model->getRecentWafBlocks(5); $this->wafBlocks = $model->getRecentWafBlocks(5);
$this->wafChartData = $model->getWafBlocksByDay(14);
$this->loginChartData = $model->getLoginsByDay(14);
$this->mokoExtensions = $model->getMokoExtensions();
// Check for importable Akeeba data
try
{
$importModel = new \Moko\Component\MokoWaaS\Administrator\Model\ImportModel();
$this->adminToolsAvailable = $importModel->checkAdminToolsAvailable();
$this->atsAvailable = $importModel->checkAtsAvailable();
}
catch (\Throwable $e)
{
$this->adminToolsAvailable = null;
$this->atsAvailable = null;
}
$this->addToolbar(); $this->addToolbar();
@@ -0,0 +1,27 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Database;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $tableData = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
$this->tableData = $model->getTableStatus();
ToolbarHelper::title('Database Tools', 'database');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\View\Htaccess;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $options = [];
protected $preview = '';
protected $nginxPreview = '';
protected $currentHtaccess = '';
public function display($tpl = null)
{
$model = $this->getModel();
$this->options = $model->getOptions();
$this->preview = $model->generateHtaccess($this->options);
$this->nginxPreview = $model->generateNginx($this->options);
$this->currentHtaccess = $model->readCurrentHtaccess();
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_MOKOWAAS_HTACCESS_TITLE'), 'file-code');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
}
}
@@ -0,0 +1,39 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Privacy;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $requests = [];
protected $policies = [];
protected $summary;
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$filterStatus = Factory::getApplication()->getInput()->getString('filter_status', '');
$this->requests = $model->getDataRequests($filterStatus);
$this->policies = $model->getRetentionPolicies();
$this->summary = $model->getDashboardSummary();
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Privacy Guard', 'lock');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\View\Ticket;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $ticket;
protected $cannedResponses = [];
public function display($tpl = null)
{
$model = $this->getModel('Tickets');
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->ticket = $model->getTicket($id);
$this->cannedResponses = $model->getCannedResponses((int) ($this->ticket->category_id ?? 0));
if (!$this->ticket)
{
Factory::getApplication()->enqueueMessage('Ticket not found.', 'error');
Factory::getApplication()->redirect('index.php?option=com_mokowaas&view=tickets');
return;
}
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
$title = $this->ticket ? 'Ticket #' . $this->ticket->id . ' — ' . $this->ticket->subject : 'Ticket';
ToolbarHelper::title($title, 'headphones');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
}
}
@@ -0,0 +1,56 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Administrator\View\Tickets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $tickets = [];
protected $categories = [];
protected $statusCounts;
protected $overdue = [];
protected $atsAvailable = null;
public function display($tpl = null)
{
$model = $this->getModel();
$app = Factory::getApplication();
$filters = [
'status' => $app->getInput()->getString('filter_status', ''),
'priority' => $app->getInput()->getString('filter_priority', ''),
'category_id' => $app->getInput()->getInt('filter_category', 0),
];
$this->tickets = $model->getTickets($filters);
$this->categories = $model->getCategories();
$this->statusCounts = $model->getStatusCounts();
$this->overdue = $model->getOverdueTickets();
$this->atsAvailable = $model->checkAtsAvailable();
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_MOKOWAAS_TICKETS_TITLE'), 'headphones');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
}
}
@@ -0,0 +1,55 @@
<?php
namespace Moko\Component\MokoWaaS\Administrator\View\Waflog;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
{
protected $logs = [];
protected $ruleCounts = [];
protected $topIps = [];
protected $ruleNames = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'rule' => $input->getString('filter_rule', ''),
'ip' => $input->getString('filter_ip', ''),
'search' => $input->getString('filter_search', ''),
'date_from' => $input->getString('filter_date_from', ''),
'date_to' => $input->getString('filter_date_to', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->logs = $model->getLogs($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->ruleCounts = $model->getRuleCounts();
$this->topIps = $model->getTopIps(10);
$this->ruleNames = $model->getRuleNames();
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('WAF Log Viewer', 'shield-alt');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
}
}
@@ -0,0 +1,141 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$rules = $this->rules;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveAutomation&format=json');
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteAutomation&format=json');
$toggleUrl = Route::_('index.php?option=com_mokowaas&task=display.toggleAutomation&format=json');
$triggerLabels = ['ticket_created' => 'On Ticket Created', 'ticket_replied' => 'On Reply', 'status_changed' => 'On Status Change', 'scheduled' => 'Scheduled (Cron)'];
?>
<div id="mokowaas-automation">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($rules); ?> Automation Rules</h4>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newRuleModal">
<span class="icon-plus"></span> Add Rule
</button>
</div>
<?php foreach ($rules as $r): ?>
<?php $conditions = json_decode($r->conditions, true) ?: []; $actions = json_decode($r->actions, true) ?: []; ?>
<div class="card mb-2 <?php echo !$r->enabled ? 'opacity-50' : ''; ?>" data-id="<?php echo $r->id; ?>">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="d-flex align-items-center gap-2">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input rule-toggle" data-id="<?php echo $r->id; ?>" <?php echo $r->enabled ? 'checked' : ''; ?>>
</div>
<strong><?php echo htmlspecialchars($r->title); ?></strong>
<span class="badge bg-secondary"><?php echo $triggerLabels[$r->trigger_event] ?? $r->trigger_event; ?></span>
</div>
<div class="small text-muted mt-1">
<span class="text-primary">IF</span>
<?php foreach ($conditions as $i => $c): ?>
<?php echo $i > 0 ? ' AND ' : ''; ?><?php echo htmlspecialchars($c['field'] ?? ''); ?> <?php echo htmlspecialchars($c['op'] ?? ''); ?> <?php echo htmlspecialchars($c['value'] ?? ''); ?>
<?php endforeach; ?>
<span class="text-success ms-2">THEN</span>
<?php foreach ($actions as $a): ?>
<?php echo htmlspecialchars($a['type'] ?? ''); ?>=<?php echo htmlspecialchars(mb_substr($a['value'] ?? '', 0, 30)); ?>
<?php endforeach; ?>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-rule" data-id="<?php echo $r->id; ?>">
<span class="icon-trash"></span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($rules)): ?>
<div class="alert alert-info">No automation rules. Click "Add Rule" to create one.</div>
<?php endif; ?>
</div>
<!-- New Rule Modal -->
<div class="modal fade" id="newRuleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"><h5>Add Automation Rule</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" id="rule-title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Trigger</label>
<select id="rule-trigger" class="form-select">
<?php foreach ($triggerLabels as $k => $v): ?><option value="<?php echo $k; ?>"><?php echo $v; ?></option><?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Conditions (JSON)</label>
<textarea id="rule-conditions" class="form-control font-monospace" rows="3" placeholder='[{"field":"status","op":"eq","value":"resolved"}]'></textarea>
<small class="text-muted">Fields: status, priority, category_id, assigned_to, sla_responded, age_hours. Ops: eq, neq, gt, lt, in, not_in</small>
</div>
<div class="mb-3">
<label class="form-label">Actions (JSON)</label>
<textarea id="rule-actions" class="form-control font-monospace" rows="3" placeholder='[{"type":"set_status","value":"closed"}]'></textarea>
<small class="text-muted">Types: set_status, set_priority, assign, add_note, send_email</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-save-rule"><span class="icon-save"></span> Save Rule</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
// Save new rule
document.getElementById('btn-save-rule').addEventListener('click', function() {
var fd = new FormData();
fd.append('id', '0');
fd.append('title', document.getElementById('rule-title').value);
fd.append('trigger_event', document.getElementById('rule-trigger').value);
fd.append('conditions', document.getElementById('rule-conditions').value || '[]');
fd.append('actions', document.getElementById('rule-actions').value || '[]');
fd.append(token, '1');
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (d.success) location.reload(); else Joomla.renderMessages({error:[d.message]}); });
});
// Toggle rule
document.querySelectorAll('.rule-toggle').forEach(function(cb) {
cb.addEventListener('change', function() {
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append('enabled', this.checked ? '1' : '0');
fd.append(token, '1');
fetch('<?php echo $toggleUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (!d.success) Joomla.renderMessages({error:[d.message]}); });
});
});
// Delete rule
document.querySelectorAll('.btn-delete-rule').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this rule?')) return;
var card = this.closest('.card');
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append(token, '1');
fetch('<?php echo $deleteUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (d.success) card.remove(); else Joomla.renderMessages({error:[d.message]}); });
});
});
});
</script>
@@ -0,0 +1,107 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$responses = $this->responses;
$categories = $this->categories;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveCanned&format=json');
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCanned&format=json');
?>
<div id="mokowaas-canned">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($responses); ?> Canned Responses</h4>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newCannedModal">
<span class="icon-plus"></span> Add Response
</button>
</div>
<?php foreach ($responses as $r): ?>
<div class="card mb-2" data-id="<?php echo $r->id; ?>">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong><?php echo htmlspecialchars($r->title); ?></strong>
<p class="text-muted small mb-0 mt-1"><?php echo htmlspecialchars(mb_substr($r->body, 0, 150)); ?></p>
</div>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-canned" data-id="<?php echo $r->id; ?>">
<span class="icon-trash"></span>
</button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($responses)): ?>
<div class="alert alert-info">No canned responses yet. Click "Add Response" to create one.</div>
<?php endif; ?>
</div>
<!-- New Canned Modal -->
<div class="modal fade" id="newCannedModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"><h5>Add Canned Response</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" id="canned-title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Category (optional)</label>
<select id="canned-category" class="form-select">
<option value="">All categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo htmlspecialchars($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Response Text</label>
<textarea id="canned-body" class="form-control" rows="6" required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-save-canned"><span class="icon-save"></span> Save</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
document.getElementById('btn-save-canned').addEventListener('click', function() {
var fd = new FormData();
fd.append('id', '0');
fd.append('title', document.getElementById('canned-title').value);
fd.append('body', document.getElementById('canned-body').value);
fd.append('category_id', document.getElementById('canned-category').value);
fd.append(token, '1');
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) location.reload();
else Joomla.renderMessages({error:[d.message]});
});
});
document.querySelectorAll('.btn-delete-canned').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this canned response?')) return;
var card = this.closest('.card');
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append(token, '1');
fetch('<?php echo $deleteUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if (d.success) card.remove(); else Joomla.renderMessages({error:[d.message]}); });
});
});
});
</script>
@@ -0,0 +1,126 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$categories = $this->categories;
$users = $this->users;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveCategory&format=json');
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCategory&format=json');
?>
<div id="mokowaas-categories">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4><?php echo count($categories); ?> Categories</h4>
<button type="button" class="btn btn-primary btn-sm" id="btn-add-cat">
<span class="icon-plus"></span> Add Category
</button>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-striped mb-0" id="cat-table">
<thead><tr><th>Title</th><th>SLA Response</th><th>SLA Resolution</th><th>Auto-Assign</th><th>Active</th><th></th></tr></thead>
<tbody>
<?php foreach ($categories as $c): ?>
<tr data-id="<?php echo $c->id; ?>">
<td><input type="text" class="form-control form-control-sm cat-field" data-field="title" value="<?php echo htmlspecialchars($c->title); ?>"></td>
<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_response_minutes" value="<?php echo $c->sla_response_minutes; ?>" style="width:80px"> min</td>
<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_resolution_minutes" value="<?php echo $c->sla_resolution_minutes; ?>" style="width:80px"> min</td>
<td>
<select class="form-select form-select-sm cat-field" data-field="auto_assign_user">
<option value="">None</option>
<?php foreach ($users as $u): ?>
<option value="<?php echo $u->id; ?>" <?php echo (int)$c->auto_assign_user === (int)$u->id ? 'selected' : ''; ?>><?php echo htmlspecialchars($u->name); ?></option>
<?php endforeach; ?>
</select>
</td>
<td>
<input type="checkbox" class="form-check-input cat-field" data-field="published" <?php echo $c->published ? 'checked' : ''; ?>>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-success btn-save-cat" title="Save"><span class="icon-save"></span></button>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-cat" title="Delete"><span class="icon-trash"></span></button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
// Save category
document.querySelectorAll('.btn-save-cat').forEach(function(btn) {
btn.addEventListener('click', function() {
var row = this.closest('tr');
var id = row.dataset.id || '0';
var fd = new FormData();
fd.append('id', id);
fd.append(token, '1');
row.querySelectorAll('.cat-field').forEach(function(f) {
fd.append(f.dataset.field, f.type === 'checkbox' ? (f.checked ? '1' : '0') : f.value);
});
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); if (d.id && id === '0') row.dataset.id = d.id; }
else Joomla.renderMessages({error:[d.message]});
});
});
});
// Delete category
document.querySelectorAll('.btn-delete-cat').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this category?')) return;
var row = this.closest('tr');
var fd = new FormData();
fd.append('id', row.dataset.id);
fd.append(token, '1');
fetch('<?php echo $deleteUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) row.remove();
else Joomla.renderMessages({error:[d.message]});
});
});
});
// Add new row
document.getElementById('btn-add-cat').addEventListener('click', function() {
var tbody = document.querySelector('#cat-table tbody');
var tr = document.createElement('tr');
tr.dataset.id = '0';
tr.innerHTML = '<td><input type="text" class="form-control form-control-sm cat-field" data-field="title" value=""></td>'
+ '<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_response_minutes" value="480" style="width:80px"> min</td>'
+ '<td><input type="number" class="form-control form-control-sm cat-field" data-field="sla_resolution_minutes" value="2880" style="width:80px"> min</td>'
+ '<td><select class="form-select form-select-sm cat-field" data-field="auto_assign_user"><option value="">None</option><?php foreach ($users as $u): ?><option value="<?php echo $u->id; ?>"><?php echo htmlspecialchars($u->name); ?></option><?php endforeach; ?></select></td>'
+ '<td><input type="checkbox" class="form-check-input cat-field" data-field="published" checked></td>'
+ '<td><button type="button" class="btn btn-sm btn-outline-success btn-save-cat"><span class="icon-save"></span></button></td>';
tbody.appendChild(tr);
tr.querySelector('.btn-save-cat').addEventListener('click', function() {
var row = this.closest('tr');
var fd = new FormData();
fd.append('id', '0');
fd.append(token, '1');
row.querySelectorAll('.cat-field').forEach(function(f) {
fd.append(f.dataset.field, f.type === 'checkbox' ? (f.checked ? '1' : '0') : f.value);
});
fetch('<?php echo $saveUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else Joomla.renderMessages({error:[d.message]});
});
});
tr.querySelector('input').focus();
});
});
</script>
@@ -0,0 +1,63 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$dirs = $this->dirs;
$token = Session::getFormToken();
$cleanUrl = Route::_('index.php?option=com_mokowaas&task=display.cleanDirectory&format=json');
$dirKeys = ['site_cache', 'admin_cache', 'tmp', 'logs'];
$totalMb = 0;
$totalFiles = 0;
foreach ($dirs as $d) { $totalMb += $d->size_mb; $totalFiles += $d->files; }
?>
<div id="mokowaas-cleanup">
<div class="row g-3 mb-4">
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalMb, 1); ?> MB</span><small class="text-muted">Total Size</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo number_format($totalFiles); ?></span><small class="text-muted">Total Files</small></div></div>
</div>
<div class="row g-3">
<?php foreach ($dirs as $i => $d): ?>
<div class="col-12 col-md-6 col-xl-3">
<div class="card h-100">
<div class="card-body text-center">
<h5><?php echo htmlspecialchars($d->label); ?></h5>
<p class="fs-3 fw-bold mb-1 <?php echo $d->size_mb > 50 ? 'text-warning' : ''; ?>"><?php echo number_format($d->size_mb, 1); ?> MB</p>
<p class="text-muted small"><?php echo number_format($d->files); ?> files</p>
<?php if (!$d->writable): ?>
<span class="badge bg-danger">Not writable</span>
<?php else: ?>
<button type="button" class="btn btn-outline-danger btn-clean" data-key="<?php echo $dirKeys[$i] ?? ''; ?>" data-label="<?php echo htmlspecialchars($d->label); ?>">
<span class="icon-trash"></span> Clean
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<script>
document.querySelectorAll('.btn-clean').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Clean all files in ' + this.dataset.label + '?')) return;
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append('dir_key', el.dataset.key);
fd.append('<?php echo $token; ?>', '1');
fetch('<?php echo $cleanUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); setTimeout(function(){location.reload()},1500); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
})
.catch(function(){ el.disabled = false; });
});
});
</script>
@@ -19,6 +19,9 @@ $siteInfo = $this->siteInfo;
$plugins = $this->plugins; $plugins = $this->plugins;
$recentLogins = $this->recentLogins; $recentLogins = $this->recentLogins;
$pendingUpdates = $this->pendingUpdates; $pendingUpdates = $this->pendingUpdates;
$mokoExts = $this->mokoExtensions;
$adminToolsAvail = $this->adminToolsAvailable ?? null;
$atsAvail = $this->atsAvailable ?? null;
$checkedOut = $this->checkedOutItems; $checkedOut = $this->checkedOutItems;
$wafBlocks = $this->wafBlocks; $wafBlocks = $this->wafBlocks;
$token = Session::getFormToken(); $token = Session::getFormToken();
@@ -63,29 +66,118 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<?php if ($siteInfo->offline): ?> <?php if ($siteInfo->offline): ?>
<span class="badge bg-danger"><?php echo Text::_('COM_MOKOWAAS_OFFLINE'); ?></span> <span class="badge bg-danger"><?php echo Text::_('COM_MOKOWAAS_OFFLINE'); ?></span>
<?php endif; ?> <?php endif; ?>
<div class="mokowaas-info-item ms-auto">
<span class="icon-globe" aria-hidden="true"></span>
<code><?php echo $this->escape($_SERVER['REMOTE_ADDR'] ?? ''); ?></code>
</div>
</div> </div>
</div> </div>
<?php if (!empty($mokoExts)): ?>
<!-- Moko Component & Module Versions -->
<div class="d-flex flex-wrap gap-2 mb-4">
<?php
$extIcons = [
'com_mokowaas' => 'icon-cogs',
'mod_mokowaas_cpanel' => 'icon-tachometer-alt',
'mod_mokowaas_menu' => 'icon-bars',
'mod_mokowaas_cache' => 'icon-bolt',
'mod_mokowaas_categories' => 'icon-folder',
];
foreach ($mokoExts as $ext):
$icon = $extIcons[$ext->element] ?? 'icon-puzzle-piece';
$label = str_replace(['mod_mokowaas_', 'com_mokowaas'], ['', 'Component'], $ext->element);
$label = ucfirst($label ?: 'Component');
?>
<div class="d-flex align-items-center gap-2 px-3 py-2 rounded border bg-white" style="font-size:0.85rem;">
<span class="<?php echo $icon; ?>" aria-hidden="true" style="color:#1a2744;"></span>
<span><?php echo $this->escape($label); ?></span>
<span class="badge bg-light text-dark"><?php echo $this->escape($ext->version); ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($adminToolsAvail || $atsAvail): ?>
<!-- Akeeba Import Banner -->
<div class="alert alert-info d-flex flex-wrap align-items-center gap-3 mb-4">
<span class="icon-info-circle" style="font-size:1.25rem"></span>
<strong>Akeeba data detected import into MokoWaaS:</strong>
<?php if ($adminToolsAvail): ?>
<button type="button" class="btn btn-sm btn-info" id="btn-import-admintools"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.importAdminTools&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-shield-alt"></span> Import Admin Tools Settings
</button>
<?php endif; ?>
<?php if ($atsAvail): ?>
<button type="button" class="btn btn-sm btn-info" id="btn-import-ats-dash"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.importAts&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-headphones"></span> Import Tickets (<?php echo $atsAvail->tickets; ?> tickets)
</button>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Quick Actions (large buttons) --> <!-- Quick Actions (large buttons) -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-12 col-md-4"> <div class="col-6 col-md-4 col-xl-3">
<button type="button" class="btn btn-outline-primary w-100 py-3" id="mokowaas-btn-cache" <button type="button" class="btn btn-outline-primary w-100 py-3" id="mokowaas-btn-cache"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.clearCache&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.clearCache&format=json'); ?>"
data-token="<?php echo $token; ?>"> data-token="<?php echo $token; ?>">
<span class="icon-trash d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span> <span class="icon-bolt d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo Text::_('COM_MOKOWAAS_CLEAR_CACHE'); ?> Clear Cache
</button> </button>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="btn btn-outline-primary w-100 py-3"> <a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="btn btn-outline-primary w-100 py-3">
<span class="icon-refresh d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span> <span class="icon-refresh d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo Text::_('COM_MOKOWAAS_CHECK_UPDATES'); ?> Check Updates
</a> </a>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=extensions'); ?>" class="btn btn-outline-primary w-100 py-3"> <a href="<?php echo Route::_('index.php?option=com_mokowaas&view=extensions'); ?>" class="btn btn-outline-primary w-100 py-3">
<span class="icon-puzzle-piece d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span> <span class="icon-puzzle-piece d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo Text::_('COM_MOKOWAAS_EXTENSIONS_LINK'); ?> Moko Extensions
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-check-square d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Global Check-in
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_actionlogs'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-list d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
View Logs
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_scheduler'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-clock d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Scheduled Tasks
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<?php
// Use MokoJoomCommunity if available, otherwise Joomla user manager
$useCB = file_exists(JPATH_ADMINISTRATOR . '/components/com_comprofiler/comprofiler.php');
$userUrl = $useCB
? Route::_('index.php?option=com_comprofiler&task=showusers')
: Route::_('index.php?option=com_users');
$userLabel = $useCB ? 'MokoJoomCommunity' : 'User Manager';
?>
<a href="<?php echo $userUrl; ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-users d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
<?php echo $userLabel; ?>
</a>
</div>
<div class="col-6 col-md-4 col-xl-3">
<a href="<?php echo Route::_('index.php?option=com_redirect'); ?>" class="btn btn-outline-secondary w-100 py-3">
<span class="icon-arrow-right d-block mb-1" aria-hidden="true" style="font-size:1.5rem"></span>
Redirects
</a> </a>
</div> </div>
</div> </div>
@@ -118,10 +210,14 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<span class="badge bg-light text-dark"><?php echo $this->escape($plugin->version); ?></span> <span class="badge bg-light text-dark"><?php echo $this->escape($plugin->version); ?></span>
<?php endif; ?> <?php endif; ?>
</div> </div>
<p class="card-text text-muted small flex-grow-1"><?php echo $this->escape($plugin->description); ?></p> <p class="card-text text-muted text-muted flex-grow-1"><?php echo $this->escape($plugin->description); ?></p>
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top"> <div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
<?php if ($plugin->protected): ?> <?php if ($plugin->protected): ?>
<span class="badge bg-dark"><?php echo Text::_('COM_MOKOWAAS_PROTECTED'); ?></span> <span class="badge bg-dark"><?php echo Text::_('COM_MOKOWAAS_PROTECTED'); ?></span>
<?php elseif ($plugin->configure_only): ?>
<span class="badge bg-<?php echo $plugin->enabled ? 'success' : 'secondary'; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOWAAS_ENABLED') : Text::_('COM_MOKOWAAS_DISABLED'); ?>
</span>
<?php else: ?> <?php else: ?>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input type="checkbox" class="form-check-input mokowaas-toggle" role="switch" <input type="checkbox" class="form-check-input mokowaas-toggle" role="switch"
@@ -130,7 +226,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.togglePlugin&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.togglePlugin&format=json'); ?>"
data-token="<?php echo $token; ?>" data-token="<?php echo $token; ?>"
<?php echo $plugin->enabled ? 'checked' : ''; ?>> <?php echo $plugin->enabled ? 'checked' : ''; ?>>
<label class="form-check-label small" for="toggle-<?php echo $plugin->extension_id; ?>"> <label class="form-check-label" for="toggle-<?php echo $plugin->extension_id; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOWAAS_ENABLED') : Text::_('COM_MOKOWAAS_DISABLED'); ?> <?php echo $plugin->enabled ? Text::_('COM_MOKOWAAS_ENABLED') : Text::_('COM_MOKOWAAS_DISABLED'); ?>
</label> </label>
</div> </div>
@@ -149,8 +245,28 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<!-- Right: Information Tables (4 cols) --> <!-- Right: Charts & Information (4 cols) -->
<div class="col-12 col-xl-4"> <div class="col-12 col-xl-4" style="border-left:1px solid var(--gray-300, #dee2e6);padding-left:1.5rem;">
<!-- WAF Activity Chart -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-shield-alt" aria-hidden="true"></span> WAF Activity (14 days)</strong>
</div>
<div class="card-body py-2">
<canvas id="mokowaas-chart-waf" height="140"></canvas>
</div>
</div>
<!-- Login Activity Chart -->
<div class="card mb-3">
<div class="card-header">
<strong><span class="icon-user" aria-hidden="true"></span> Login Activity (14 days)</strong>
</div>
<div class="card-body py-2">
<canvas id="mokowaas-chart-logins" height="140"></canvas>
</div>
</div>
<!-- Pending Updates --> <!-- Pending Updates -->
<div class="card mb-3"> <div class="card mb-3">
@@ -165,16 +281,16 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody> <tbody>
<?php foreach ($pendingUpdates as $upd): ?> <?php foreach ($pendingUpdates as $upd): ?>
<tr> <tr>
<td class="small"><?php echo $this->escape($upd->name); ?></td> <td class="text-muted"><?php echo $this->escape($upd->name); ?></td>
<td class="small text-muted"><?php echo $this->escape($upd->current_version); ?></td> <td class="text-muted"><?php echo $this->escape($upd->current_version); ?></td>
<td class="small text-success"><?php echo $this->escape($upd->version); ?></td> <td class="text-success fw-bold"><?php echo $this->escape($upd->version); ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="card-body text-center text-muted small py-3"> <div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> All extensions up to date <span class="icon-check-circle text-success"></span> All extensions up to date
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -193,19 +309,19 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody> <tbody>
<?php foreach ($checkedOut as $item): ?> <?php foreach ($checkedOut as $item): ?>
<tr> <tr>
<td class="small"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td> <td class="text-muted"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td>
<td class="small"><?php echo $this->escape($item->username ?? ''); ?></td> <td class="text-muted"><?php echo $this->escape($item->username ?? ''); ?></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, 'M d H:i'); ?></td> <td class="text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, 'M d H:i'); ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="card-footer text-center py-1"> <div class="card-footer text-center py-1">
<a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="small">Global Check-in</a> <a href="<?php echo Route::_('index.php?option=com_checkin'); ?>" class="text-muted">Global Check-in</a>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="card-body text-center text-muted small py-3"> <div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> No checked out items <span class="icon-check-circle text-success"></span> No checked out items
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -224,16 +340,16 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody> <tbody>
<?php foreach ($wafBlocks as $block): ?> <?php foreach ($wafBlocks as $block): ?>
<tr> <tr>
<td class="small"><code><?php echo $this->escape($block->ip); ?></code></td> <td class="text-muted"><code><?php echo $this->escape($block->ip); ?></code></td>
<td class="small"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td> <td class="text-muted"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $block->created, 'M d H:i'); ?></td> <td class="text-muted"><?php echo HTMLHelper::_('date', $block->created, 'M d H:i'); ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="card-body text-center text-muted small py-3"> <div class="card-body text-center text-muted py-3">
<span class="icon-check-circle text-success"></span> No recent blocks <span class="icon-check-circle text-success"></span> No recent blocks
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -251,19 +367,85 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
<tbody> <tbody>
<?php foreach ($recentLogins as $login): ?> <?php foreach ($recentLogins as $login): ?>
<tr> <tr>
<td class="small"><?php echo $this->escape($login->username ?? ''); ?></td> <td class="text-muted"><?php echo $this->escape($login->username ?? ''); ?></td>
<td class="small"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td> <td class="text-muted"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td>
<td class="small text-muted"><?php echo HTMLHelper::_('date', $login->log_date, 'M d H:i'); ?></td> <td class="text-muted"><?php echo HTMLHelper::_('date', $login->log_date, 'M d H:i'); ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
<?php else: ?> <?php else: ?>
<div class="card-body text-center text-muted small py-3">No login activity recorded</div> <div class="card-body text-center text-muted py-3">No login activity recorded</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div><!-- /.col-xl-4 --> </div><!-- /.col-xl-4 -->
</div><!-- /.row --> </div><!-- /.row -->
</div> </div>
<?php
// Prepare chart data as JSON for JavaScript
$wafChartData = $this->wafChartData ?? [];
$loginChartData = $this->loginChartData ?? [];
$wafLabels = array_map(fn($d) => $d->day, $wafChartData);
$wafValues = array_map(fn($d) => $d->total, $wafChartData);
$loginLabels = array_map(fn($d) => $d->day, $loginChartData);
$loginValues = array_map(fn($d) => $d->total, $loginChartData);
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var chartDefaults = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { display: false }, ticks: { maxRotation: 45, font: { size: 10 } } },
y: { beginAtZero: true, ticks: { stepSize: 1, font: { size: 10 } } }
}
};
// WAF chart
var wafCtx = document.getElementById('mokowaas-chart-waf');
if (wafCtx) {
new Chart(wafCtx, {
type: 'bar',
data: {
labels: <?php echo json_encode($wafLabels); ?>,
datasets: [{
data: <?php echo json_encode($wafValues); ?>,
backgroundColor: 'rgba(197, 40, 39, 0.6)',
borderColor: '#c52827',
borderWidth: 1,
borderRadius: 3
}]
},
options: chartDefaults
});
}
// Login chart
var loginCtx = document.getElementById('mokowaas-chart-logins');
if (loginCtx) {
new Chart(loginCtx, {
type: 'line',
data: {
labels: <?php echo json_encode($loginLabels); ?>,
datasets: [{
data: <?php echo json_encode($loginValues); ?>,
borderColor: '#2a69b8',
backgroundColor: 'rgba(42, 105, 184, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 3,
pointBackgroundColor: '#2a69b8'
}]
},
options: chartDefaults
});
}
});
</script>
@@ -0,0 +1,72 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$data = $this->tableData;
$tables = $data['tables'] ?? [];
$token = Session::getFormToken();
$optimizeUrl = Route::_('index.php?option=com_mokowaas&task=display.optimizeDb&format=json');
$repairUrl = Route::_('index.php?option=com_mokowaas&task=display.repairDb&format=json');
$purgeUrl = Route::_('index.php?option=com_mokowaas&task=display.purgeSessions&format=json');
?>
<div id="mokowaas-database">
<div class="row g-3 mb-4">
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['count']; ?></span><small class="text-muted">Tables</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3"><?php echo $data['total_size_mb']; ?> MB</span><small class="text-muted">Total Size</small></div></div>
<div class="col-6 col-md-3"><div class="card text-center p-3"><span class="fw-bold fs-3 <?php echo $data['total_overhead_kb'] > 100 ? 'text-warning' : 'text-success'; ?>"><?php echo $data['total_overhead_kb']; ?> KB</span><small class="text-muted">Overhead</small></div></div>
<div class="col-6 col-md-3">
<div class="card p-3 d-grid gap-2">
<button type="button" class="btn btn-sm btn-primary btn-db-action" data-url="<?php echo $optimizeUrl; ?>" data-token="<?php echo $token; ?>" data-confirm="Optimize all tables with overhead?">
<span class="icon-bolt"></span> Optimize All
</button>
<button type="button" class="btn btn-sm btn-outline-warning btn-db-action" data-url="<?php echo $repairUrl; ?>" data-token="<?php echo $token; ?>" data-confirm="Repair all tables?">
<span class="icon-wrench"></span> Repair All
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-db-action" data-url="<?php echo $purgeUrl; ?>" data-token="<?php echo $token; ?>" data-confirm="Purge expired sessions?">
<span class="icon-trash"></span> Purge Sessions
</button>
</div>
</div>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-striped table-sm mb-0">
<thead><tr><th>Table</th><th>Engine</th><th class="text-end">Rows</th><th class="text-end">Size</th><th class="text-end">Overhead</th></tr></thead>
<tbody>
<?php foreach ($tables as $t): ?>
<tr class="<?php echo $t->overhead_kb > 10 ? 'table-warning' : ''; ?> <?php echo $t->is_moko ? 'fw-bold' : ''; ?>">
<td class="small"><?php echo htmlspecialchars($t->name); ?></td>
<td class="small"><?php echo htmlspecialchars($t->engine); ?></td>
<td class="text-end small"><?php echo number_format($t->rows); ?></td>
<td class="text-end small"><?php echo $t->size_mb; ?> MB</td>
<td class="text-end small <?php echo $t->overhead_kb > 10 ? 'text-warning fw-bold' : ''; ?>"><?php echo $t->overhead_kb > 0 ? $t->overhead_kb . ' KB' : '—'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<script>
document.querySelectorAll('.btn-db-action').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm(this.dataset.confirm)) return;
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); setTimeout(function(){location.reload()},1500); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
})
.catch(function(){ el.disabled = false; });
});
});
</script>
@@ -26,6 +26,7 @@ foreach ($packages as $pkg)
$statusBadge = [ $statusBadge = [
'installed' => ['bg-success', 'Installed'], 'installed' => ['bg-success', 'Installed'],
'update_available' => ['bg-warning text-dark', 'Update Available'],
'not_installed' => ['bg-secondary', 'Not Installed'], 'not_installed' => ['bg-secondary', 'Not Installed'],
]; ];
?> ?>
@@ -63,6 +64,9 @@ $statusBadge = [
<div class="small text-muted"> <div class="small text-muted">
<?php if ($pkg->local_version): ?> <?php if ($pkg->local_version): ?>
v<?php echo htmlspecialchars($pkg->local_version); ?> v<?php echo htmlspecialchars($pkg->local_version); ?>
<?php if ($pkg->remote_version && $pkg->status === 'update_available'): ?>
&rarr; <?php echo htmlspecialchars($pkg->remote_version); ?>
<?php endif; ?>
<?php elseif ($pkg->remote_version): ?> <?php elseif ($pkg->remote_version): ?>
Latest: <?php echo htmlspecialchars($pkg->remote_version); ?> Latest: <?php echo htmlspecialchars($pkg->remote_version); ?>
<?php endif; ?> <?php endif; ?>
@@ -73,7 +77,16 @@ $statusBadge = [
<span class="icon-book" aria-hidden="true"></span> <span class="icon-book" aria-hidden="true"></span>
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($pkg->download_url && $pkg->status === 'not_installed'): ?> <?php if ($pkg->download_url && $pkg->status === 'update_available'): ?>
<button type="button" class="btn btn-sm btn-warning mokowaas-install-btn"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>"
data-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
data-token="<?php echo $token; ?>"
data-label="<?php echo htmlspecialchars($pkg->label); ?>">
<span class="icon-refresh" aria-hidden="true"></span>
Update to <?php echo htmlspecialchars($pkg->remote_version); ?>
</button>
<?php elseif ($pkg->download_url && $pkg->status === 'not_installed'): ?>
<button type="button" class="btn btn-sm btn-primary mokowaas-install-btn" <button type="button" class="btn btn-sm btn-primary mokowaas-install-btn"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>" data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>"
data-download="<?php echo htmlspecialchars($pkg->download_url); ?>" data-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
@@ -83,6 +96,26 @@ $statusBadge = [
Install Install
</button> </button>
<?php elseif ($pkg->status === 'installed'): ?> <?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="btn btn-sm btn-outline-success disabled">
<span class="icon-check" aria-hidden="true"></span> Installed <span class="icon-check" aria-hidden="true"></span> Installed
</span> </span>
@@ -0,0 +1,306 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$opts = $this->options;
$preview = $this->preview;
$nginx = $this->nginxPreview;
$current = $this->currentHtaccess;
$token = Session::getFormToken();
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveHtaccess&format=json');
$genUrl = Route::_('index.php?option=com_mokowaas&task=display.generateHtaccess&format=json');
// Helper for toggle switch
$sw = function($name, $label, $desc = '') use ($opts) {
$checked = !empty($opts[$name]) ? 'checked' : '';
echo '<div class="d-flex justify-content-between align-items-center py-2 border-bottom">';
echo '<div><strong>' . htmlspecialchars($label) . '</strong>';
if ($desc) echo '<br><small class="text-muted">' . htmlspecialchars($desc) . '</small>';
echo '</div>';
echo '<div class="form-check form-switch">';
echo '<input type="checkbox" class="form-check-input htaccess-opt" name="' . $name . '" id="htopt-' . $name . '" ' . $checked . '>';
echo '</div></div>';
};
?>
<div id="mokowaas-htaccess">
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-htaccess" role="tab">.htaccess</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-nginx" role="tab">NginX</a></li>
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-current" role="tab">Current File</a></li>
</ul>
<div class="tab-content">
<!-- .htaccess Tab -->
<div class="tab-pane fade show active" id="tab-htaccess" role="tabpanel">
<div class="row">
<!-- Left: Options -->
<div class="col-12 col-xl-6">
<div class="card mb-3">
<div class="card-header"><strong><span class="icon-shield-alt"></span> Security</strong></div>
<div class="card-body">
<?php $sw('disable_directory_listing', 'Disable Directory Listing', 'Options -Indexes'); ?>
<?php $sw('block_sensitive_files', 'Block Sensitive Files', 'htaccess.txt, configuration.php-dist, etc.'); ?>
<?php $sw('block_php_in_uploads', 'Block PHP in Uploads', 'Prevent .php in images/, media/, tmp/'); ?>
<?php $sw('disable_server_signature', 'Hide Server Signature', 'ServerSignature Off, remove X-Powered-By'); ?>
<?php $sw('prevent_clickjacking', 'Clickjacking Protection', 'X-Frame-Options: SAMEORIGIN'); ?>
<?php $sw('prevent_mime_sniffing', 'MIME Sniffing Prevention', 'X-Content-Type-Options: nosniff'); ?>
<?php $sw('xss_protection', 'XSS Protection Header', 'X-XSS-Protection: 1; mode=block'); ?>
<?php $sw('disable_trace_track', 'Disable TRACE/TRACK', 'Block HTTP TRACE and TRACK methods'); ?>
<div class="py-2 border-bottom">
<label class="form-label fw-bold" for="htopt-referrer_policy">Referrer Policy</label>
<select class="form-select form-select-sm htaccess-opt" name="referrer_policy" id="htopt-referrer_policy">
<option value="off" <?php echo ($opts['referrer_policy'] ?? '') === 'off' ? 'selected' : ''; ?>>Off</option>
<option value="no-referrer" <?php echo ($opts['referrer_policy'] ?? '') === 'no-referrer' ? 'selected' : ''; ?>>no-referrer</option>
<option value="same-origin" <?php echo ($opts['referrer_policy'] ?? '') === 'same-origin' ? 'selected' : ''; ?>>same-origin</option>
<option value="strict-origin-when-cross-origin" <?php echo ($opts['referrer_policy'] ?? '') === 'strict-origin-when-cross-origin' ? 'selected' : ''; ?>>strict-origin-when-cross-origin</option>
</select>
</div>
<?php $sw('hsts_enabled', 'HSTS (Force HTTPS)', 'Strict-Transport-Security header'); ?>
<div class="ps-4 <?php echo empty($opts['hsts_enabled']) ? 'd-none' : ''; ?>" id="hsts-options">
<div class="row g-2 py-2">
<div class="col-6">
<label class="form-label small">Max Age (seconds)</label>
<input type="number" class="form-control form-control-sm htaccess-opt" name="hsts_max_age" value="<?php echo (int) ($opts['hsts_max_age'] ?? 31536000); ?>">
</div>
<div class="col-6 d-flex align-items-end">
<div class="form-check">
<input type="checkbox" class="form-check-input htaccess-opt" name="hsts_subdomains" id="htopt-hsts_sub" <?php echo !empty($opts['hsts_subdomains']) ? 'checked' : ''; ?>>
<label class="form-check-label small" for="htopt-hsts_sub">Include Subdomains</label>
</div>
</div>
</div>
</div>
<?php $sw('csp_enabled', 'Content Security Policy', 'CSP header'); ?>
<div class="ps-4 <?php echo empty($opts['csp_enabled']) ? 'd-none' : ''; ?>" id="csp-options">
<textarea class="form-control form-control-sm htaccess-opt mt-1" name="csp_value" rows="2" placeholder="default-src 'self'; script-src 'self' 'unsafe-inline'"><?php echo htmlspecialchars($opts['csp_value'] ?? ''); ?></textarea>
</div>
<?php $sw('permissions_policy', 'Permissions Policy', 'Camera, microphone, geolocation controls'); ?>
<div class="ps-4 <?php echo empty($opts['permissions_policy']) ? 'd-none' : ''; ?>" id="perms-options">
<textarea class="form-control form-control-sm htaccess-opt mt-1" name="permissions_value" rows="2" placeholder="camera=(), microphone=(), geolocation=()"><?php echo htmlspecialchars($opts['permissions_value'] ?? ''); ?></textarea>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><strong><span class="icon-bolt"></span> Performance</strong></div>
<div class="card-body">
<?php $sw('enable_gzip', 'GZip Compression', 'Compress CSS, JS, HTML, XML, JSON'); ?>
<?php $sw('enable_expires', 'Browser Caching', 'Set expiration headers for static files'); ?>
<div class="ps-4 <?php echo empty($opts['enable_expires']) ? 'd-none' : ''; ?>" id="expires-options">
<div class="row g-2 py-2">
<div class="col-4">
<label class="form-label small">HTML (sec)</label>
<input type="number" class="form-control form-control-sm htaccess-opt" name="expires_html" value="<?php echo (int) ($opts['expires_html'] ?? 3600); ?>">
</div>
<div class="col-4">
<label class="form-label small">CSS/JS (sec)</label>
<input type="number" class="form-control form-control-sm htaccess-opt" name="expires_css_js" value="<?php echo (int) ($opts['expires_css_js'] ?? 2592000); ?>">
</div>
<div class="col-4">
<label class="form-label small">Images (sec)</label>
<input type="number" class="form-control form-control-sm htaccess-opt" name="expires_images" value="<?php echo (int) ($opts['expires_images'] ?? 31536000); ?>">
</div>
</div>
</div>
<?php $sw('etag_control', 'Disable ETags', 'For load-balanced environments'); ?>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><strong><span class="icon-search"></span> SEO / Redirects</strong></div>
<div class="card-body">
<div class="py-2 border-bottom">
<label class="form-label fw-bold">WWW Redirect</label>
<select class="form-select form-select-sm htaccess-opt" name="www_redirect">
<option value="off" <?php echo ($opts['www_redirect'] ?? 'off') === 'off' ? 'selected' : ''; ?>>Off</option>
<option value="www" <?php echo ($opts['www_redirect'] ?? '') === 'www' ? 'selected' : ''; ?>>Force www</option>
<option value="non-www" <?php echo ($opts['www_redirect'] ?? '') === 'non-www' ? 'selected' : ''; ?>>Force non-www</option>
</select>
</div>
<?php $sw('redirect_index_php', 'Redirect /index.php to /', 'SEO-friendly root redirect'); ?>
<?php $sw('force_trailing_slash', 'Force Trailing Slash', 'Append / to URLs without file extension'); ?>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><strong><span class="icon-code"></span> Custom Rules</strong></div>
<div class="card-body">
<textarea class="form-control htaccess-opt" name="custom_rules" rows="4" placeholder="# Add custom Apache directives here"><?php echo htmlspecialchars($opts['custom_rules'] ?? ''); ?></textarea>
</div>
</div>
</div>
<!-- Right: Preview -->
<div class="col-12 col-xl-6">
<div class="card mb-3 sticky-top" style="top:1rem">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Preview</strong>
<span class="badge bg-secondary" id="htaccess-line-count"><?php echo substr_count($preview, "\n"); ?> lines</span>
</div>
<div class="card-body p-0">
<textarea id="htaccess-preview" class="form-control font-monospace border-0" rows="30" readonly style="font-size:0.8rem;resize:none"><?php echo htmlspecialchars($preview); ?></textarea>
</div>
<div class="card-footer d-flex gap-2">
<button type="button" class="btn btn-primary" id="htaccess-save"
data-url="<?php echo $saveUrl; ?>" data-token="<?php echo $token; ?>">
<span class="icon-save"></span> Save to .htaccess
</button>
<button type="button" class="btn btn-outline-secondary" id="htaccess-download">
<span class="icon-download"></span> Download
</button>
</div>
</div>
</div>
</div>
</div>
<!-- NginX Tab -->
<div class="tab-pane fade" id="tab-nginx" role="tabpanel">
<div class="card">
<div class="card-header"><strong>NginX Configuration Snippet</strong></div>
<div class="card-body p-0">
<textarea id="nginx-preview" class="form-control font-monospace border-0" rows="25" readonly style="font-size:0.8rem;resize:none"><?php echo htmlspecialchars($nginx); ?></textarea>
</div>
<div class="card-footer">
<button type="button" class="btn btn-outline-secondary" id="nginx-download">
<span class="icon-download"></span> Download NginX Config
</button>
</div>
</div>
</div>
<!-- Current File Tab -->
<div class="tab-pane fade" id="tab-current" role="tabpanel">
<div class="card">
<div class="card-header"><strong>Current .htaccess on Disk</strong></div>
<div class="card-body p-0">
<textarea class="form-control font-monospace border-0" rows="25" readonly style="font-size:0.8rem;resize:none"><?php echo htmlspecialchars($current); ?></textarea>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var saveBtn = document.getElementById('htaccess-save');
var preview = document.getElementById('htaccess-preview');
var lineCount = document.getElementById('htaccess-line-count');
// Toggle sub-option visibility
document.getElementById('htopt-hsts_enabled').addEventListener('change', function() {
document.getElementById('hsts-options').classList.toggle('d-none', !this.checked);
});
document.getElementById('htopt-csp_enabled').addEventListener('change', function() {
document.getElementById('csp-options').classList.toggle('d-none', !this.checked);
});
document.getElementById('htopt-permissions_policy').addEventListener('change', function() {
document.getElementById('perms-options').classList.toggle('d-none', !this.checked);
});
document.getElementById('htopt-enable_expires') && document.getElementById('htopt-enable_expires').addEventListener('change', function() {
document.getElementById('expires-options').classList.toggle('d-none', !this.checked);
});
// Regenerate preview on any option change
document.querySelectorAll('.htaccess-opt').forEach(function(el) {
el.addEventListener('change', regeneratePreview);
el.addEventListener('input', regeneratePreview);
});
function collectOptions() {
var opts = {};
document.querySelectorAll('.htaccess-opt').forEach(function(el) {
if (el.type === 'checkbox') {
opts[el.name] = el.checked ? 1 : 0;
} else {
opts[el.name] = el.value;
}
});
return opts;
}
var debounceTimer;
function regeneratePreview() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
var fd = new FormData();
var opts = collectOptions();
for (var k in opts) fd.append(k, opts[k]);
fd.append('<?php echo $token; ?>', '1');
fetch('<?php echo $genUrl; ?>', {
method: 'POST', body: fd,
headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.htaccess) {
preview.value = d.htaccess;
lineCount.textContent = d.htaccess.split('\n').length + ' lines';
}
if (d.nginx) {
document.getElementById('nginx-preview').value = d.nginx;
}
});
}, 300);
}
// Save to disk
saveBtn.addEventListener('click', function() {
if (!confirm('This will overwrite your current .htaccess file. A backup will be created at .htaccess.mokowaas.bak. Continue?')) return;
var btn = this;
btn.disabled = true;
var fd = new FormData();
fd.append('content', preview.value);
var opts = collectOptions();
for (var k in opts) fd.append('opt_' + k, opts[k]);
fd.append('<?php echo $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]});
else Joomla.renderMessages({error: [d.message]});
})
.catch(function() { Joomla.renderMessages({error: ['Network error']}); })
.finally(function() { btn.disabled = false; });
});
// Download buttons
function downloadText(content, filename) {
var blob = new Blob([content], {type: 'text/plain'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
document.getElementById('htaccess-download').addEventListener('click', function() {
downloadText(preview.value, '.htaccess');
});
document.getElementById('nginx-download').addEventListener('click', function() {
downloadText(document.getElementById('nginx-preview').value, 'mokowaas-nginx.conf');
});
});
</script>
@@ -0,0 +1,267 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$requests = $this->requests;
$policies = $this->policies;
$summary = $this->summary;
$token = Session::getFormToken();
$statusBadge = [
'pending' => 'bg-warning text-dark',
'processing' => 'bg-info',
'completed' => 'bg-success',
'denied' => 'bg-secondary',
];
$typeBadge = [
'export' => 'bg-primary',
'delete' => 'bg-danger',
'anonymize' => 'bg-warning text-dark',
];
?>
<div id="mokowaas-privacy">
<!-- Summary cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3 <?php echo $summary->pending_requests > 0 ? 'text-warning' : 'text-success'; ?>"><?php echo $summary->pending_requests; ?></span>
<small class="text-muted">Pending Requests</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3"><?php echo $summary->total_requests; ?></span>
<small class="text-muted">Total Requests</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3"><?php echo $summary->consent_entries; ?></span>
<small class="text-muted">Consent Entries</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-center p-3">
<span class="fw-bold fs-3"><?php echo $summary->policies_active; ?></span>
<small class="text-muted">Active Policies</small>
</div>
</div>
</div>
<!-- New Request Form -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-plus"></span> Create Data Request</strong>
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newRequestForm" aria-expanded="false">
<span class="icon-plus"></span> New Request
</button>
</div>
<div class="collapse" id="newRequestForm">
<div class="card-body">
<form id="formNewRequest" class="row g-3">
<div class="col-12 col-md-5">
<label for="req_user_id" class="form-label">User</label>
<select id="req_user_id" class="form-select" required>
<option value="">Select a user...</option>
<?php
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('name'), $db->quoteName('email')])
->from($db->quoteName('#__users'))
->where($db->quoteName('block') . ' = 0')
->order($db->quoteName('name'))
);
foreach ($db->loadObjectList() as $u):
?>
<option value="<?php echo (int) $u->id; ?>"><?php echo $this->escape($u->name); ?> (<?php echo $this->escape($u->email); ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-3">
<label for="req_type" class="form-label">Request Type</label>
<select id="req_type" class="form-select" required>
<option value="export">Export Data</option>
<option value="delete">Delete Data</option>
<option value="anonymize">Anonymize Data</option>
</select>
</div>
<div class="col-12 col-md-2">
<label for="req_auto" class="form-label">Auto-process</label>
<select id="req_auto" class="form-select">
<option value="0">No (pending)</option>
<option value="1">Yes (immediate)</option>
</select>
</div>
<div class="col-12 col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100" id="btnCreateRequest"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-check"></span> Submit
</button>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<!-- Data Requests -->
<div class="col-12 col-xl-8">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><span class="icon-user-shield"></span> Data Subject Requests</strong>
<form method="get" class="d-inline">
<input type="hidden" name="option" value="com_mokowaas">
<input type="hidden" name="view" value="privacy">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All</option>
<?php foreach (['pending','processing','completed','denied'] as $s): ?>
<option value="<?php echo $s; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_status') === $s ? 'selected' : ''; ?>><?php echo ucfirst($s); ?></option>
<?php endforeach; ?>
</select>
</form>
</div>
<?php if (empty($requests)): ?>
<div class="card-body text-center text-muted py-4">No data requests found.</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead><tr><th>#</th><th>User</th><th>Type</th><th>Status</th><th>Created</th><th>Processed</th><th>Actions</th></tr></thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td><?php echo $r->id; ?></td>
<td><?php echo $this->escape($r->user_name ?? ''); ?><br><small class="text-muted"><?php echo $this->escape($r->user_email ?? ''); ?></small></td>
<td><span class="badge <?php echo $typeBadge[$r->type] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->type); ?></span></td>
<td><span class="badge <?php echo $statusBadge[$r->status] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->status); ?></span></td>
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $r->created, 'M d, Y H:i'); ?></td>
<td class="text-nowrap small"><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, 'M d, Y H:i') : '—'; ?></td>
<td>
<?php if ($r->status === 'pending'): ?>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-success btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="approve"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">Approve</button>
<button type="button" class="btn btn-outline-danger btn-privacy-action" data-id="<?php echo $r->id; ?>" data-action="deny"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
data-token="<?php echo $token; ?>">Deny</button>
</div>
<?php elseif ($r->status === 'completed' && $r->type === 'export'): ?>
<button type="button" class="btn btn-sm btn-outline-primary btn-export-download" data-user="<?php echo $r->user_id; ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.exportUserData&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-download"></span> Download
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Retention Policies -->
<div class="col-12 col-xl-4">
<div class="card mb-4">
<div class="card-header"><strong><span class="icon-clock"></span> Retention Policies</strong></div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>Type</th><th>Days</th><th>Action</th><th>Active</th></tr></thead>
<tbody>
<?php foreach ($policies as $p): ?>
<tr>
<td class="small"><?php echo $this->escape($p->content_type); ?></td>
<td><?php echo $p->retention_days; ?></td>
<td><span class="badge bg-secondary"><?php echo $p->action; ?></span></td>
<td><?php echo (int) $p->enabled ? '<span class="text-success">Yes</span>' : '<span class="text-muted">No</span>'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Process request buttons
document.querySelectorAll('.btn-privacy-action').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
var action = el.dataset.action;
if (!confirm(action === 'approve' ? 'Approve and process this data request?' : 'Deny this request?')) return;
el.disabled = true;
var fd = new FormData();
fd.append('request_id', el.dataset.id);
fd.append('action', action);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
});
});
});
// Create new request
var form = document.getElementById('formNewRequest');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
var btn = document.getElementById('btnCreateRequest');
var userId = document.getElementById('req_user_id').value;
var type = document.getElementById('req_type').value;
var auto = document.getElementById('req_auto').value;
if (!userId) { Joomla.renderMessages({warning:['Please select a user.']}); return; }
btn.disabled = true;
var fd = new FormData();
fd.append('user_id', userId);
fd.append('type', type);
fd.append('action', auto === '1' ? 'approve' : 'create');
fd.append(btn.dataset.token, '1');
fetch(btn.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message || 'Request created.']}); location.reload(); }
else { Joomla.renderMessages({error:[d.message || 'Failed.']}); btn.disabled = false; }
})
.catch(function(){ btn.disabled = false; });
});
}
// Export download
document.querySelectorAll('.btn-export-download').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
var fd = new FormData();
fd.append('user_id', el.dataset.user);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success && d.data) {
var blob = new Blob([JSON.stringify(d.data, null, 2)], {type:'application/json'});
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'user-data-export-' + el.dataset.user + '.json';
a.click();
} else {
Joomla.renderMessages({error:[d.message || 'Export failed']});
}
});
});
});
});
</script>
@@ -0,0 +1,198 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$t = $this->ticket;
$canned = $this->cannedResponses;
$token = Session::getFormToken();
$statusBadge = [
'open' => 'bg-primary', 'in_progress' => 'bg-info',
'waiting' => 'bg-warning text-dark', 'resolved' => 'bg-success', 'closed' => 'bg-secondary',
];
$priorityBadge = [
'low' => 'bg-secondary', 'normal' => 'bg-primary', 'high' => 'bg-warning text-dark', 'urgent' => 'bg-danger',
];
?>
<div id="mokowaas-ticket" class="row">
<!-- Left: conversation thread -->
<div class="col-12 col-xl-8">
<!-- Original ticket -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong><?php echo $this->escape($t->created_by_name); ?></strong>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?></small>
</div>
<span class="badge bg-dark">Original</span>
</div>
<div class="card-body"><?php echo nl2br($this->escape($t->body)); ?></div>
</div>
<!-- Replies -->
<?php foreach ($t->replies as $reply): ?>
<div class="card mb-3 <?php echo $reply->is_internal ? 'border-warning' : ''; ?>">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong><?php echo $this->escape($reply->user_name ?? 'System'); ?></strong>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $reply->created, 'M d, Y H:i'); ?></small>
</div>
<?php if ($reply->is_internal): ?>
<span class="badge bg-warning text-dark">Internal Note</span>
<?php endif; ?>
</div>
<div class="card-body"><?php echo nl2br($this->escape($reply->body)); ?></div>
</div>
<?php endforeach; ?>
<!-- Reply form -->
<div class="card mb-3">
<div class="card-header"><strong>Reply</strong></div>
<div class="card-body">
<?php if (!empty($canned)): ?>
<div class="mb-2">
<select class="form-select form-select-sm" id="canned-select">
<option value="">Insert canned response...</option>
<?php foreach ($canned as $c): ?>
<option value="<?php echo $this->escape($c->body); ?>"><?php echo $this->escape($c->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<textarea id="reply-body" class="form-control mb-2" rows="5" placeholder="Type your reply..."></textarea>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary" id="btn-reply"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.addTicketReply&format=json'); ?>"
data-ticket="<?php echo $t->id; ?>" data-token="<?php echo $token; ?>">
<span class="icon-reply"></span> Send Reply
</button>
<button type="button" class="btn btn-outline-warning" id="btn-internal"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.addTicketReply&format=json'); ?>"
data-ticket="<?php echo $t->id; ?>" data-token="<?php echo $token; ?>" data-internal="1">
<span class="icon-eye-slash"></span> Internal Note
</button>
</div>
</div>
</div>
</div>
<!-- Right: ticket metadata -->
<div class="col-12 col-xl-4">
<div class="card mb-3">
<div class="card-header"><strong>Details</strong></div>
<div class="card-body">
<table class="table table-sm mb-0">
<tr><td class="text-muted">Status</td><td><span class="badge <?php echo $statusBadge[$t->status] ?? ''; ?>"><?php echo ucwords(str_replace('_', ' ', $t->status)); ?></span></td></tr>
<tr><td class="text-muted">Priority</td><td><span class="badge <?php echo $priorityBadge[$t->priority] ?? ''; ?>"><?php echo ucfirst($t->priority); ?></span></td></tr>
<tr><td class="text-muted">Category</td><td><?php echo $this->escape($t->category_title ?? '—'); ?></td></tr>
<tr><td class="text-muted">Created By</td><td><?php echo $this->escape($t->created_by_name); ?><br><small><?php echo $this->escape($t->created_by_email ?? ''); ?></small></td></tr>
<tr><td class="text-muted">Assigned To</td><td><?php echo $this->escape($t->assigned_to_name ?? 'Unassigned'); ?></td></tr>
<tr><td class="text-muted">Created</td><td><?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?></td></tr>
<?php if ($t->resolved): ?><tr><td class="text-muted">Resolved</td><td><?php echo HTMLHelper::_('date', $t->resolved, 'M d, Y H:i'); ?></td></tr><?php endif; ?>
<?php if ($t->closed): ?><tr><td class="text-muted">Closed</td><td><?php echo HTMLHelper::_('date', $t->closed, 'M d, Y H:i'); ?></td></tr><?php endif; ?>
<tr><td class="text-muted">Replies</td><td><?php echo $t->reply_count; ?></td></tr>
</table>
</div>
</div>
<!-- SLA -->
<?php if ($t->sla_response_due || $t->sla_resolution_due): ?>
<div class="card mb-3">
<div class="card-header"><strong>SLA</strong></div>
<div class="card-body">
<?php if ($t->sla_response_due): ?>
<div class="mb-2">
<small class="text-muted">Response Due</small><br>
<?php
$responseOverdue = !$t->sla_responded && strtotime($t->sla_response_due) < time();
?>
<span class="<?php echo $t->sla_responded ? 'text-success' : ($responseOverdue ? 'text-danger fw-bold' : ''); ?>">
<?php echo $t->sla_responded ? 'Responded' : HTMLHelper::_('date', $t->sla_response_due, 'M d H:i'); ?>
<?php echo $responseOverdue ? ' OVERDUE' : ''; ?>
</span>
</div>
<?php endif; ?>
<?php if ($t->sla_resolution_due): ?>
<div>
<small class="text-muted">Resolution Due</small><br>
<?php
$resolutionOverdue = !\in_array($t->status, ['resolved','closed']) && strtotime($t->sla_resolution_due) < time();
?>
<span class="<?php echo \in_array($t->status, ['resolved','closed']) ? 'text-success' : ($resolutionOverdue ? 'text-danger fw-bold' : ''); ?>">
<?php echo \in_array($t->status, ['resolved','closed']) ? 'Met' : HTMLHelper::_('date', $t->sla_resolution_due, 'M d H:i'); ?>
<?php echo $resolutionOverdue ? ' OVERDUE' : ''; ?>
</span>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Status actions -->
<div class="card mb-3">
<div class="card-header"><strong>Actions</strong></div>
<div class="card-body d-grid gap-2">
<?php foreach (['open' => 'Reopen', 'in_progress' => 'In Progress', 'waiting' => 'Waiting', 'resolved' => 'Resolve', 'closed' => 'Close'] as $s => $label): ?>
<?php if ($s !== $t->status): ?>
<button type="button" class="btn btn-sm btn-outline-<?php echo $s === 'closed' ? 'danger' : ($s === 'resolved' ? 'success' : 'secondary'); ?> btn-status"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.updateTicketStatus&format=json'); ?>"
data-ticket="<?php echo $t->id; ?>" data-status="<?php echo $s; ?>" data-token="<?php echo $token; ?>">
<?php echo $label; ?>
</button>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Canned response insert
var cannedSel = document.getElementById('canned-select');
if (cannedSel) {
cannedSel.addEventListener('change', function() {
if (this.value) { document.getElementById('reply-body').value = this.value; this.selectedIndex = 0; }
});
}
// Reply buttons
document.querySelectorAll('#btn-reply, #btn-internal').forEach(function(btn) {
btn.addEventListener('click', function() {
var body = document.getElementById('reply-body').value.trim();
if (!body) return;
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append('ticket_id', el.dataset.ticket);
fd.append('body', body);
fd.append('is_internal', el.dataset.internal || '0');
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if(d.success) location.reload(); else Joomla.renderMessages({error:[d.message]}); })
.finally(function(){ el.disabled = false; });
});
});
// Status buttons
document.querySelectorAll('.btn-status').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
el.disabled = true;
var fd = new FormData();
fd.append('ticket_id', el.dataset.ticket);
fd.append('status', el.dataset.status);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){ if(d.success) location.reload(); else Joomla.renderMessages({error:[d.message]}); })
.finally(function(){ el.disabled = false; });
});
});
});
</script>
@@ -0,0 +1,291 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$tickets = $this->tickets;
$categories = $this->categories;
$counts = $this->statusCounts;
$overdue = $this->overdue;
$atsAvailable = $this->atsAvailable;
$token = Session::getFormToken();
$statusBadge = [
'open' => 'bg-primary',
'in_progress' => 'bg-info',
'waiting' => 'bg-warning text-dark',
'resolved' => 'bg-success',
'closed' => 'bg-secondary',
];
$priorityBadge = [
'low' => 'bg-secondary',
'normal' => 'bg-primary',
'high' => 'bg-warning text-dark',
'urgent' => 'bg-danger',
];
?>
<div id="mokowaas-tickets">
<!-- Status summary cards -->
<div class="row g-3 mb-4">
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->open; ?></span><small class="text-muted">Open</small></div></div>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->in_progress; ?></span><small class="text-muted">In Progress</small></div></div>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->waiting; ?></span><small class="text-muted">Waiting</small></div></div>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->resolved; ?></span><small class="text-muted">Resolved</small></div></div>
<div class="col"><div class="card text-center p-2"><span class="fw-bold fs-4"><?php echo $counts->closed; ?></span><small class="text-muted">Closed</small></div></div>
<?php if (\count($overdue) > 0): ?>
<div class="col"><div class="card text-center p-2 border-danger"><span class="fw-bold fs-4 text-danger"><?php echo \count($overdue); ?></span><small class="text-danger">SLA Overdue</small></div></div>
<?php endif; ?>
</div>
<!-- New ticket + filters -->
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3">
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newTicketModal">
<span class="icon-plus"></span> New Ticket
</button>
<?php if ($atsAvailable): ?>
<button type="button" class="btn btn-outline-info" id="btn-import-ats"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.importAts&format=json'); ?>"
data-token="<?php echo $token; ?>"
data-tickets="<?php echo $atsAvailable->tickets; ?>"
data-posts="<?php echo $atsAvailable->posts; ?>">
<span class="icon-upload"></span> Import from Akeeba (<?php echo $atsAvailable->tickets; ?> tickets, <?php echo $atsAvailable->posts; ?> posts)
</button>
<?php endif; ?>
</div>
<form method="get" class="d-flex gap-2">
<input type="hidden" name="option" value="com_mokowaas">
<input type="hidden" name="view" value="tickets">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Statuses</option>
<?php foreach (['open','in_progress','waiting','resolved','closed'] as $s): ?>
<option value="<?php echo $s; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_status') === $s ? 'selected' : ''; ?>><?php echo ucwords(str_replace('_', ' ', $s)); ?></option>
<?php endforeach; ?>
</select>
<select name="filter_priority" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Priorities</option>
<?php foreach (['low','normal','high','urgent'] as $p): ?>
<option value="<?php echo $p; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_priority') === $p ? 'selected' : ''; ?>><?php echo ucfirst($p); ?></option>
<?php endforeach; ?>
</select>
</form>
</div>
<!-- Ticket table -->
<div class="card">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>#</th>
<th>Subject</th>
<th>Status</th>
<th>Priority</th>
<th>Category</th>
<th>Created By</th>
<th>Assigned To</th>
<th>Created</th>
<th>SLA</th>
</tr>
</thead>
<tbody>
<?php if (empty($tickets)): ?>
<tr><td colspan="9" class="text-center text-muted py-4">No tickets found.</td></tr>
<?php else: ?>
<?php foreach ($tickets as $t): ?>
<?php
$slaClass = '';
$now = time();
if ($t->sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now) $slaClass = 'table-danger';
elseif ($t->sla_resolution_due && strtotime($t->sla_resolution_due) < $now && !\in_array($t->status, ['resolved','closed'])) $slaClass = 'table-danger';
elseif ($t->sla_response_due && !$t->sla_responded && strtotime($t->sla_response_due) < $now + 3600) $slaClass = 'table-warning';
?>
<tr class="<?php echo $slaClass; ?>">
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=ticket&id=' . $t->id); ?>"><?php echo $t->id; ?></a></td>
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=ticket&id=' . $t->id); ?>"><?php echo $this->escape(mb_substr($t->subject, 0, 60)); ?></a></td>
<td><span class="badge <?php echo $statusBadge[$t->status] ?? 'bg-secondary'; ?>"><?php echo ucwords(str_replace('_', ' ', $t->status)); ?></span></td>
<td><span class="badge <?php echo $priorityBadge[$t->priority] ?? 'bg-secondary'; ?>"><?php echo ucfirst($t->priority); ?></span></td>
<td><?php echo $this->escape($t->category_title ?? '—'); ?></td>
<td><?php echo $this->escape($t->created_by_name ?? ''); ?></td>
<td><?php echo $t->assigned_to_name ? $this->escape($t->assigned_to_name) : '<em>Unassigned</em>'; ?></td>
<td class="small"><?php echo HTMLHelper::_('date', $t->created, 'M d H:i'); ?></td>
<td class="small">
<?php if ($t->sla_response_due && !$t->sla_responded): ?>
<span title="Response due"><?php echo HTMLHelper::_('date', $t->sla_response_due, 'M d H:i'); ?></span>
<?php elseif ($t->sla_resolution_due): ?>
<span title="Resolution due"><?php echo HTMLHelper::_('date', $t->sla_resolution_due, 'M d H:i'); ?></span>
<?php else: ?>—<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- New Ticket Modal -->
<div class="modal fade" id="newTicketModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header"><h5 class="modal-title">New Ticket</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<!-- KB Search step -->
<div id="modal-kb-step">
<label class="form-label fw-bold">What's the issue?</label>
<div class="input-group mb-3">
<input type="text" id="modal-kb-search" class="form-control" placeholder="Describe your issue to search for existing answers...">
<button type="button" class="btn btn-outline-primary" id="modal-kb-btn"><span class="icon-search"></span></button>
</div>
<div id="modal-kb-results" class="list-group mb-3 d-none"></div>
<button type="button" class="btn btn-primary" id="modal-show-form">
<span class="icon-plus"></span> Create Ticket
</button>
</div>
<!-- Ticket form step (hidden initially) -->
<form id="modal-ticket-form" class="d-none" method="post" action="<?php echo Route::_('index.php?option=com_mokowaas&task=display.createTicket&format=json'); ?>">
<div class="mb-3">
<label class="form-label">Subject</label>
<input type="text" name="subject" id="modal-subject" class="form-control" required>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Category</label>
<select name="category_id" class="form-select">
<option value="">— Select —</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo $this->escape($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Priority</label>
<select name="priority" class="form-select">
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="body" class="form-control" rows="6" required></textarea>
</div>
<input type="hidden" name="<?php echo $token; ?>" value="1">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"><span class="icon-plus"></span> Create Ticket</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
// Modal KB search
var modalSearch = document.getElementById('modal-kb-search');
var modalSearchBtn = document.getElementById('modal-kb-btn');
var modalResults = document.getElementById('modal-kb-results');
var modalShowForm = document.getElementById('modal-show-form');
var modalKbStep = document.getElementById('modal-kb-step');
var modalForm = document.getElementById('modal-ticket-form');
var modalSubject = document.getElementById('modal-subject');
function modalDoSearch() {
var q = modalSearch.value.trim();
if (q.length < 3) return;
fetch('<?php echo Route::_('index.php?option=com_mokowaas&task=display.searchKb&format=json'); ?>&q=' + encodeURIComponent(q), {
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d) {
modalResults.textContent = '';
if (d.results && d.results.length > 0) {
d.results.forEach(function(item) {
var a = document.createElement('a');
a.href = item.url;
a.target = '_blank';
a.className = 'list-group-item list-group-item-action';
var strong = document.createElement('strong');
strong.textContent = item.title;
a.appendChild(strong);
if (item.description) {
a.appendChild(document.createElement('br'));
var small = document.createElement('small');
small.className = 'text-muted';
small.textContent = item.description;
a.appendChild(small);
}
modalResults.appendChild(a);
});
modalResults.classList.remove('d-none');
} else {
modalResults.classList.add('d-none');
}
});
}
if (modalSearchBtn) modalSearchBtn.addEventListener('click', modalDoSearch);
if (modalSearch) modalSearch.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); modalDoSearch(); } });
// Show ticket form
if (modalShowForm) {
modalShowForm.addEventListener('click', function() {
modalKbStep.classList.add('d-none');
modalForm.classList.remove('d-none');
if (modalSearch.value && !modalSubject.value) modalSubject.value = modalSearch.value;
modalSubject.focus();
});
}
// Submit ticket from modal
if (modalForm) {
modalForm.addEventListener('submit', function(e) {
e.preventDefault();
var form = this;
var fd = new FormData(form);
fetch(form.action, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { location.href = 'index.php?option=com_mokowaas&view=ticket&id=' + d.id; }
else { Joomla.renderMessages({error:[d.message]}); }
});
});
}
// Reset modal on close
document.getElementById('newTicketModal').addEventListener('hidden.bs.modal', function() {
modalKbStep.classList.remove('d-none');
modalForm.classList.add('d-none');
modalResults.classList.add('d-none');
modalSearch.value = '';
modalForm.reset();
});
// ATS Import
var atsBtn = document.getElementById('btn-import-ats');
if (atsBtn) {
atsBtn.addEventListener('click', function() {
var el = this;
if (!confirm('Import ' + el.dataset.tickets + ' tickets and ' + el.dataset.posts + ' posts from Akeeba Ticket System? Duplicates will be skipped.')) return;
el.disabled = true;
el.textContent = ' Importing...';
var fd = new FormData();
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; el.textContent = 'Import Failed - Retry'; }
})
.catch(function(){ Joomla.renderMessages({error:['Network error']}); el.disabled = false; });
});
}
</script>
@@ -0,0 +1,212 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$logs = $this->logs;
$ruleCounts = $this->ruleCounts;
$topIps = $this->topIps;
$ruleNames = $this->ruleNames;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$totalPages = max(1, ceil($total / 50));
$ruleBadge = [
'sqli' => 'bg-danger', 'xss' => 'bg-danger', 'mua' => 'bg-warning text-dark',
'rfi' => 'bg-danger', 'dfi' => 'bg-danger', 'blocked_file' => 'bg-info',
'blocked_php' => 'bg-info', 'tmpl_switch' => 'bg-secondary',
'ip_blocklist' => 'bg-dark', 'admin_secret' => 'bg-dark',
];
?>
<div id="mokowaas-waflog">
<!-- Rule distribution cards -->
<div class="d-flex flex-wrap gap-2 mb-4">
<?php foreach ($ruleCounts as $rc): ?>
<div class="card p-2 text-center" style="min-width:100px">
<span class="badge <?php echo $ruleBadge[$rc->rule] ?? 'bg-secondary'; ?> mb-1"><?php echo htmlspecialchars($rc->rule); ?></span>
<span class="fw-bold"><?php echo number_format($rc->cnt); ?></span>
</div>
<?php endforeach; ?>
<div class="card p-2 text-center" style="min-width:100px">
<span class="badge bg-primary mb-1">Total</span>
<span class="fw-bold"><?php echo number_format($total); ?></span>
</div>
</div>
<div class="row">
<!-- Main: Log table -->
<div class="col-12 col-xl-9">
<!-- Filters -->
<form method="get" class="card mb-3">
<div class="card-body">
<input type="hidden" name="option" value="com_mokowaas">
<input type="hidden" name="view" value="waflog">
<div class="row g-2">
<div class="col-md-2">
<select name="filter_rule" class="form-select form-select-sm">
<option value="">All Rules</option>
<?php foreach ($ruleNames as $r): ?>
<option value="<?php echo htmlspecialchars($r); ?>" <?php echo $filters['rule'] === $r ? 'selected' : ''; ?>><?php echo htmlspecialchars($r); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<input type="text" name="filter_ip" class="form-control form-control-sm" placeholder="IP address" value="<?php echo htmlspecialchars($filters['ip']); ?>">
</div>
<div class="col-md-2">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search URI/detail" value="<?php echo htmlspecialchars($filters['search']); ?>">
</div>
<div class="col-md-2">
<input type="date" name="filter_date_from" class="form-control form-control-sm" value="<?php echo htmlspecialchars($filters['date_from']); ?>">
</div>
<div class="col-md-2">
<input type="date" name="filter_date_to" class="form-control form-control-sm" value="<?php echo htmlspecialchars($filters['date_to']); ?>">
</div>
<div class="col-md-2 d-flex gap-1">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</div>
</form>
<!-- Log table -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><?php echo number_format($total); ?> blocked requests</strong>
<button type="button" class="btn btn-sm btn-outline-danger" id="btn-purge"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.purgeWafLog&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-trash"></span> Purge Old Logs
</button>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover table-sm mb-0">
<thead>
<tr><th>Time</th><th>IP</th><th>Rule</th><th>URI</th><th>Detail</th><th>User Agent</th><th></th></tr>
</thead>
<tbody>
<?php if (empty($logs)): ?>
<tr><td colspan="7" class="text-center text-muted py-4">No blocked requests found.</td></tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr>
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $log->created, 'M d H:i:s'); ?></td>
<td><code><?php echo htmlspecialchars($log->ip); ?></code></td>
<td><span class="badge <?php echo $ruleBadge[$log->rule] ?? 'bg-secondary'; ?>"><?php echo htmlspecialchars($log->rule); ?></span></td>
<td class="small" style="max-width:250px;overflow:hidden;text-overflow:ellipsis" title="<?php echo htmlspecialchars($log->uri); ?>"><?php echo htmlspecialchars(mb_substr($log->uri, 0, 60)); ?></td>
<td class="small" style="max-width:200px;overflow:hidden;text-overflow:ellipsis"><?php echo htmlspecialchars(mb_substr($log->detail, 0, 50)); ?></td>
<td class="small" style="max-width:200px;overflow:hidden;text-overflow:ellipsis"><?php echo htmlspecialchars(mb_substr($log->user_agent, 0, 40)); ?></td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($log->ip); ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.banIpFromLog&format=json'); ?>"
data-token="<?php echo $token; ?>" title="Ban this IP">
<span class="icon-ban"></span>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<div class="card-footer d-flex justify-content-between align-items-center">
<small class="text-muted">Page <?php echo $page; ?> of <?php echo $totalPages; ?></small>
<nav>
<ul class="pagination pagination-sm mb-0">
<?php if ($page > 1): ?>
<li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog&page=' . ($page - 1)); ?>">Prev</a></li>
<?php endif; ?>
<?php if ($page < $totalPages): ?>
<li class="page-item"><a class="page-link" href="<?php echo Route::_('index.php?option=com_mokowaas&view=waflog&page=' . ($page + 1)); ?>">Next</a></li>
<?php endif; ?>
</ul>
</nav>
</div>
<?php endif; ?>
</div>
</div>
<!-- Sidebar: Top IPs -->
<div class="col-12 col-xl-3">
<div class="card">
<div class="card-header"><strong>Top Blocked IPs</strong></div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>IP</th><th>Blocks</th><th>Last</th><th></th></tr></thead>
<tbody>
<?php foreach ($topIps as $tip): ?>
<tr>
<td><code class="small"><?php echo htmlspecialchars($tip->ip); ?></code></td>
<td class="fw-bold"><?php echo $tip->cnt; ?></td>
<td class="small text-nowrap"><?php echo HTMLHelper::_('date', $tip->last_seen, 'M d'); ?></td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($tip->ip); ?>"
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.banIpFromLog&format=json'); ?>"
data-token="<?php echo $token; ?>" title="Ban">
<span class="icon-ban"></span>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
// Ban IP buttons
document.querySelectorAll('.btn-ban-ip').forEach(function(btn) {
btn.addEventListener('click', function() {
var el = this;
var ip = el.dataset.ip;
if (!confirm('Add ' + ip + ' to the firewall IP blocklist?')) return;
el.disabled = true;
var fd = new FormData();
fd.append('ip', ip);
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); el.textContent = 'Banned'; }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; }
});
});
});
// Purge button
var purgeBtn = document.getElementById('btn-purge');
if (purgeBtn) {
purgeBtn.addEventListener('click', function() {
var days = prompt('Delete WAF logs older than how many days?', '30');
if (!days || isNaN(days)) return;
this.disabled = true;
var fd = new FormData();
fd.append('days', days);
fd.append(this.dataset.token, '1');
fetch(this.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); location.reload(); }
else { Joomla.renderMessages({error:[d.message]}); }
})
.finally(function(){ purgeBtn.disabled = false; });
});
}
});
</script>
@@ -29,7 +29,7 @@ class CacheController extends BaseController
* *
* @since 1.0.0 * @since 1.0.0
*/ */
public function execute(): void public function execute($task = 'cache'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
@@ -42,7 +42,7 @@ class InstallController extends BaseController
* *
* @since 02.21.00 * @since 02.21.00
*/ */
public function execute(): void public function execute($task = 'install'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
@@ -104,7 +104,7 @@ class PluginsController extends BaseController
* *
* @return void * @return void
*/ */
public function execute(): void public function execute($task = 'plugins'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
$user = $app->getIdentity(); $user = $app->getIdentity();
@@ -0,0 +1,236 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Provision reset API controller.
*
* POST /api/index.php/v1/mokowaas/provision-reset
*
* Resets a site for new client provisioning: clears hits, versions,
* download keys, and flags the site for fresh client info collection.
* Used after copying a demo site to create a new client install.
*
* @since 02.35.00
*/
class ProvisionController extends BaseController
{
/**
* Reset the site for new client provisioning.
*
* @return void
*/
public function execute($task = 'provision'): void
{
$app = Factory::getApplication();
$user = $app->getIdentity();
if (!$user->authorise('core.manage', 'com_mokowaas'))
{
$this->sendJson(403, ['error' => 'Not authorized']);
return;
}
if ($app->input->getMethod() !== 'POST')
{
$this->sendJson(405, ['error' => 'POST required']);
return;
}
$db = Factory::getDbo();
$results = [];
// 1. Reset article hit counters
try
{
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__content'))
->set($db->quoteName('hits') . ' = 0')
)->execute();
$results['hits_reset'] = $db->getAffectedRows();
}
catch (\Throwable $e)
{
$results['hits_reset'] = 'error: ' . $e->getMessage();
}
// 2. Delete content version history
try
{
$db->setQuery(
$db->getQuery(true)->delete($db->quoteName('#__history'))
)->execute();
$results['versions_deleted'] = $db->getAffectedRows();
}
catch (\Throwable $e)
{
$results['versions_deleted'] = 'error: ' . $e->getMessage();
}
// 3. Regenerate heartbeat token if requested
$input = $app->getInput()->json;
$resetToken = (bool) ($input->get('reset_token', false, 'BOOLEAN'));
if ($resetToken)
{
try
{
$newToken = bin2hex(random_bytes(32));
$plugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokowaas');
if ($plugin)
{
$pluginParams = new \Joomla\Registry\Registry($plugin->params);
$pluginParams->set('health_api_token', $newToken);
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote($pluginParams->toString()))
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
)->execute();
$results['token_regenerated'] = true;
$results['new_token'] = $newToken;
}
}
catch (\Throwable $e)
{
$results['token_regenerated'] = 'error: ' . $e->getMessage();
}
}
// 4. Reset all user API tokens if requested
$resetApiTokens = (bool) ($input->get('reset_api_tokens', false, 'BOOLEAN'));
if ($resetApiTokens)
{
try
{
// Get users who have API tokens before deleting
$db->setQuery(
$db->getQuery(true)
->select('DISTINCT ' . $db->quoteName('user_id'))
->from($db->quoteName('#__user_keys'))
->where($db->quoteName('series') . ' LIKE ' . $db->quote('api-%'))
);
$affectedUserIds = $db->loadColumn() ?: [];
$db->setQuery(
$db->getQuery(true)->delete($db->quoteName('#__user_keys'))
->where($db->quoteName('series') . ' LIKE ' . $db->quote('api-%'))
)->execute();
$results['api_tokens_revoked'] = $db->getAffectedRows();
// Notify affected users
if (!empty($affectedUserIds))
{
$this->notifyTokenReset($db, $affectedUserIds);
$results['users_notified'] = \count($affectedUserIds);
}
}
catch (\Throwable $e)
{
$results['api_tokens_revoked'] = 'error: ' . $e->getMessage();
}
}
// 5. Flag site for fresh client info setup
try
{
// Write a flag file that the core plugin checks on next admin load
$flagFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_setup_required.flag';
file_put_contents($flagFile, json_encode([
'created' => gmdate('Y-m-d\TH:i:s\Z'),
'reason' => 'provision-reset',
'remote_ip' => $_SERVER['REMOTE_ADDR'] ?? '',
]));
$results['setup_flag'] = true;
}
catch (\Throwable $e)
{
$results['setup_flag'] = 'error: ' . $e->getMessage();
}
$this->sendJson(200, [
'status' => 'ok',
'message' => 'Site provisioned for new client.',
'results' => $results,
]);
}
/**
* Notify users that their API tokens have been revoked.
*/
private function notifyTokenReset($db, array $userIds): void
{
try
{
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('name'), $db->quoteName('email')])
->from($db->quoteName('#__users'))
->whereIn($db->quoteName('id'), $userIds)
->where($db->quoteName('block') . ' = 0')
);
$users = $db->loadObjectList() ?: [];
$config = Factory::getConfig();
$siteName = $config->get('sitename', 'Joomla');
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
$mailer = Factory::getMailer();
foreach ($users as $u)
{
try
{
$mailer->clearAllRecipients();
$mailer->addRecipient($u->email, $u->name);
$mailer->setSubject($siteName . ' — API tokens have been reset');
$mailer->setBody(
"Hello {$u->name},\n\n"
. "Your API access tokens on {$siteName} have been revoked by an administrator.\n\n"
. "If you use API integrations, please log in and generate a new token:\n"
. "{$siteUrl}/administrator/\n\n"
. "{$siteName}"
);
$mailer->send();
}
catch (\Throwable $e)
{
// Non-critical
}
}
}
catch (\Throwable $e)
{
// Non-critical
}
}
private function sendJson(int $code, array $data): void
{
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_SLASHES);
Factory::getApplication()->close();
}
}
@@ -0,0 +1,173 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\Registry\Registry;
/**
* Remote login API controller.
*
* POST /api/index.php/v1/mokowaas/remote-login
* Body: {"token": "health_api_token", "user": "requesting_username", "origin": "MokoWaaSBase"}
*
* Validates the health API token, generates a one-time login token
* for the master user, and returns a URL that auto-authenticates.
*
* @since 02.35.00
*/
class RemoteLoginController extends BaseController
{
/**
* One-time token validity in seconds.
*/
private const OTL_TTL = 60;
/**
* Generate a one-time login URL for the master user.
*
* @return void
*/
public function execute($task = 'remoteLogin'): void
{
$app = Factory::getApplication();
$input = $app->getInput()->json;
$token = $input->get('token', '', 'RAW');
$origin = $input->get('origin', '', 'STRING');
if (empty($token))
{
$this->sendJson(401, ['error' => 'Missing token']);
return;
}
// Validate against the core plugin's health_api_token
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
if (!$plugin)
{
$this->sendJson(503, ['error' => 'MokoWaaS core plugin not found']);
return;
}
$params = new Registry($plugin->params);
$healthToken = $params->get('health_api_token', '');
if (empty($healthToken) || !hash_equals($healthToken, $token))
{
$this->sendJson(401, ['error' => 'Invalid token']);
return;
}
// Find the master user
$masterUsernames = $this->getMasterUsernames($params);
if (empty($masterUsernames))
{
$this->sendJson(403, ['error' => 'No master user configured']);
return;
}
// Use the first master username
$masterUsername = $masterUsernames[0];
// Look up the user
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('username')])
->from($db->quoteName('#__users'))
->where($db->quoteName('username') . ' = ' . $db->quote($masterUsername))
->where($db->quoteName('block') . ' = 0')
);
$user = $db->loadObject();
if (!$user)
{
$this->sendJson(403, ['error' => 'Master user not found or blocked']);
return;
}
// Generate one-time login token
$otlToken = bin2hex(random_bytes(32));
$expires = time() + self::OTL_TTL;
// Store in a temp file (avoids DB schema changes)
$otlFile = JPATH_ADMINISTRATOR . '/cache/mokowaas_otl_' . md5($otlToken) . '.json';
file_put_contents($otlFile, json_encode([
'token' => $otlToken,
'user_id' => (int) $user->id,
'username' => $user->username,
'expires' => $expires,
'origin' => substr($origin, 0, 100),
]));
// Build login URL
$loginUrl = rtrim(Uri::root(), '/') . '/administrator/index.php?mokowaas_otl=' . $otlToken;
$this->sendJson(200, [
'status' => 'ok',
'login_url' => $loginUrl,
'expires' => $expires,
'user' => $user->username,
]);
}
/**
* Decode master usernames from plugin params.
*
* @param Registry $params Plugin params.
*
* @return array
*/
private function getMasterUsernames(Registry $params): array
{
// Use MokoWaaSHelper if available
$helperFile = JPATH_PLUGINS . '/system/mokowaas/Helper/MokoWaaSHelper.php';
if (file_exists($helperFile))
{
require_once $helperFile;
if (method_exists(\Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper::class, 'getMasterUsernames'))
{
return \Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper::getMasterUsernames();
}
}
return [];
}
/**
* Send JSON response and terminate.
*
* @param int $code HTTP status code.
* @param array $data Response data.
*
* @return void
*/
private function sendJson(int $code, array $data): void
{
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_SLASHES);
Factory::getApplication()->close();
}
}
@@ -35,7 +35,7 @@ class ResetController extends BaseController
* *
* @since 02.21.00 * @since 02.21.00
*/ */
public function execute(): void public function execute($task = 'reset'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
@@ -90,18 +90,18 @@ class ResetController extends BaseController
*/ */
private function createService(Registry $params) private function createService(Registry $params)
{ {
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php'; $serviceFile = JPATH_PLUGINS . '/task/mokowaasdemo/src/Service/DemoResetService.php';
if (!file_exists($serviceFile)) if (!file_exists($serviceFile))
{ {
throw new \RuntimeException('DemoResetService not found — is the MokoWaaS plugin installed?'); throw new \RuntimeException('DemoResetService not found — is the demo reset plugin installed?');
} }
require_once $serviceFile; require_once $serviceFile;
$media = (bool) $params->get('demo_snapshot_include_media', 1); $media = (bool) $params->get('demo_snapshot_include_media', 1);
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($media); return new \Moko\Plugin\Task\MokoWaaSDemo\Service\DemoResetService($media);
} }
/** /**
@@ -68,7 +68,7 @@ class SnapshotController extends BaseController
* *
* @since 02.21.00 * @since 02.21.00
*/ */
public function execute(): void public function execute($task = 'snapshot'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
@@ -118,11 +118,11 @@ class SnapshotController extends BaseController
*/ */
private function createService() private function createService()
{ {
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php'; $serviceFile = JPATH_PLUGINS . '/task/mokowaasdemo/src/Service/DemoResetService.php';
if (!file_exists($serviceFile)) if (!file_exists($serviceFile))
{ {
throw new \RuntimeException('DemoResetService not found'); throw new \RuntimeException('DemoResetService not found — is the demo reset plugin installed?');
} }
require_once $serviceFile; require_once $serviceFile;
@@ -132,7 +132,7 @@ class SnapshotController extends BaseController
$media = (bool) $params->get('demo_snapshot_include_media', 1); $media = (bool) $params->get('demo_snapshot_include_media', 1);
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($media); return new \Moko\Plugin\Task\MokoWaaSDemo\Service\DemoResetService($media);
} }
/** /**
@@ -26,7 +26,7 @@ use Joomla\Registry\Registry;
*/ */
class SyncController extends BaseController class SyncController extends BaseController
{ {
public function execute(): void public function execute($task = 'sync'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
@@ -57,10 +57,10 @@ class SyncController extends BaseController
$params = new Registry($plugin->params); $params = new Registry($plugin->params);
$targets = json_decode($params->get('sync_targets', '[]'), true) ?: []; $targets = json_decode($params->get('sync_targets', '[]'), true) ?: [];
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncService.php'; $serviceFile = JPATH_PLUGINS . '/task/mokowaassync/src/Service/ContentSyncService.php';
require_once $serviceFile; require_once $serviceFile;
$service = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncService(); $service = new \Moko\Plugin\Task\MokoWaaSSync\Service\ContentSyncService();
$result = $service->syncAllTargets($targets); $result = $service->syncAllTargets($targets);
$this->sendJson(200, $result); $this->sendJson(200, $result);
@@ -24,7 +24,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
*/ */
class SyncReceiveController extends BaseController class SyncReceiveController extends BaseController
{ {
public function execute(): void public function execute($task = 'syncReceive'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
@@ -52,10 +52,10 @@ class SyncReceiveController extends BaseController
return; return;
} }
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/ContentSyncReceiver.php'; $serviceFile = JPATH_PLUGINS . '/task/mokowaassync/src/Service/ContentSyncReceiver.php';
require_once $serviceFile; require_once $serviceFile;
$receiver = new \Moko\Plugin\System\MokoWaaS\Service\ContentSyncReceiver(); $receiver = new \Moko\Plugin\Task\MokoWaaSSync\Service\ContentSyncReceiver();
$result = $receiver->receive($payload); $result = $receiver->receive($payload);
$this->sendJson(200, $result); $this->sendJson(200, $result);
@@ -29,7 +29,7 @@ class UpdateController extends BaseController
* *
* @since 1.0.0 * @since 1.0.0
*/ */
public function execute(): void public function execute($task = 'update'): void
{ {
$app = Factory::getApplication(); $app = Factory::getApplication();
@@ -109,4 +109,26 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
}); });
} }
// Akeeba import buttons
['btn-import-admintools', 'btn-import-ats-dash'].forEach(function(id) {
var btn = document.getElementById(id);
if (!btn) return;
btn.addEventListener('click', function() {
var el = this;
if (!confirm('Import Akeeba data into MokoWaaS? Akeeba extensions will be disabled after import.')) return;
el.disabled = true;
var origText = el.textContent;
el.textContent = ' Importing...';
var fd = new FormData();
fd.append(el.dataset.token, '1');
fetch(el.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json()})
.then(function(d){
if (d.success) { Joomla.renderMessages({message:[d.message]}); setTimeout(function(){location.reload()}, 2000); }
else { Joomla.renderMessages({error:[d.message]}); el.disabled = false; el.textContent = origText; }
})
.catch(function(){ Joomla.renderMessages({error:['Network error']}); el.disabled = false; el.textContent = origText; });
});
});
}); });
+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
FILE INFORMATION
DEFGROUP: Joomla.Component
INGROUP: MokoWaaS
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
VERSION: 02.34.16
PATH: /mokowaas.xml
BRIEF: Component manifest for MokoWaaS admin dashboard and REST API
-->
<extension type="component" method="upgrade">
<name>MokoWaaS</name>
<author>Moko Consulting</author>
<creationDate>2026-06-02</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</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>
<administration>
<menu img="class:cogs">MokoWaaS</menu>
<submenu>
<menu link="option=com_mokowaas" img="class:cogs">COM_MOKOWAAS_MENU_DASHBOARD</menu>
<menu link="option=com_mokowaas&amp;view=extensions" img="class:puzzle-piece">COM_MOKOWAAS_MENU_EXTENSIONS</menu>
<menu link="option=com_mokowaas&amp;view=tickets" img="class:headphones">COM_MOKOWAAS_MENU_TICKETS</menu>
<menu link="option=com_mokowaas&amp;view=htaccess" img="class:file-code">COM_MOKOWAAS_MENU_HTACCESS</menu>
<menu link="option=com_mokowaas&amp;view=privacy" img="class:lock">COM_MOKOWAAS_MENU_PRIVACY</menu>
<menu link="option=com_mokowaas&amp;view=waflog" img="class:shield-alt">COM_MOKOWAAS_MENU_WAFLOG</menu>
<menu link="option=com_mokowaas&amp;view=database" img="class:database">COM_MOKOWAAS_MENU_DATABASE</menu>
<menu link="option=com_mokowaas&amp;view=cleanup" img="class:trash">COM_MOKOWAAS_MENU_CLEANUP</menu>
<menu link="option=com_plugins&amp;filter[folder]=system&amp;filter[search]=mokowaas" img="class:power-off">COM_MOKOWAAS_MENU_PLUGINS</menu>
<menu link="option=com_installer&amp;view=update" img="class:refresh">COM_MOKOWAAS_MENU_UPDATES</menu>
<menu link="option=com_checkin" img="class:check-square">COM_MOKOWAAS_MENU_CHECKIN</menu>
<menu link="option=com_cache" img="class:bolt">COM_MOKOWAAS_MENU_CACHE</menu>
</submenu>
<files folder="admin">
<filename>access.xml</filename>
<filename>catalog.xml</filename>
<filename>config.xml</filename>
<folder>language</folder>
<folder>services</folder>
<folder>sql</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="admin/language">
<language tag="en-GB">en-GB/com_mokowaas.sys.ini</language>
</languages>
</administration>
<files folder="site">
<folder>language</folder>
<folder>services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<install>
<sql><file driver="mysql" charset="utf8">admin/sql/install.mysql.sql</file></sql>
</install>
<api>
<files folder="api">
<folder>src</folder>
</files>
</api>
<media destination="com_mokowaas" folder="media">
<folder>css</folder>
<folder>js</folder>
</media>
</extension>
@@ -0,0 +1,11 @@
; MokoWaaS Customer Portal - Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOWAAS_PORTAL_TITLE="Support Portal"
COM_MOKOWAAS_PORTAL_MY_TICKETS="My Support Tickets"
COM_MOKOWAAS_PORTAL_NEW_TICKET="New Ticket"
COM_MOKOWAAS_PORTAL_SUBMIT="Submit Ticket"
COM_MOKOWAAS_PORTAL_REPLY="Send Reply"
COM_MOKOWAAS_PORTAL_NO_TICKETS="You haven't submitted any support tickets yet."
COM_MOKOWAAS_PORTAL_LOGIN_REQUIRED="Please log in to access the support portal."
@@ -0,0 +1,38 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoWaaS'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoWaaS'));
$container->set(
ComponentInterface::class,
function (Container $container) {
$component = new \Joomla\CMS\Extension\MVCComponent(
$container->get(ComponentDispatcherFactoryInterface::class)
);
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
return $component;
}
);
}
};
@@ -0,0 +1,267 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Site\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
class DisplayController extends BaseController
{
protected $default_view = 'tickets';
public function display($cachable = false, $urlparams = [])
{
$user = Factory::getApplication()->getIdentity();
if ($user->guest)
{
Factory::getApplication()->enqueueMessage('Please log in to access the support portal.', 'warning');
Factory::getApplication()->redirect(Route::_(
'index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_mokowaas&view=tickets'),
false
));
return;
}
return parent::display($cachable, $urlparams);
}
/**
* Submit a new ticket.
*/
public function submitTicket()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
$user = Factory::getApplication()->getIdentity();
if ($user->guest)
{
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
return;
}
$input = Factory::getApplication()->getInput();
// Use admin TicketsModel
$model = $this->getModel('Tickets', 'Administrator');
$this->jsonResponse($model->createTicket([
'subject' => $input->getString('subject', ''),
'body' => $input->getRaw('body', ''),
'priority' => $input->getString('priority', 'normal'),
'category_id' => $input->getInt('category_id', 0),
]));
}
/**
* Submit a reply.
*/
public function submitReply()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
$user = Factory::getApplication()->getIdentity();
$input = Factory::getApplication()->getInput();
if ($user->guest)
{
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
return;
}
$ticketId = $input->getInt('ticket_id', 0);
$model = $this->getModel('Tickets', 'Administrator');
$ticket = $model->getTicket($ticketId);
if (!$ticket)
{
$this->jsonResponse(['success' => false, 'message' => 'Ticket not found.']);
return;
}
// Customers can only reply to their own tickets; staff can reply to any
if ((int) $ticket->created_by !== $user->id && !$this->isStaff($user))
{
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
return;
}
// Staff replies from frontend are not internal notes
$this->jsonResponse($model->addReply(
$ticketId,
$input->getRaw('body', ''),
false
));
}
/**
* Update ticket status (staff/manager only from frontend).
*/
public function updateStatus()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
$user = Factory::getApplication()->getIdentity();
if (!$this->isStaff($user))
{
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
return;
}
$input = Factory::getApplication()->getInput();
$model = $this->getModel('Tickets', 'Administrator');
$this->jsonResponse($model->updateStatus(
$input->getInt('ticket_id', 0),
$input->getString('status', '')
));
}
/**
* Assign a ticket (manager only from frontend).
*/
public function assignTicket()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
$user = Factory::getApplication()->getIdentity();
if (!$user->authorise('mokowaas.tickets.assign', 'com_mokowaas'))
{
$this->jsonResponse(['success' => false, 'message' => 'Access denied.']);
return;
}
$input = Factory::getApplication()->getInput();
$ticketId = $input->getInt('ticket_id', 0);
$assignTo = $input->getInt('assigned_to', 0);
try
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__mokowaas_tickets'))
->set($db->quoteName('assigned_to') . ' = ' . ($assignTo ?: 'NULL'))
->set($db->quoteName('modified') . ' = ' . $db->quote(Factory::getDate()->toSql()))
->where($db->quoteName('id') . ' = ' . $ticketId)
)->execute();
$this->jsonResponse(['success' => true, 'message' => 'Ticket assigned.']);
}
catch (\Throwable $e)
{
$this->jsonResponse(['success' => false, 'message' => $e->getMessage()]);
return;
}
}
/**
* Submit a data privacy request from frontend.
*/
public function submitDataRequest()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
$user = Factory::getApplication()->getIdentity();
if ($user->guest)
{
$this->jsonResponse(['success' => false, 'message' => 'Please log in.']);
return;
}
$type = Factory::getApplication()->getInput()->getString('type', '');
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
$this->jsonResponse($model->createRequest($user->id, $type, 'Submitted via self-service portal'));
}
/**
* Check if user is support staff (can manage tickets beyond their own).
*/
private function isStaff($user): bool
{
if ($user->guest)
{
return false;
}
// Super admins always staff
if ($user->authorise('core.admin'))
{
return true;
}
// Anyone with mokowaas.tickets ACL on the component is staff
return $user->authorise('mokowaas.tickets', 'com_mokowaas');
}
/**
* Search KB articles via Smart Search (com_finder).
*/
public function searchKb()
{
$query = Factory::getApplication()->getInput()->getString('q', '');
if (strlen($query) < 3)
{
$this->jsonResponse(['results' => []]);
}
try
{
$db = Factory::getDbo();
$escaped = $db->quote('%' . $db->escape($query, true) . '%');
$results = $db->setQuery(
$db->getQuery(true)
->select([
$db->quoteName('l.link_id'),
$db->quoteName('l.title'),
$db->quoteName('l.url'),
$db->quoteName('l.description'),
])
->from($db->quoteName('#__finder_links', 'l'))
->where($db->quoteName('l.published') . ' = 1')
->where('(' . $db->quoteName('l.title') . ' LIKE ' . $escaped
. ' OR ' . $db->quoteName('l.description') . ' LIKE ' . $escaped . ')')
->order($db->quoteName('l.title') . ' ASC')
->setLimit(8)
)->loadObjectList() ?: [];
foreach ($results as $r)
{
$r->description = mb_substr(strip_tags($r->description ?? ''), 0, 150);
}
$this->jsonResponse(['results' => $results]);
}
catch (\Throwable $e)
{
$this->jsonResponse(['results' => []]);
}
}
private function jsonResponse(array $data): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json');
echo json_encode($data);
$app->close();
}
}
@@ -0,0 +1,68 @@
<?php
namespace Moko\Component\MokoWaaS\Site\View\Privacy;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
class HtmlView extends BaseHtmlView
{
protected $requests = [];
protected $consent = [];
public function display($tpl = null)
{
$user = Factory::getApplication()->getIdentity();
if ($user->guest)
{
Factory::getApplication()->redirect(Route::_(
'index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_mokowaas&view=privacy'),
false
));
return;
}
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
// Get user's data requests
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_data_requests'))
->where($db->quoteName('user_id') . ' = ' . (int) $user->id)
->order($db->quoteName('created') . ' DESC');
try
{
$db->setQuery($query);
$this->requests = $db->loadObjectList() ?: [];
}
catch (\Throwable $e)
{
$this->requests = [];
}
// Get consent history
try
{
$db->setQuery(
$db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokowaas_consent_log'))
->where($db->quoteName('user_id') . ' = ' . (int) $user->id)
->order($db->quoteName('created') . ' DESC')
->setLimit(20)
);
$this->consent = $db->loadObjectList() ?: [];
}
catch (\Throwable $e)
{
$this->consent = [];
}
parent::display($tpl);
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Site\View\Ticket;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
class HtmlView extends BaseHtmlView
{
protected $ticket;
protected $isStaff = false;
protected $canAssign = false;
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$user = Factory::getApplication()->getIdentity();
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->isStaff = $user->authorise('core.admin') || $user->authorise('mokowaas.tickets', 'com_mokowaas');
$this->canAssign = $user->authorise('core.admin') || $user->authorise('mokowaas.tickets.assign', 'com_mokowaas');
// Get ticket — staff see any, customers see only their own
$query = $db->getQuery(true)
->select([
$db->quoteName('t') . '.*',
$db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'),
$db->quoteName('u.email', 'created_by_email'),
$db->quoteName('a.name', 'assigned_to_name'),
])
->from($db->quoteName('#__mokowaas_tickets', 't'))
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to')
->where($db->quoteName('t.id') . ' = ' . $id);
if (!$this->isStaff)
{
$query->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id);
}
$db->setQuery($query);
$this->ticket = $db->loadObject();
if (!$this->ticket)
{
Factory::getApplication()->enqueueMessage('Ticket not found.', 'error');
Factory::getApplication()->redirect(Route::_('index.php?option=com_mokowaas&view=tickets', false));
return;
}
// Load replies — staff see internal notes, customers don't
$query = $db->getQuery(true)
->select([
$db->quoteName('r') . '.*',
$db->quoteName('u.name', 'user_name'),
])
->from($db->quoteName('#__mokowaas_ticket_replies', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->where($db->quoteName('r.ticket_id') . ' = ' . $id);
if (!$this->isStaff)
{
$query->where($db->quoteName('r.is_internal') . ' = 0');
}
$query->order($db->quoteName('r.created') . ' ASC');
$db->setQuery($query);
$this->ticket->replies = $db->loadObjectList() ?: [];
parent::display($tpl);
}
}
@@ -0,0 +1,75 @@
<?php
/**
* @package MokoWaaS
* @subpackage com_mokowaas.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoWaaS\Site\View\Tickets;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
class HtmlView extends BaseHtmlView
{
protected $tickets = [];
protected $categories = [];
protected $isStaff = false;
public function display($tpl = null)
{
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$user = Factory::getApplication()->getIdentity();
$this->isStaff = $user->authorise('core.admin')
|| $user->authorise('mokowaas.tickets', 'com_mokowaas');
// Staff see all tickets, customers see their own
$query = $db->getQuery(true)
->select([
$db->quoteName('t.id'),
$db->quoteName('t.subject'),
$db->quoteName('t.status'),
$db->quoteName('t.priority'),
$db->quoteName('t.created'),
$db->quoteName('t.assigned_to'),
$db->quoteName('c.title', 'category_title'),
$db->quoteName('u.name', 'created_by_name'),
$db->quoteName('a.name', 'assigned_to_name'),
])
->from($db->quoteName('#__mokowaas_tickets', 't'))
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to');
if (!$this->isStaff)
{
$query->where($db->quoteName('t.created_by') . ' = ' . (int) $user->id);
}
$filterStatus = Factory::getApplication()->getInput()->getString('filter_status', '');
if ($filterStatus)
{
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($filterStatus));
}
$query->order($db->quoteName('t.created') . ' DESC')->setLimit(50);
$db->setQuery($query);
$this->tickets = $db->loadObjectList() ?: [];
// Categories for new ticket form
$query = $db->getQuery(true)
->select([$db->quoteName('id'), $db->quoteName('title')])
->from($db->quoteName('#__mokowaas_ticket_categories'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->categories = $db->loadObjectList() ?: [];
parent::display($tpl);
}
}
@@ -0,0 +1,114 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$user = Factory::getApplication()->getIdentity();
$requests = $this->requests;
$consent = $this->consent;
$token = Session::getFormToken();
$statusLabel = ['pending' => 'Pending', 'processing' => 'Processing', 'completed' => 'Completed', 'denied' => 'Denied'];
$statusClass = ['pending' => 'warning', 'processing' => 'info', 'completed' => 'success', 'denied' => 'secondary'];
?>
<div class="mokowaas-portal">
<h2>My Privacy &amp; Data</h2>
<p class="text-muted">Manage your personal data, download your information, or request account deletion.</p>
<!-- Action buttons -->
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<button type="button" class="btn btn-primary w-100 py-3 btn-data-request" data-type="export">
<span class="icon-download d-block mb-1" style="font-size:1.5rem"></span>
Download My Data
</button>
</div>
<div class="col-12 col-md-4">
<button type="button" class="btn btn-outline-warning w-100 py-3 btn-data-request" data-type="anonymize">
<span class="icon-user-shield d-block mb-1" style="font-size:1.5rem"></span>
Anonymize My Account
</button>
</div>
<div class="col-12 col-md-4">
<button type="button" class="btn btn-outline-danger w-100 py-3 btn-data-request" data-type="delete">
<span class="icon-trash d-block mb-1" style="font-size:1.5rem"></span>
Delete My Account
</button>
</div>
</div>
<!-- My requests -->
<?php if (!empty($requests)): ?>
<div class="card mb-4">
<div class="card-header"><strong>My Data Requests</strong></div>
<div class="table-responsive">
<table class="table table-striped mb-0">
<thead><tr><th>Type</th><th>Status</th><th>Submitted</th><th>Processed</th></tr></thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td><?php echo ucfirst($r->type); ?></td>
<td><span class="badge bg-<?php echo $statusClass[$r->status] ?? 'secondary'; ?>"><?php echo $statusLabel[$r->status] ?? $r->status; ?></span></td>
<td><?php echo HTMLHelper::_('date', $r->created, 'M d, Y H:i'); ?></td>
<td><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, 'M d, Y H:i') : '—'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Consent history -->
<?php if (!empty($consent)): ?>
<div class="card mb-4">
<div class="card-header"><strong>Consent History</strong></div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<thead><tr><th>Category</th><th>Action</th><th>Date</th></tr></thead>
<tbody>
<?php foreach ($consent as $c): ?>
<tr>
<td><?php echo htmlspecialchars(ucwords(str_replace('_', ' ', $c->category))); ?></td>
<td><span class="badge bg-<?php echo $c->action === 'granted' ? 'success' : 'secondary'; ?>"><?php echo ucfirst($c->action); ?></span></td>
<td><?php echo HTMLHelper::_('date', $c->created, 'M d, Y H:i'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
<script>
document.querySelectorAll('.btn-data-request').forEach(function(btn) {
btn.addEventListener('click', function() {
var type = this.dataset.type;
var messages = {
'export': 'Request a download of all your personal data?',
'anonymize': 'Request your account to be anonymized? Your name, email, and personal details will be replaced. This cannot be undone.',
'delete': 'Request permanent deletion of your account and all data? This cannot be undone.'
};
if (!confirm(messages[type] || 'Submit this request?')) return;
this.disabled = true;
var fd = new FormData();
fd.append('type', type);
fd.append('<?php echo $token; ?>', '1');
fetch('<?php echo Route::_("index.php?option=com_mokowaas&task=display.submitDataRequest&format=json"); ?>', {
method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success) { alert(d.message); location.reload(); }
else { alert(d.message || 'Failed.'); }
})
.catch(function() { alert('Network error.'); })
.finally(function() { btn.disabled = false; });
});
});
</script>
@@ -0,0 +1,241 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Factory;
$t = $this->ticket;
$isStaff = $this->isStaff;
$canAssign = $this->canAssign;
$token = Session::getFormToken();
$userId = Factory::getApplication()->getIdentity()->id;
$statusLabel = [
'open' => 'Open', 'in_progress' => 'In Progress', 'waiting' => 'Awaiting Response',
'resolved' => 'Resolved', 'closed' => 'Closed',
];
$statusClass = [
'open' => 'primary', 'in_progress' => 'info', 'waiting' => 'warning',
'resolved' => 'success', 'closed' => 'secondary',
];
?>
<div class="mokowaas-portal-ticket">
<div class="mb-3">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=tickets'); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-arrow-left"></span> Back to Tickets
</a>
</div>
<div class="row">
<!-- Main column: conversation -->
<div class="col-12 <?php echo $isStaff ? 'col-lg-8' : ''; ?>">
<!-- Ticket header -->
<div class="card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h3 class="mb-1">#<?php echo $t->id; ?> — <?php echo htmlspecialchars($t->subject); ?></h3>
<small class="text-muted">
<?php echo htmlspecialchars($t->category_title ?? 'General'); ?>
&middot; <?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?>
&middot; <?php echo ucfirst($t->priority); ?>
<?php if ($isStaff): ?>
&middot; By: <?php echo htmlspecialchars($t->created_by_name); ?>
<?php endif; ?>
</small>
</div>
<span class="badge bg-<?php echo $statusClass[$t->status] ?? 'secondary'; ?> fs-6">
<?php echo $statusLabel[$t->status] ?? $t->status; ?>
</span>
</div>
</div>
</div>
<!-- Original message -->
<div class="card mb-3">
<div class="card-header">
<strong><?php echo htmlspecialchars($t->created_by_name); ?></strong>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?></small>
</div>
<div class="card-body"><?php echo nl2br(htmlspecialchars($t->body)); ?></div>
</div>
<!-- Replies -->
<?php foreach ($t->replies as $reply): ?>
<?php
$replyIsStaffUser = ((int) $reply->user_id !== (int) $t->created_by);
$isInternal = (int) $reply->is_internal;
?>
<div class="card mb-3 <?php echo $isInternal ? 'border-warning bg-warning bg-opacity-10' : ($replyIsStaffUser ? 'border-primary' : ''); ?>">
<div class="card-header d-flex justify-content-between">
<div>
<strong><?php echo htmlspecialchars($reply->user_name ?? 'Support'); ?></strong>
<?php if ($replyIsStaffUser): ?><span class="badge bg-primary ms-1">Staff</span><?php endif; ?>
<?php if ($isInternal): ?><span class="badge bg-warning text-dark ms-1">Internal Note</span><?php endif; ?>
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $reply->created, 'M d, Y H:i'); ?></small>
</div>
</div>
<div class="card-body"><?php echo nl2br(htmlspecialchars($reply->body)); ?></div>
</div>
<?php endforeach; ?>
<!-- Reply form -->
<?php if (!\in_array($t->status, ['closed'])): ?>
<div class="card mt-4">
<div class="card-body">
<h5>Reply</h5>
<form id="portalReply">
<textarea name="body" class="form-control mb-3" rows="5" required placeholder="Type your reply..."></textarea>
<input type="hidden" name="ticket_id" value="<?php echo $t->id; ?>">
<input type="hidden" name="<?php echo $token; ?>" value="1">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<span class="icon-paper-plane"></span> Send Reply
</button>
<?php if ($isStaff): ?>
<button type="button" class="btn btn-outline-warning" id="btn-internal-note">
<span class="icon-eye-slash"></span> Internal Note
</button>
<?php endif; ?>
</div>
</form>
</div>
</div>
<?php elseif ($t->status === 'closed'): ?>
<div class="alert alert-secondary mt-4">
This ticket is closed. <a href="<?php echo Route::_('index.php?option=com_mokowaas&view=tickets&layout=submit'); ?>">Open a new ticket</a> if you need further help.
</div>
<?php endif; ?>
</div>
<!-- Staff sidebar -->
<?php if ($isStaff): ?>
<div class="col-12 col-lg-4">
<!-- Ticket info -->
<div class="card mb-3">
<div class="card-header"><strong>Details</strong></div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5 text-muted">Status</dt>
<dd class="col-7"><span class="badge bg-<?php echo $statusClass[$t->status] ?? 'secondary'; ?>"><?php echo $statusLabel[$t->status] ?? $t->status; ?></span></dd>
<dt class="col-5 text-muted">Priority</dt>
<dd class="col-7"><?php echo ucfirst($t->priority); ?></dd>
<dt class="col-5 text-muted">Category</dt>
<dd class="col-7"><?php echo htmlspecialchars($t->category_title ?? '—'); ?></dd>
<dt class="col-5 text-muted">Submitted By</dt>
<dd class="col-7"><?php echo htmlspecialchars($t->created_by_name); ?><br><small class="text-muted"><?php echo htmlspecialchars($t->created_by_email ?? ''); ?></small></dd>
<dt class="col-5 text-muted">Assigned To</dt>
<dd class="col-7"><?php echo htmlspecialchars($t->assigned_to_name ?? 'Unassigned'); ?></dd>
<dt class="col-5 text-muted">Created</dt>
<dd class="col-7"><?php echo HTMLHelper::_('date', $t->created, 'M d H:i'); ?></dd>
<dt class="col-5 text-muted">Replies</dt>
<dd class="col-7"><?php echo \count($t->replies); ?></dd>
</dl>
</div>
</div>
<!-- Status actions -->
<div class="card mb-3">
<div class="card-header"><strong>Change Status</strong></div>
<div class="card-body d-grid gap-2">
<?php foreach (['open' => 'Reopen', 'in_progress' => 'In Progress', 'waiting' => 'Waiting on Customer', 'resolved' => 'Resolve', 'closed' => 'Close'] as $s => $label): ?>
<?php if ($s !== $t->status): ?>
<button type="button" class="btn btn-sm btn-outline-<?php echo $s === 'closed' ? 'danger' : ($s === 'resolved' ? 'success' : 'secondary'); ?> btn-status"
data-status="<?php echo $s; ?>">
<?php echo $label; ?>
</button>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php if ($canAssign): ?>
<!-- Quick assign -->
<div class="card mb-3">
<div class="card-header"><strong>Assign</strong></div>
<div class="card-body">
<button type="button" class="btn btn-sm btn-outline-primary w-100" id="btn-assign-me">
Assign to Me
</button>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var token = '<?php echo $token; ?>';
var ticketId = <?php echo $t->id; ?>;
// Reply
var replyForm = document.getElementById('portalReply');
if (replyForm) {
replyForm.addEventListener('submit', function(e) {
e.preventDefault();
sendReply(false);
});
}
// Internal note
var internalBtn = document.getElementById('btn-internal-note');
if (internalBtn) {
internalBtn.addEventListener('click', function() { sendReply(true); });
}
function sendReply(isInternal) {
var body = replyForm.querySelector('textarea[name=body]').value.trim();
if (!body) return;
var fd = new FormData();
fd.append('ticket_id', ticketId);
fd.append('body', body);
fd.append('is_internal', isInternal ? '1' : '0');
fd.append(token, '1');
fetch('<?php echo Route::_("index.php?option=com_mokowaas&task=display.submitReply&format=json"); ?>', {
method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d){
if (d.success) location.reload();
else alert(d.message);
});
}
// Status buttons
document.querySelectorAll('.btn-status').forEach(function(btn) {
btn.addEventListener('click', function() {
var fd = new FormData();
fd.append('ticket_id', ticketId);
fd.append('status', this.dataset.status);
fd.append(token, '1');
fetch('<?php echo Route::_("index.php?option=com_mokowaas&task=display.updateStatus&format=json"); ?>', {
method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d){
if (d.success) location.reload();
else alert(d.message);
});
});
});
// Assign to me
var assignBtn = document.getElementById('btn-assign-me');
if (assignBtn) {
assignBtn.addEventListener('click', function() {
var fd = new FormData();
fd.append('ticket_id', ticketId);
fd.append('assigned_to', <?php echo $userId; ?>);
fd.append(token, '1');
fetch('<?php echo Route::_("index.php?option=com_mokowaas&task=display.assignTicket&format=json"); ?>', {
method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}
}).then(function(r){return r.json()}).then(function(d){
if (d.success) location.reload();
else alert(d.message);
});
});
}
});
</script>
@@ -0,0 +1,83 @@
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$tickets = $this->tickets;
$categories = $this->categories;
$isStaff = $this->isStaff;
$token = Session::getFormToken();
$statusLabel = [
'open' => 'Open', 'in_progress' => 'In Progress', 'waiting' => 'Awaiting Response',
'resolved' => 'Resolved', 'closed' => 'Closed',
];
$statusClass = [
'open' => 'primary', 'in_progress' => 'info', 'waiting' => 'warning',
'resolved' => 'success', 'closed' => 'secondary',
];
?>
<div class="mokowaas-portal">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><?php echo $isStaff ? 'All Support Tickets' : 'My Support Tickets'; ?></h2>
<div class="d-flex gap-2">
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=tickets&layout=submit'); ?>" class="btn btn-primary">
<span class="icon-plus"></span> New Ticket
</a>
<?php if ($isStaff): ?>
<form method="get" class="d-inline">
<input type="hidden" name="option" value="com_mokowaas">
<input type="hidden" name="view" value="tickets">
<select name="filter_status" class="form-select form-select-sm" style="width:auto" onchange="this.form.submit()">
<option value="">All Statuses</option>
<?php foreach ($statusLabel as $k => $v): ?>
<option value="<?php echo $k; ?>" <?php echo Factory::getApplication()->getInput()->getString('filter_status') === $k ? 'selected' : ''; ?>><?php echo $v; ?></option>
<?php endforeach; ?>
</select>
</form>
<?php endif; ?>
</div>
</div>
<?php if (empty($tickets)): ?>
<div class="alert alert-info">
<span class="icon-info-circle"></span>
<?php echo $isStaff ? 'No tickets found.' : 'You haven\'t submitted any support tickets yet.'; ?>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>#</th>
<th>Subject</th>
<th>Status</th>
<th>Priority</th>
<th>Category</th>
<?php if ($isStaff): ?><th>Submitted By</th><th>Assigned To</th><?php endif; ?>
<th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach ($tickets as $t): ?>
<tr>
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=ticket&id=' . $t->id); ?>"><?php echo $t->id; ?></a></td>
<td><a href="<?php echo Route::_('index.php?option=com_mokowaas&view=ticket&id=' . $t->id); ?>"><?php echo htmlspecialchars(mb_substr($t->subject, 0, 60)); ?></a></td>
<td><span class="badge bg-<?php echo $statusClass[$t->status] ?? 'secondary'; ?>"><?php echo $statusLabel[$t->status] ?? $t->status; ?></span></td>
<td><?php echo ucfirst($t->priority); ?></td>
<td><?php echo htmlspecialchars($t->category_title ?? '—'); ?></td>
<?php if ($isStaff): ?>
<td><?php echo htmlspecialchars($t->created_by_name ?? ''); ?></td>
<td><?php echo htmlspecialchars($t->assigned_to_name ?? '<em>Unassigned</em>'); ?></td>
<?php endif; ?>
<td class="text-nowrap"><?php echo HTMLHelper::_('date', $t->created, 'M d, Y'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
@@ -0,0 +1,204 @@
<?php
/**
* Submit a Ticket layout — search KB first, then submit form.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$categories = $this->categories;
$token = Session::getFormToken();
$searchUrl = Route::_('index.php?option=com_mokowaas&task=display.searchKb&format=json');
$submitUrl = Route::_('index.php?option=com_mokowaas&task=display.submitTicket&format=json');
$ticketUrl = Route::_('index.php?option=com_mokowaas&view=ticket&id=');
$ticketsUrl = Route::_('index.php?option=com_mokowaas&view=tickets');
// Check if Smart Search has indexed content
$finderEnabled = false;
try {
$db = \Joomla\CMS\Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
$db->setQuery('SELECT COUNT(*) FROM #__finder_links WHERE published = 1');
$finderEnabled = (int) $db->loadResult() > 0;
} catch (\Throwable $e) {}
?>
<div class="mokowaas-portal">
<h2>Submit a Support Request</h2>
<?php if ($finderEnabled): ?>
<!-- Step 1: Search -->
<div id="step-search" class="mb-4">
<p class="text-muted">Before submitting, let's see if we already have an answer for you.</p>
<div class="card">
<div class="card-body">
<label class="form-label fw-bold" for="kb-search">Describe your issue</label>
<div class="input-group input-group-lg">
<input type="text" id="kb-search" class="form-control" placeholder="e.g. how do I reset my password?" autofocus>
<button type="button" class="btn btn-primary" id="kb-search-btn">
<span class="icon-search"></span> Search
</button>
</div>
</div>
</div>
<!-- Search results -->
<div id="kb-results" class="mt-3 d-none">
<h5>Related Articles</h5>
<div id="kb-results-list" class="list-group mb-3"></div>
<p class="text-muted">Didn't find what you need?</p>
</div>
<div class="mt-3">
<button type="button" class="btn btn-outline-primary" id="btn-show-form">
<span class="icon-plus"></span> Submit a Ticket Anyway
</button>
</div>
</div>
<?php endif; ?>
<!-- Step 2: Ticket Form -->
<div id="step-form" class="<?php echo $finderEnabled ? 'd-none' : ''; ?>">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-3">Ticket Details</h5>
<form id="submitTicketForm">
<div class="mb-3">
<label class="form-label" for="ticket-subject">Subject <span class="text-danger">*</span></label>
<input type="text" id="ticket-subject" name="subject" class="form-control" required placeholder="Brief description of your issue">
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label" for="ticket-category">Category</label>
<select id="ticket-category" name="category_id" class="form-select">
<option value="">Select a category</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo $cat->id; ?>"><?php echo htmlspecialchars($cat->title); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="ticket-priority">Priority</label>
<select id="ticket-priority" name="priority" class="form-select">
<option value="normal">Normal</option>
<option value="low">Low</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="ticket-body">Description <span class="text-danger">*</span></label>
<textarea id="ticket-body" name="body" class="form-control" rows="8" required placeholder="Please describe your issue in detail."></textarea>
</div>
<input type="hidden" name="<?php echo $token; ?>" value="1">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-lg">
<span class="icon-paper-plane"></span> Submit Ticket
</button>
<a href="<?php echo $ticketsUrl; ?>" class="btn btn-outline-secondary btn-lg">
My Tickets
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var searchInput = document.getElementById('kb-search');
var searchBtn = document.getElementById('kb-search-btn');
var resultBox = document.getElementById('kb-results');
var resultList = document.getElementById('kb-results-list');
var showFormBtn = document.getElementById('btn-show-form');
var stepSearch = document.getElementById('step-search');
var stepForm = document.getElementById('step-form');
var subjectField = document.getElementById('ticket-subject');
// Search
function doSearch() {
var q = (searchInput ? searchInput.value.trim() : '');
if (q.length < 3) return;
fetch('<?php echo $searchUrl; ?>&q=' + encodeURIComponent(q), {
headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
resultList.textContent = '';
if (d.results && d.results.length > 0) {
d.results.forEach(function(item) {
var a = document.createElement('a');
a.href = item.url;
a.target = '_blank';
a.className = 'list-group-item list-group-item-action';
var strong = document.createElement('strong');
strong.textContent = item.title;
a.appendChild(strong);
if (item.description) {
a.appendChild(document.createElement('br'));
var small = document.createElement('small');
small.className = 'text-muted';
small.textContent = item.description;
a.appendChild(small);
}
resultList.appendChild(a);
});
resultBox.classList.remove('d-none');
} else {
resultBox.classList.add('d-none');
}
// Always show the "submit anyway" button after search
if (showFormBtn) showFormBtn.classList.remove('d-none');
});
}
if (searchBtn) searchBtn.addEventListener('click', doSearch);
if (searchInput) {
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); doSearch(); }
});
}
// Show form and prefill subject from search query
if (showFormBtn) {
showFormBtn.addEventListener('click', function() {
if (stepSearch) stepSearch.classList.add('d-none');
if (stepForm) stepForm.classList.remove('d-none');
if (searchInput && subjectField && !subjectField.value) {
subjectField.value = searchInput.value;
}
subjectField.focus();
});
}
// Submit ticket
var form = document.getElementById('submitTicketForm');
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
var btn = form.querySelector('button[type=submit]');
btn.disabled = true;
btn.textContent = ' Submitting...';
var fd = new FormData(form);
fetch('<?php echo $submitUrl; ?>', {
method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.success && d.id) {
window.location.href = '<?php echo $ticketUrl; ?>' + d.id;
} else {
alert(d.message || 'Failed.');
btn.disabled = false;
btn.textContent = ' Submit Ticket';
}
})
.catch(function() { alert('Network error.'); btn.disabled = false; });
});
}
});
</script>
@@ -0,0 +1,4 @@
MOD_MOKOWAAS_CACHE="MokoWaaS Cache Cleaner"
MOD_MOKOWAAS_CACHE_DESC="One-click cache and temp cleaner in the admin status bar."
MOD_MOKOWAAS_CACHE_CLEAR_ALL="Clear All Cache"
MOD_MOKOWAAS_CACHE_CLEAR_TEMP="Clear Temp"
@@ -0,0 +1,2 @@
MOD_MOKOWAAS_CACHE="MokoWaaS Cache Cleaner"
MOD_MOKOWAAS_CACHE_DESC="One-click cache cleaner in the admin status bar. Clears all Joomla cache (site, admin, and expired)."
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokowaas_cache</name>
<author>Moko Consulting</author>
<creationDate>2026-06-04</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.34.15</version>
<description>MOD_MOKOWAAS_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoWaaSCache</namespace>
<files>
<folder module="mod_mokowaas_cache">services</folder>
<folder>src</folder>
<folder>tmpl</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/mod_mokowaas_cache.ini</language>
<language tag="en-GB">en-GB/mod_mokowaas_cache.sys.ini</language>
</languages>
</extension>
@@ -0,0 +1,23 @@
<?php
/**
* @package MokoWaaS
* @subpackage mod_mokowaas_cache
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\Service\Provider\Module;
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
return new class implements ServiceProviderInterface
{
public function register(Container $container): void
{
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoWaaSCache'));
$container->registerServiceProvider(new Module());
}
};

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