Compare commits

...

83 Commits

Author SHA1 Message Date
gitea-actions[bot] 9b0f67a43a chore(version): pre-release bump to 02.47.81-dev [skip ci] 2026-06-23 22:32:57 +00:00
gitea-actions[bot] 96fe631e61 chore(version): auto-bump patch 02.47.80-dev [skip ci] 2026-06-23 22:32:12 +00:00
Jonathan Miller 35a3d40c6a fix: address all 9 PR review findings
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 1m0s
Generic: Project CI / Lint & Validate (pull_request) Failing after 24s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 50s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 55s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 11s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 50s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 54s
1. Sourcerer: expanded forbidden function blocklist (+12 functions),
   added backtick, variable function, and string concat detection
2. Removed orphaned ticket API routes from webservices plugin
3. Removed orphaned tickets submenu from component manifest
4. Added mokosuiteclient.htaccess ACL action to access.xml + language
5. Removed dead AutomationEngine/NotificationService calls from plugin
6. SSL verify enabled in ExtensionsModel (install + update fetch)
7. ConditionsHelper: escape # delimiter in URL regex patterns
8. ReReplacer: escape / delimiter in user-supplied regex patterns
9. CLAUDE.md: removed ticket references, added content tools
2026-06-23 17:31:09 -05:00
gitea-actions[bot] 51185b548f chore(version): pre-release bump to 02.47.79-dev [skip ci] 2026-06-23 22:10:14 +00:00
gitea-actions[bot] 4d534c724d chore(version): auto-bump patch 02.47.78-dev [skip ci] 2026-06-23 22:09:56 +00:00
Jonathan Miller 333966416b feat: support PIN on-demand with 72-hour TTL + controller cleanup
Generic: Project CI / Tests (pull_request) Blocked by required conditions
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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 41s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m1s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m2s
- PIN hidden by default — "Request PIN" button generates on click
- PIN valid for 72 hours (stored as support_pin_requested_at in params)
- HMAC uses 72h window instead of daily date for stability
- requestPin() AJAX endpoint in controller stores timestamp + returns PIN
- Applied to both dashboard info bar and cpanel module
- Dashboard JS handles PIN request with badge replacement
- Cpanel JS handles same with inline script
- Fixed orphaned ticket code fragments in controller (syntax error)
- Removed duplicate maintenance section
2026-06-23 17:09:36 -05:00
gitea-actions[bot] 8f3d3cea8b chore(version): pre-release bump to 02.47.77-dev [skip ci] 2026-06-23 19:32:02 +00:00
gitea-actions[bot] 44664426f5 chore(version): auto-bump patch 02.47.76-dev [skip ci] 2026-06-23 19:31:37 +00:00
Jonathan Miller 37721cd061 feat: full extension catalog + channel gating by domain
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Generic: Project CI / Lint & Validate (pull_request) Failing after 39s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 35s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m11s
- catalog.xml: all 21 MokoSuite repos listed with proper categories
  (Platform, Business, Industry, Content, SEO, Modules, Templates)
- Update server URLs point to main branch (stable releases)
- Dev channel only available on *.mokoconsulting.tech domains
- Non-Moko domains forced to stable regardless of Joomla config
- ExtensionsModel respects Joomla's com_installer update_channel
  param on Moko domains, skips dev-tagged versions on stable channel
2026-06-23 14:31:07 -05:00
gitea-actions[bot] ef6052006e chore(version): pre-release bump to 02.47.75-dev [skip ci] 2026-06-23 19:27:44 +00:00
gitea-actions[bot] 19b3b33d70 chore(version): auto-bump patch 02.47.74-dev [skip ci] 2026-06-23 19:27:16 +00:00
Jonathan Miller 0a374ac8d5 feat: fuzzy detection of all MokoSuite/MokoJoom ecosystem packages
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
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Project CI / Lint & Validate (pull_request) Failing after 17s
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 19s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
- DashboardModel.getMokoExtensions() now fuzzy-matches mokosuite*,
  mokosuiteclient*, mokosuitehq*, mokosuitecrm*, mokojoom* across
  packages, components, modules, plugins, and libraries
- Each extension tagged with product family for grouping
- Heartbeat payload includes moko_packages map (element → version)
  so HQ can see all installed MokoSuite products per site
- Menu module already catches com_moko% (no change needed)
2026-06-23 14:26:52 -05:00
gitea-actions[bot] 32e76ecc75 chore(version): pre-release bump to 02.47.73-dev [skip ci] 2026-06-23 19:25:32 +00:00
gitea-actions[bot] d89d0f95f6 chore(version): auto-bump patch 02.47.72-dev [skip ci] 2026-06-23 19:25:01 +00:00
Jonathan Miller 3c4fe24056 feat: enforce ACLs in dashboard WAF sections and sidebar menu
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
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
Generic: Project CI / Lint & Validate (pull_request) Failing after 57s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 59s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m1s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m9s
- Dashboard: WAF chart and Recent WAF Blocks gated behind
  mokosuiteclient.security.waflog ACL
- Menu: all items ACL-gated, added Snippets/Templates/Replacements/
  Conditions entries, removed ticket items, super admins bypass all
2026-06-23 14:24:36 -05:00
gitea-actions[bot] abfdbbcaa2 chore(version): pre-release bump to 02.47.71-dev [skip ci] 2026-06-23 19:23:30 +00:00
gitea-actions[bot] 6e03ff7560 chore(version): auto-bump patch 02.47.70-dev [skip ci] 2026-06-23 19:22:56 +00:00
Jonathan Miller 8cc8cadda2 chore: remove all ticket/helpdesk code — migrated to MokoSuiteCRM
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
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 19s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 49s
Generic: Project CI / Lint & Validate (pull_request) Failing after 49s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 50s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 32s
- Controller: removed 20+ ticket methods (createTicket, addReply,
  updateStatus, saveCategory, saveCanned, uploadAttachment, etc.)
- Controller: updated VIEW_ACL map — removed tickets/canned/categories,
  added snippets/templates/replacements/conditions
- Controller: WAF log ACL changed from core.admin to security.waflog
- Deleted: TicketsModel, AttachmentService, AutomationEngine,
  NotificationService, TicketsController (API)
- Deleted: all Ticket/Tickets/Ticketsettings/Canned views and templates
  (admin and site)
- Removed importAts method
2026-06-23 14:22:34 -05:00
gitea-actions[bot] 10ef685ab4 chore(version): pre-release bump to 02.47.69-dev [skip ci] 2026-06-23 18:35:28 +00:00
gitea-actions[bot] 79eaebf8a1 chore(version): auto-bump patch 02.47.68-dev [skip ci] 2026-06-23 18:35:01 +00:00
Jonathan Miller 50beb170e4 chore: update access.xml, config.xml, language for all new features
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 23s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 24s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 12s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 37s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
access.xml: removed stale ticket ACLs, added waflog, impersonate,
  snippets, templates, replacements, conditions actions
config.xml: removed helpdesk/IMAP sections (migrated to CRM), added
  content tools, impersonation settings, updated branding to MokoSuite
language: added all new ACL keys, removed ticket references
2026-06-23 13:28:27 -05:00
gitea-actions[bot] 9418e56dfe chore(version): pre-release bump to 02.47.67-dev [skip ci] 2026-06-23 18:19:04 +00:00
gitea-actions[bot] 157a8a9453 chore(version): pre-release bump to 02.47.66-dev [skip ci] 2026-06-23 18:08:32 +00:00
gitea-actions[bot] 3277ca18c9 chore(version): pre-release bump to 02.47.65-dev [skip ci] 2026-06-23 18:05:58 +00:00
jmiller 4c815e7e81 chore: remove security-audit.yml -- handled by MokoGitea
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 33s
Generic: Project CI / Lint & Validate (pull_request) Successful in 9s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 44s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 39s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 30s
2026-06-23 18:05:14 +00:00
Jonathan Miller 60a541fec1 feat: Articles Field, Content Templater — complete Regular Labs suite
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Auto Version Bump / Version Bump (push) Successful in 24s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 59s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 51s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 55s
- Articles Field (#162): custom ListField type for article selection
  in any Joomla form, shows "Title [Category]" options
- Content Templater (#165): {template alias="x"} tags load pre-defined
  article templates from DB, returns introtext+fulltext from JSON
- content_templates table added to install.mysql.sql
- All Regular Labs replacements now implemented:
  #160 conditions + AMM, #161 articles anywhere, #162 articles field,
  #164 conditional content, #165 content templater, #167 email protector,
  #175 rereplacer, #176 snippets, #177 sourcerer, #180 users anywhere,
  #181 cache cleaner
2026-06-23 13:01:32 -05:00
jmiller 2702aea14a chore: remove deploy-manual.yml -- no longer needed
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m13s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m0s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
2026-06-23 17:59:44 +00:00
gitea-actions[bot] 32cd96c92b chore(version): pre-release bump to 02.47.64-dev [skip ci] 2026-06-23 17:57:48 +00:00
gitea-actions[bot] da7f4578d2 chore(version): auto-bump patch 02.47.63-dev [skip ci] 2026-06-23 17:56:56 +00:00
Jonathan Miller db9c68dc5f feat: Articles Anywhere, Sourcerer, Users Anywhere tag engines
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 18s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 53s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 20s
- Articles Anywhere (#161): {article id="42"}[title][introtext]{/article}
  with 18 data tags, date formatting, introtext truncation
- Sourcerer (#177): {source}<?php ... ?>{/source} with forbidden
  function blocklist, PHP execution via eval with output buffering
- Users Anywhere (#180): {user name} for current user, {user id="42"}
  [name][email]{/user} for specific users, email/username masking
- All controlled by params (off by default)
2026-06-23 12:56:39 -05:00
gitea-actions[bot] e513c757b9 chore(version): pre-release bump to 02.47.62-dev [skip ci] 2026-06-23 17:54:05 +00:00
gitea-actions[bot] ce15178dfd chore(version): auto-bump patch 02.47.61-dev [skip ci] 2026-06-23 17:53:41 +00:00
Jonathan Miller 377076e60f style(dashboard): show toggle switch for all configured 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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (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
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 34s
Generic: Project CI / Lint & Validate (pull_request) Successful in 55s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m0s
Toggle switch shown for any plugin with extension_id (installed).
Protected plugins still show badge only. Unconfigured plugins
show no switch or configure button. Removes configure_only logic.
2026-06-23 12:53:20 -05:00
gitea-actions[bot] ed399998d4 chore(version): pre-release bump to 02.47.60-dev [skip ci] 2026-06-23 17:46:39 +00:00
gitea-actions[bot] 50356f8b05 chore(version): auto-bump patch 02.47.59-dev [skip ci] 2026-06-23 17:46:26 +00:00
Jonathan Miller 65ffa835d9 feat: email protector + snippets engine
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 39s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 49s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 43s
- Email Protector (#167): base64 obfuscation with JS decloaking,
  protects mailto links and plain emails, inject script before </body>
- Snippets (#176): {snippet alias="x" var="val"} tags with DB table,
  variable substitution, nested snippet support (max depth 5)
- Both controlled by params (off by default)
- Snippets table added to install.mysql.sql
2026-06-23 12:46:03 -05:00
gitea-actions[bot] a91d78beff chore(version): pre-release bump to 02.47.58-dev [skip ci] 2026-06-23 17:38:27 +00:00
gitea-actions[bot] 561ea3691a chore(version): auto-bump patch 02.47.57-dev [skip ci] 2026-06-23 17:37:51 +00:00
Jonathan Miller 1fe7c77fbf feat(conditional-content): {show}/{hide} tag processing with conditions engine
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 47s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 59s
- processConditionalTags() handles {show}, {hide}, {else} blocks
- Supports condition="alias_or_id" for saved condition sets
- Supports inline rules: access_level, user_group, menu_item, home_page, date, day, url
- Multiple inline rules per tag use AND logic
- Nested tag support (processes innermost first, up to 10 iterations)
- Runs in onContentPrepare (articles) and onAfterRender (final HTML)
- ConditionsHelper: added passByAlias(), resolveAlias(), evaluateInlineRule()
- Implements #164
2026-06-23 12:37:31 -05:00
gitea-actions[bot] 3c94ffeff3 chore(version): pre-release bump to 02.47.56-dev [skip ci] 2026-06-23 17:34:38 +00:00
gitea-actions[bot] 9067dc62f7 chore(version): auto-bump patch 02.47.55-dev [skip ci] 2026-06-23 17:34:11 +00:00
Jonathan Miller 36bfe59115 feat(amm): module filtering via onPrepareModuleList + conditions engine
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 15s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 25s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m0s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 27s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m5s
Frontend modules filtered based on condition sets mapped via
ConditionsHelper::shouldDisplay('com_modules', moduleId).
Modules with no conditions pass through. Graceful skip if tables
don't exist. Implements #160.
2026-06-23 12:33:30 -05:00
gitea-actions[bot] 5b1fe5f806 chore(version): pre-release bump to 02.47.54-dev [skip ci] 2026-06-23 17:32:36 +00:00
gitea-actions[bot] 5855d03ae1 chore(version): auto-bump patch 02.47.53-dev [skip ci] 2026-06-23 17:32:22 +00:00
Jonathan Miller 6090682afd feat(cache): auto-clear Joomla cache on content/extension save
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
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Generic: Project CI / Lint & Validate (pull_request) Successful in 35s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 35s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 37s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 44s
- autoClearCache() called from onContentAfterSave and onExtensionAfterSave
- Controlled by auto_clear_cache param (default: off)
- Clears all cache groups via Cache::getInstance()->clean('')
- Silent failure — never breaks save operations
2026-06-23 12:32:01 -05:00
Jonathan Miller ae6719049d feat(conditions): add conditions engine DB schema and evaluation API
- Four tables: conditions, conditions_groups, conditions_rules, conditions_map
- ConditionsHelper with pass(), load(), shouldDisplay(), getConditionsForItem()
- 7 rule types: menu_item, home_page, user_group, access_level, date, day, url
- Hierarchical evaluation: condition → groups (AND/OR) → rules (AND/OR)
- Runtime cache for repeated evaluations within same request
- Foundation for Advanced Module Manager (#160) and Conditional Content (#164)
2026-06-23 12:32:00 -05:00
gitea-actions[bot] 98694e46d6 chore(version): pre-release bump to 02.47.52-dev [skip ci] 2026-06-23 17:30:39 +00:00
gitea-actions[bot] 0a6a4d581c chore(version): auto-bump patch 02.47.51-dev [skip ci] 2026-06-23 17:30:26 +00:00
Jonathan Miller d7efb61207 fix(cpanel): add missing sitename/db_type to helper, pill button group
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (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 4s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 17s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 20s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 40s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 14s
Generic: Project CI / Lint & Validate (pull_request) Successful in 41s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
- CpanelHelper now returns sitename and db_type for info bar
- Cache module renders as single pill with three joined buttons
  (domain key, cache clear, temp clear)
2026-06-23 12:29:53 -05:00
gitea-actions[bot] 1e081139e6 chore(version): pre-release bump to 02.47.50-dev [skip ci] 2026-06-23 17:01:28 +00:00
gitea-actions[bot] 0f81e227fc chore(version): auto-bump patch 02.47.49-dev [skip ci] 2026-06-23 17:01:09 +00:00
Jonathan Miller 57b48520af feat: guided tours framework + DevTools reset tour toggle
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 19s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 18s
Generic: Project CI / Lint & Validate (pull_request) Successful in 48s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
- registerGuidedTours() in script.php postflight registers Welcome and
  Firewall tours via Joomla's #__guidedtours/#__guidedtour_steps tables
- Tours use com_mokosuiteclient.* UIDs, auto-update on package update
- DevTools: reset_tour_prompts one-shot toggle clears all guided tour
  completion flags from #__user_profiles on save
- Language keys added for tour reset field
2026-06-23 11:59:48 -05:00
gitea-actions[bot] bda9ec1192 chore(version): pre-release bump to 02.47.48-dev [skip ci] 2026-06-23 16:48:38 +00:00
gitea-actions[bot] e9af9dc268 chore(version): auto-bump patch 02.47.47-dev [skip ci] 2026-06-23 16:48:18 +00:00
Jonathan Miller d595f23310 fix(cache-module): use Atum header-item structure for status bar items
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 4s
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Successful in 21s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 58s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 57s
Each item (domain, cache, temp) wrapped in proper header-item markup
with header-item-icon and header-item-text to match Joomla Atum CSS.
2026-06-23 11:47:46 -05:00
gitea-actions[bot] 0b95419eb6 chore(version): pre-release bump to 02.47.46-dev [skip ci] 2026-06-23 16:47:38 +00:00
gitea-actions[bot] 8a89bc1296 chore(version): auto-bump patch 02.47.45-dev [skip ci] 2026-06-23 16:47:25 +00:00
Jonathan Miller f659c73ffa fix(cache-module): fix AJAX URLs and simplify status bar markup
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
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Generic: Project CI / Lint & Validate (pull_request) Successful in 10s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 39s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 37s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 36s
Task URLs corrected to display.clearCache/display.clearTemp format.
Removed custom CSS in favor of inline Bootstrap utilities.
2026-06-23 11:47:08 -05:00
gitea-actions[bot] 8e3cd85e3d chore(version): pre-release bump to 02.47.44-dev [skip ci] 2026-06-23 16:46:35 +00:00
gitea-actions[bot] 865b769a71 chore(version): auto-bump patch 02.47.43-dev [skip ci] 2026-06-23 16:46:15 +00:00
Jonathan Miller 10c2c4bbc7 style(cpanel): remove dashboard button, shield links based on ACL
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 21s
Universal: PR Check / Validate PR (pull_request) Failing after 10s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 21s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 48s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 46s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 55s
Shield icon links to dashboard only if user has core.manage permission
on com_mokosuiteclient. Otherwise renders as static icon.
2026-06-23 11:45:50 -05:00
gitea-actions[bot] b8cd65c45c chore(version): pre-release bump to 02.47.42-dev [skip ci] 2026-06-23 16:44:41 +00:00
gitea-actions[bot] f86d598610 chore(version): auto-bump patch 02.47.41-dev [skip ci] 2026-06-23 16:44:21 +00:00
Jonathan Miller 06c618dd50 style(cpanel): simplify to single info line with clickable shield
Generic: Project CI / Tests (pull_request) Blocked by required conditions
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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 18s
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 12s
Universal: PR Check / Validate PR (pull_request) Failing after 11s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 22s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 58s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m5s
Single row: shield link, site name, badges, IP, dashboard button.
Removed collapsible body, plugin list, health stats, cache button.
2026-06-23 11:43:53 -05:00
gitea-actions[bot] 56fc3dc065 chore(version): auto-bump patch 02.47.40-dev [skip ci] 2026-06-23 16:41:26 +00:00
Jonathan Miller 8fa87ef1d7 style: label all info bar badges with title + version in both views
Generic: Project CI / Tests (pull_request) Blocked by required conditions
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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 17s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 16s
Universal: Auto Version Bump / Version Bump (push) Successful in 18s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 12s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 50s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 48s
MokoSuite 02.47.xx | Joomla 6.1.1 | PHP 8.4.20 | mysql
2026-06-23 11:41:02 -05:00
gitea-actions[bot] f1cee7268d chore(version): pre-release bump to 02.47.39-dev [skip ci] 2026-06-23 16:28:59 +00:00
gitea-actions[bot] 7c9c81b2a4 chore(version): auto-bump patch 02.47.38-dev [skip ci] 2026-06-23 16:28:40 +00:00
Jonathan Miller c79a76c9d7 style: unify dashboard and cpanel info bar to same inline badge layout
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 12s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 53s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 59s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 41s
Both now use identical structure: shield icon, site name, version badge,
PIN badge, Joomla/PHP/DB badges, debug/offline flags, IP with globe.
Dashboard button restored on cpanel toggle row.
2026-06-23 11:28:27 -05:00
gitea-actions[bot] 08d6140f2a chore(version): pre-release bump to 02.47.37-dev [skip ci] 2026-06-23 16:25:54 +00:00
gitea-actions[bot] 69127e5749 chore(version): pre-release bump to 02.47.36-dev [skip ci] 2026-06-23 16:25:15 +00:00
gitea-actions[bot] f0cf2122f4 chore(version): auto-bump patch 02.47.35-dev [skip ci] 2026-06-23 16:24:49 +00:00
Jonathan Miller d8712c1247 style: add shield icon to dashboard info bar and cpanel module header
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 10s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 19s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Project CI / Lint & Validate (pull_request) Successful in 55s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 57s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 39s
2026-06-23 11:24:35 -05:00
gitea-actions[bot] f4b1059f95 chore(version): pre-release bump to 02.47.34-dev [skip ci] 2026-06-23 16:21:03 +00:00
gitea-actions[bot] 1d3ea606c5 chore(version): auto-bump patch 02.47.33-dev [skip ci] 2026-06-23 16:20:43 +00:00
Jonathan Miller 9d3ec28504 style(cpanel): mirror dashboard info strip in cpanel module header
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
Generic: Project CI / Lint & Validate (pull_request) Successful in 15s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 17s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 47s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 33s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m0s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m3s
Shows site name, MokoSuite version, Support PIN, Joomla/PHP/DB badges,
debug/offline flags, IP, and dashboard link in a single info row.
Collapse toggle moved below with plugin count summary.
2026-06-23 11:20:26 -05:00
gitea-actions[bot] 1c7452f360 chore(version): pre-release bump to 02.47.32-dev [skip ci] 2026-06-23 16:16:16 +00:00
gitea-actions[bot] 46cfd53052 chore(version): auto-bump patch 02.47.31-dev [skip ci] 2026-06-23 16:15:52 +00:00
Jonathan Miller 0456f467c7 feat(dashboard): add heartbeat send button next to Support PIN
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
Generic: Project CI / Tests (pull_request) 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
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Project CI / Lint & Validate (pull_request) Failing after 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 11s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 28s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 51s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 54s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 53s
Upload icon button beside the PIN badge triggers sendHeartbeat AJAX
call to HQ, which includes the health token (HQ derives the PIN).
2026-06-23 11:15:34 -05:00
gitea-actions[bot] aa9f18525e chore(version): pre-release bump to 02.47.30-dev [skip ci] 2026-06-23 16:12:31 +00:00
gitea-actions[bot] 4ccb916895 chore(version): auto-bump patch 02.47.29-dev [skip ci] 2026-06-23 16:12:03 +00:00
Jonathan Miller fe74ea89a5 style(dashboard): remove ext version bar, 3-column plugin cards
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 9s
Universal: Auto Version Bump / Version Bump (push) Successful in 22s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 4s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 31s
Generic: Project CI / Lint & Validate (pull_request) Successful in 1m3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 1m6s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m10s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 1m7s
- Removed extension version bar (versions already shown on each card)
- Plugin cards now 3 per row (col-lg-4) except core category (full width)
2026-06-23 11:11:33 -05:00
gitea-actions[bot] da78796cc1 chore(version): pre-release bump to 02.47.28-dev [skip ci] 2026-06-23 15:55:50 +00:00
78 changed files with 2911 additions and 3504 deletions
+1 -2
View File
@@ -38,7 +38,7 @@ Joomla **package** (`pkg_mokosuiteclient`) with 17 sub-extensions:
### Component (`com_mokosuiteclient`)
- Admin dashboard with plugin management, WAF charts, extension catalog
- Helpdesk ticketing system
- Content tools: snippets, templates, replacements, conditions, articles anywhere, users anywhere
- REST API controllers
### Modules
@@ -50,7 +50,6 @@ Joomla **package** (`pkg_mokosuiteclient`) with 17 sub-extensions:
### Task Plugins
- `plg_task_mokosuiteclientdemo` — scheduled demo site reset
- `plg_task_mokosuiteclientsync` — scheduled content sync
- `plg_task_mokosuiteclient_tickets` — ticket automation
### Update Server
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 02.47.27
# VERSION: 02.47.81
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
-82
View File
@@ -1,82 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: "Universal: Security Audit"
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
pull_request:
branches:
- main
paths:
- 'composer.json'
- 'composer.lock'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
permissions:
contents: read
env:
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
jobs:
audit:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: |
echo "=== Composer Security Audit ==="
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
fi
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
RESULT=$?
if [ $RESULT -ne 0 ]; then
echo "::warning::Composer vulnerabilities found"
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
else
echo "No known vulnerabilities in composer dependencies"
fi
- name: NPM audit
if: hashFiles('package-lock.json') != ''
run: |
echo "=== NPM Security Audit ==="
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
echo "No known vulnerabilities in npm dependencies"
else
echo "::warning::NPM vulnerabilities found"
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
fi
- name: Notify on vulnerabilities
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
run: |
REPO="${{ github.event.repository.name }}"
curl -sS \
-H "Title: ${REPO} has vulnerable dependencies" \
-H "Tags: lock,warning" \
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
+1 -1
View File
@@ -14,7 +14,7 @@
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: ./CHANGELOG.md
VERSION: 02.47.27
VERSION: 02.47.81
BRIEF: Version history using `Keep a Changelog`
-->
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.MokoSuiteClientBrand
INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClientBrand
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for MokoSuiteClientBrand
-->
+1 -1
View File
@@ -15,7 +15,7 @@
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: ./LICENSE.md
VERSION: 02.47.27
VERSION: 02.47.81
BRIEF: Project license (GPL-3.0-or-later)
-->
GNU GENERAL PUBLIC LICENSE
+1 -1
View File
@@ -9,7 +9,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /README.md
BRIEF: MokoSuiteClient platform plugin for Joomla
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 02.47.27
VERSION: 02.47.81
BRIEF: Security vulnerability reporting and handling policy
-->
+2 -2
View File
@@ -11,13 +11,13 @@
INGROUP: MokoSuiteClient.Build
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
FILE: build-guide.md
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/
BRIEF: Build and packaging guide for the MokoSuiteClient system plugin
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
-->
# MokoSuiteClient Build Guide (VERSION: 02.47.27)
# MokoSuiteClient Build Guide (VERSION: 02.47.81)
## 1. Purpose
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/configuration-guide.md
BRIEF: Configuration guide for the MokoSuiteClient system plugin
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
-->
# MokoSuiteClient Configuration Guide (VERSION: 02.47.27)
# MokoSuiteClient Configuration Guide (VERSION: 02.47.81)
## 1. Objective
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/installation-guide.md
BRIEF: Installation guide for the MokoSuiteClient system plugin
NOTE: First document in the guide set
-->
# MokoSuiteClient Installation Guide (VERSION: 02.47.27)
# MokoSuiteClient Installation Guide (VERSION: 02.47.81)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/operations-guide.md
BRIEF: Operational guide for administering and managing the MokoSuiteClient system plugin
NOTE: Defines lifecycle, responsibilities, and operational behaviors
-->
# MokoSuiteClient Operations Guide (VERSION: 02.47.27)
# MokoSuiteClient Operations Guide (VERSION: 02.47.81)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/rollback-and-recovery-guide.md
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
NOTE: Completes the core guide set for Suite plugin governance
-->
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.47.27)
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.47.81)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/testing-guide.md
BRIEF: Testing guide for MokoSuiteClient v02.01.08
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
-->
# MokoSuiteClient Testing Guide (VERSION: 02.47.27)
# MokoSuiteClient Testing Guide (VERSION: 02.47.81)
## 1. Prerequisites
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/troubleshooting-guide.md
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuiteClient plugin
NOTE: Designed for administrators and Suite operations teams
-->
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.47.27)
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.47.81)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/guides/upgrade-and-versioning-guide.md
BRIEF: Guide for updating, versioning, and maintaining the MokoSuiteClient plugin
NOTE: Defines release flow, version rules, and upgrade validation
-->
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.47.27)
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.47.81)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.47.27
VERSION: 02.47.81
PATH: /docs/index.md
BRIEF: Master index of all documentation for the MokoSuiteClient plugin
NOTE: Automatically maintained index for all guide canvases
-->
# MokoSuiteClient Documentation Index (VERSION: 02.47.27)
# MokoSuiteClient Documentation Index (VERSION: 02.47.81)
## Introduction
+2 -2
View File
@@ -11,12 +11,12 @@
INGROUP: MokoSuiteClient
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: /docs/plugin-basic.md
VERSION: 02.47.27
VERSION: 02.47.81
BRIEF: Baseline documentation for the MokoSuiteClient system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoSuiteClient Plugin Overview (VERSION: 02.47.27)
# MokoSuiteClient Plugin Overview (VERSION: 02.47.81)
## Introduction
+1 -1
View File
@@ -10,7 +10,7 @@ DEFGROUP: MokoSuiteClient.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClient
PATH: /docs/update-server.md
VERSION: 02.47.27
VERSION: 02.47.81
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
@@ -1,15 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<access component="com_mokosuiteclient">
<section name="component">
<!-- Core Joomla ACL -->
<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
<!-- Dashboard & UI -->
<action name="mokosuiteclient.dashboard" title="COM_MOKOSUITECLIENT_ACL_DASHBOARD" description="COM_MOKOSUITECLIENT_ACL_DASHBOARD_DESC" />
<action name="mokosuiteclient.extensions" title="COM_MOKOSUITECLIENT_ACL_EXTENSIONS" description="COM_MOKOSUITECLIENT_ACL_EXTENSIONS_DESC" />
<action name="mokosuiteclient.htaccess" title="COM_MOKOSUITECLIENT_ACL_HTACCESS" description="COM_MOKOSUITECLIENT_ACL_HTACCESS_DESC" />
<action name="mokosuiteclient.tickets" title="COM_MOKOSUITECLIENT_ACL_TICKETS" description="COM_MOKOSUITECLIENT_ACL_TICKETS_DESC" />
<action name="mokosuiteclient.tickets.create" title="COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE" description="COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE_DESC" />
<action name="mokosuiteclient.tickets.assign" title="COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN" description="COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN_DESC" />
<action name="mokosuiteclient.plugins.toggle" title="COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE" description="COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE_DESC" />
<action name="mokosuiteclient.cache" title="COM_MOKOSUITECLIENT_ACL_CACHE" description="COM_MOKOSUITECLIENT_ACL_CACHE_DESC" />
<!-- Server Config -->
<action name="mokosuiteclient.htaccess" title="COM_MOKOSUITECLIENT_ACL_HTACCESS" description="COM_MOKOSUITECLIENT_ACL_HTACCESS_DESC" />
<!-- Security -->
<action name="mokosuiteclient.security.waflog" title="COM_MOKOSUITECLIENT_ACL_WAFLOG" description="COM_MOKOSUITECLIENT_ACL_WAFLOG_DESC" />
<action name="mokosuiteclient.security.impersonate" title="COM_MOKOSUITECLIENT_ACL_IMPERSONATE" description="COM_MOKOSUITECLIENT_ACL_IMPERSONATE_DESC" />
<!-- Content Tools -->
<action name="mokosuiteclient.snippets.manage" title="COM_MOKOSUITECLIENT_ACL_SNIPPETS" description="COM_MOKOSUITECLIENT_ACL_SNIPPETS_DESC" />
<action name="mokosuiteclient.templates.manage" title="COM_MOKOSUITECLIENT_ACL_TEMPLATES" description="COM_MOKOSUITECLIENT_ACL_TEMPLATES_DESC" />
<action name="mokosuiteclient.replacements.manage" title="COM_MOKOSUITECLIENT_ACL_REPLACEMENTS" description="COM_MOKOSUITECLIENT_ACL_REPLACEMENTS_DESC" />
<action name="mokosuiteclient.conditions.manage" title="COM_MOKOSUITECLIENT_ACL_CONDITIONS" description="COM_MOKOSUITECLIENT_ACL_CONDITIONS_DESC" />
<!-- Extensions & Catalog -->
<action name="mokosuiteclient.extensions" title="COM_MOKOSUITECLIENT_ACL_EXTENSIONS" description="COM_MOKOSUITECLIENT_ACL_EXTENSIONS_DESC" />
</section>
</access>
@@ -1,122 +1,219 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Extension catalog for MokoSuiteClient Extension Manager.
Each entry points to the extension's own updates.xml. The installer
resolves the latest version and download URL at runtime, respecting
the site's configured update channel (dev/stable).
To add an extension: copy an <extension> block and fill in the fields.
MokoSuite Extension Catalog
Each entry points to the extension's updates.xml on the main branch.
The installer resolves the latest version and download URL at runtime,
respecting the site's configured update channel (stable/dev) from
Joomla's com_installer params.
-->
<catalog>
<!-- ═══════════════════════════════════════════════════════════════════
Platform (Layer 0)
═══════════════════════════════════════════════════════════════════ -->
<extension>
<name>MokoSuiteClient</name>
<element>pkg_mokosuiteclient</element>
<type>package</type>
<description>Admin dashboard, security firewall, tenant restrictions, health monitoring, and REST API.</description>
<description>Admin dashboard, security firewall, tenant restrictions, health monitoring, content tools, and REST API.</description>
<icon>icon-shield-alt</icon>
<category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokosuiteclient-platform</article>
<protected>true</protected>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/dev/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteClientHQ</name>
<element>pkg_mokosuiteclienthq</element>
<name>MokoSuiteHQ</name>
<element>pkg_mokosuitehq</element>
<type>package</type>
<description>Centralized control panel for managing all MokoSuiteClient client installations.</description>
<description>Centralized control panel for managing all MokoSuite client installations.</description>
<icon>icon-tachometer-alt</icon>
<category>Platform</category>
<article>https://mokoconsulting.tech/support/products/mokosuiteclient-base</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClientHQ/raw/branch/dev/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHQ/raw/branch/main/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 MokoSuiteClient 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>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>SEO</category>
<article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteClientBackup</name>
<element>pkg_mokojoombackup</element>
<name>MokoSuiteBackup</name>
<element>pkg_mokosuitebackup</element>
<type>package</type>
<description>Full-site backup and restore for Joomla — database, files, and configuration.</description>
<icon>icon-archive</icon>
<category>Tools</category>
<article>https://mokoconsulting.tech/support/products/mokosuiteclientbackup</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClientBackup/raw/branch/dev/updates.xml</updateserver>
<category>Platform</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
Business Suite (Layers 1-4)
═══════════════════════════════════════════════════════════════════ -->
<extension>
<name>MokoSuiteCRM</name>
<element>pkg_mokosuitecrm</element>
<type>package</type>
<description>Layer 1 — Contacts, deals pipeline, activities, e-signature, email integration, helpdesk.</description>
<icon>icon-address-book</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCRM/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteERP</name>
<element>pkg_mokosuiteerp</element>
<type>package</type>
<description>Layer 2 — Products, orders, invoicing, inventory, warehouses, accounting, payments.</description>
<icon>icon-briefcase</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteERP/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteShop</name>
<element>pkg_mokosuiteshop</element>
<type>package</type>
<description>Layer 3 — Product catalog, shopping cart, checkout, coupons. Requires MokoSuiteERP.</description>
<icon>icon-shopping-cart</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteShop/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuitePOS</name>
<element>pkg_mokosuitepos</element>
<type>package</type>
<description>Layer 3 — Touch-screen POS, multi-terminal, cash register, receipt printing.</description>
<icon>icon-calculator</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuitePOS/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteMRP</name>
<element>pkg_mokosuitemrp</element>
<type>package</type>
<description>Layer 3 — BOM, manufacturing orders, workstation management, production scheduling.</description>
<icon>icon-cog</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteMRP/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteHRM</name>
<element>pkg_mokosuitehrm</element>
<type>package</type>
<description>Layer 3 — Human Resource Management: employees, leave, expenses, payroll, recruiting.</description>
<icon>icon-users</icon>
<category>Business</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHRM/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteRestaurant</name>
<element>pkg_mokosuiterestaurant</element>
<type>package</type>
<description>Layer 4 — Floor plan, table management, kitchen display, split bills, online ordering.</description>
<icon>icon-utensils</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteRestaurant/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteChild</name>
<element>pkg_mokosuitechild</element>
<type>package</type>
<description>Layer 2 — Child Care Management: enrollment, attendance, billing, parent portal.</description>
<icon>icon-child</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteChild/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteNPO</name>
<element>pkg_mokosuitenpo</element>
<type>package</type>
<description>Nonprofit management: donors, donations, campaigns, grants, volunteers, events.</description>
<icon>icon-heart</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteNPO/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteField</name>
<element>pkg_mokosuitefield</element>
<type>package</type>
<description>Field Service — dispatch, work orders, scheduling, mobile tech, plumbing/HVAC.</description>
<icon>icon-wrench</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCreate</name>
<element>pkg_mokosuitecreate</element>
<type>package</type>
<description>Layer 2 — Creative Agency: projects, tasks, timesheets, client proofing.</description>
<icon>icon-paint-brush</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCreate/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
Content & Community
═══════════════════════════════════════════════════════════════════ -->
<extension>
<name>MokoSuiteForms</name>
<element>pkg_mokosuiteforms</element>
<type>package</type>
<description>Form builder — custom forms, submissions, notifications, and data exports.</description>
<icon>icon-list-alt</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteForms/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCommunity</name>
<element>pkg_mokosuitecommunity</element>
<type>package</type>
<description>Community profiles, connections, and activity streams for Joomla.</description>
<icon>icon-users</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCommunity/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCross</name>
<element>pkg_mokosuitecross</element>
<type>package</type>
<description>Cross-posting Joomla content to social media, email marketing, and chat platforms.</description>
<icon>icon-share-alt</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteOpenGraph</name>
<element>pkg_mokosuiteopengraph</element>
<type>package</type>
<description>Open Graph, Twitter Card, JSON-LD structured data, and social sharing meta tags.</description>
<icon>icon-share-alt</icon>
<category>SEO</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteStoreLocator</name>
<element>pkg_mokosuitestorelocator</element>
<type>package</type>
<description>Interactive map, location search, and admin management for store locations.</description>
<icon>icon-map-marker-alt</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
Standalone Extensions (MokoJoom)
═══════════════════════════════════════════════════════════════════ -->
<extension>
<name>MokoJoomHero</name>
<element>mod_mokojoomhero</element>
<type>module</type>
<description>Random hero image module from a configurable folder.</description>
<description>Hero module — image slideshow, video backgrounds, solid color/gradient, parallax.</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>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
Templates
═══════════════════════════════════════════════════════════════════ -->
<extension>
<name>MokoJoomCommunity</name>
<element>pkg_mokojoomcommunity</element>
<type>package</type>
<description>Community Builder integration package with custom fields and user management.</description>
<icon>icon-users</icon>
<category>Community</category>
<article>https://mokoconsulting.tech/support/products/mokojoomcommunity</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCommunity/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomCross</name>
<element>plg_system_mokojoomcross</element>
<type>plugin</type>
<description>Cross-extension integration plugin for Joomla component interoperability.</description>
<icon>icon-link</icon>
<category>Plugins</category>
<article>https://mokoconsulting.tech/support/products/mokojoomcross</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/raw/branch/dev/updates.xml</updateserver>
</extension>
<extension>
<name>MokoJoomStoreLocator</name>
<element>mod_mokojoomstorelocator</element>
<type>module</type>
<description>Store locator module with Google Maps integration and search.</description>
<icon>icon-map-marker-alt</icon>
<category>Modules</category>
<article>https://mokoconsulting.tech/support/products/mokojoomstorelocator</article>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/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>
<name>MokoOnyx</name>
<element>mokoonyx</element>
<type>template</type>
<description>Modern Joomla site template with dark mode, custom layouts, and MokoSuite integration.</description>
<icon>icon-paint-brush</icon>
<category>Templates</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml</updateserver>
</extension>
</catalog>
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<config>
<fieldset name="general" label="General" description="General component settings.">
<field name="brand_name" type="text" default="MokoSuiteClient"
<field name="brand_name" type="text" default="MokoSuite"
label="Brand Name"
description="Displayed in the admin sidebar, dashboard, and emails."
hint="MokoSuiteClient" />
hint="MokoSuite" />
<field name="support_email" type="email" default=""
label="Support Email"
description="Reply-to address for outbound notification emails."
hint="support@example.com" />
</fieldset>
<fieldset name="notifications" label="Email Notifications" description="Configure email recipients for ticket and security notifications.">
<fieldset name="notifications" label="Notifications" description="Email and push notification settings.">
<field name="admin_emails" type="text" default=""
label="Admin Email Addresses"
description="Comma-separated email addresses to receive all notifications."
@@ -31,7 +31,7 @@
<field name="spacer_ntfy" type="spacer" label="Push Notifications (ntfy)" />
<field name="ntfy_enabled" type="radio" default="0"
label="Enable ntfy Push"
description="Send push notifications via ntfy for ticket and security events."
description="Send push notifications via ntfy for security and system events."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
@@ -40,13 +40,13 @@
label="ntfy Server URL"
description="Full URL to your ntfy server."
showon="ntfy_enabled:1" />
<field name="ntfy_topic" type="text" default="mokosuiteclient-tickets"
label="Ticket Topic"
description="ntfy topic name for helpdesk ticket notifications."
<field name="ntfy_topic" type="text" default="mokosuite-alerts"
label="Alert Topic"
description="ntfy topic name for general alert notifications."
showon="ntfy_enabled:1" />
<field name="ntfy_security_topic" type="text" default="mokosuiteclient-security"
<field name="ntfy_security_topic" type="text" default="mokosuite-security"
label="Security Topic"
description="ntfy topic name for security alert notifications. Falls back to ticket topic if empty."
description="ntfy topic name for security alerts. Falls back to alert topic if empty."
showon="ntfy_enabled:1" />
<field name="ntfy_token" type="password" default=""
label="ntfy Auth Token"
@@ -54,59 +54,42 @@
showon="ntfy_enabled:1" />
</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 #__mokosuiteclient_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>
<field name="satisfaction_enabled" type="radio" default="1"
label="Satisfaction Ratings"
description="Show rating prompt on resolved tickets."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="max_attachment_size" type="number" default="10"
label="Max Attachment Size (MB)"
description="Maximum upload size per file in megabytes." />
<fieldset name="content_tools" label="Content Tools" description="Settings for content tag engines and replacements.">
<field name="spacer_snippets" type="spacer" label="Snippets" />
<field name="snippets_default_category" type="text" default=""
label="Default Snippet Category"
description="Category assigned to new snippets if none selected." />
<field name="spacer_templates" type="spacer" label="Content Templates" />
<field name="templates_default_category" type="text" default=""
label="Default Template Category"
description="Category assigned to new content templates if none selected." />
<field name="spacer_replacements" type="spacer" label="Replacements" />
<field name="replacements_max_rules" type="number" default="100"
label="Max Active Rules"
description="Maximum number of replacement rules processed per page load. 0 = unlimited." />
</fieldset>
<fieldset name="email_to_ticket" label="Email-to-Ticket (IMAP)" description="Create tickets from incoming emails via IMAP polling.">
<field name="imap_host" type="text" default=""
label="IMAP Server"
description="IMAP hostname (e.g. imap.gmail.com)"
hint="imap.gmail.com" />
<field name="imap_port" type="number" default="993"
label="Port"
description="IMAP port (993 for SSL, 143 for plain)" />
<field name="imap_ssl" type="radio" default="1"
label="Use SSL"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="imap_user" type="text" default=""
label="Username"
description="IMAP login username or email address." />
<field name="imap_password" type="password" default=""
label="Password"
description="IMAP password or app-specific password." />
<field name="imap_folder" type="text" default="INBOX"
label="Inbox Folder"
description="IMAP folder to poll for new messages." />
<field name="imap_processed_folder" type="text" default="INBOX.Processed"
label="Processed Folder"
description="Move processed emails to this folder. Leave empty to just mark as read." />
<fieldset name="impersonation" label="User Impersonation" description="Skeleton Key — log into the frontend as another user for support.">
<field name="skeleton_key_control_groups" type="usergrouplist" default="8"
label="Groups Allowed to Impersonate"
description="User groups that can log in as another user."
multiple="true"
layout="joomla.form.field.list-fancy-select" />
<field name="skeleton_key_target_groups" type="usergrouplist" default="2"
label="Groups That Can Be Impersonated"
description="User groups whose accounts can be accessed via impersonation."
multiple="true"
layout="joomla.form.field.list-fancy-select" />
<field name="skeleton_key_blocked_groups" type="usergrouplist" default="7,8"
label="Groups That Cannot Be Impersonated"
description="User groups protected from impersonation (overrides target groups)."
multiple="true"
layout="joomla.form.field.list-fancy-select" />
<field name="skeleton_key_cookie_lifetime" type="number" default="10"
label="Cookie Lifetime (seconds)"
description="How long the impersonation cookie remains valid. Short values are more secure." />
</fieldset>
<fieldset name="permissions" label="COM_MOKOSUITECLIENT_ACL_TITLE"
@@ -1,10 +1,10 @@
; MokoSuiteClient Admin Dashboard - Language Strings
; MokoSuite Admin Dashboard - Language Strings
; Copyright (C) 2026 Moko Consulting. All rights reserved.
; License: GPL-3.0-or-later
COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuiteClient Control Panel"
COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuite Control Panel"
; Joomla core fallback keys (in case language files are corrupt/missing)
; Joomla core fallback keys
COM_ACTIONLOGS_DISABLED="User Action Logging is disabled. Please enable the &quot;Action Log - Joomla&quot; plugin."
COM_MOKOSUITECLIENT_SITE="Site"
COM_MOKOSUITECLIENT_DATABASE="Database"
@@ -23,22 +23,29 @@ COM_MOKOSUITECLIENT_EXTENSIONS_TITLE="Moko Extensions"
COM_MOKOSUITECLIENT_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_MOKOSUITECLIENT_EXTENSIONS_LINK="Moko Extensions"
COM_MOKOSUITECLIENT_HTACCESS_TITLE=".htaccess Maker"
COM_MOKOSUITECLIENT_TICKETS_TITLE="Helpdesk"
; ACL
COM_MOKOSUITECLIENT_ACL_TITLE="Permissions"
COM_MOKOSUITECLIENT_ACL_DESC="Manage access permissions for MokoSuite component features."
COM_MOKOSUITECLIENT_ACL_DASHBOARD="View Dashboard"
COM_MOKOSUITECLIENT_ACL_DASHBOARD_DESC="Allow viewing the MokoSuiteClient control panel dashboard."
COM_MOKOSUITECLIENT_ACL_DASHBOARD_DESC="Allow viewing the MokoSuite control panel dashboard."
COM_MOKOSUITECLIENT_ACL_EXTENSIONS="Manage Extensions"
COM_MOKOSUITECLIENT_ACL_EXTENSIONS_DESC="Allow installing and uninstalling Moko extensions."
COM_MOKOSUITECLIENT_ACL_HTACCESS="Manage .htaccess"
COM_MOKOSUITECLIENT_ACL_HTACCESS_DESC="Allow editing and saving the .htaccess configuration."
COM_MOKOSUITECLIENT_ACL_TICKETS="View Tickets"
COM_MOKOSUITECLIENT_ACL_TICKETS_DESC="Allow viewing helpdesk tickets."
COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE="Create Tickets"
COM_MOKOSUITECLIENT_ACL_TICKETS_CREATE_DESC="Allow creating new helpdesk tickets."
COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN="Assign Tickets"
COM_MOKOSUITECLIENT_ACL_TICKETS_ASSIGN_DESC="Allow assigning tickets to other users."
COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE="Toggle Plugins"
COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoSuiteClient feature plugins."
COM_MOKOSUITECLIENT_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoSuite feature plugins."
COM_MOKOSUITECLIENT_ACL_CACHE="Clear Cache"
COM_MOKOSUITECLIENT_ACL_CACHE_DESC="Allow clearing the Joomla cache from the dashboard."
COM_MOKOSUITECLIENT_ACL_HTACCESS="Manage .htaccess"
COM_MOKOSUITECLIENT_ACL_HTACCESS_DESC="Allow editing and saving the .htaccess and nginx configuration."
COM_MOKOSUITECLIENT_ACL_WAFLOG="View WAF Log"
COM_MOKOSUITECLIENT_ACL_WAFLOG_DESC="Allow viewing the Web Application Firewall activity log."
COM_MOKOSUITECLIENT_ACL_IMPERSONATE="Impersonate Users"
COM_MOKOSUITECLIENT_ACL_IMPERSONATE_DESC="Allow logging into the frontend as another user for support purposes."
COM_MOKOSUITECLIENT_ACL_SNIPPETS="Manage Snippets"
COM_MOKOSUITECLIENT_ACL_SNIPPETS_DESC="Allow creating, editing, and deleting reusable content snippets."
COM_MOKOSUITECLIENT_ACL_TEMPLATES="Manage Content Templates"
COM_MOKOSUITECLIENT_ACL_TEMPLATES_DESC="Allow creating, editing, and deleting article content templates."
COM_MOKOSUITECLIENT_ACL_REPLACEMENTS="Manage Replacements"
COM_MOKOSUITECLIENT_ACL_REPLACEMENTS_DESC="Allow creating, editing, and deleting text replacement rules."
COM_MOKOSUITECLIENT_ACL_CONDITIONS="Manage Conditions"
COM_MOKOSUITECLIENT_ACL_CONDITIONS_DESC="Allow creating, editing, and deleting display condition sets for modules and content."
@@ -227,3 +227,126 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_license_cache` (
PRIMARY KEY (`dlid_hash`),
KEY `idx_checked` (`checked_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- Conditions Engine — rule-based display conditions
-- ============================================================
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(100) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`match_all` TINYINT(1) NOT NULL DEFAULT 1,
`published` TINYINT(1) NOT NULL DEFAULT 1,
`hash` VARCHAR(32) NOT NULL DEFAULT '',
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_groups` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`condition_id` INT UNSIGNED NOT NULL,
`match_all` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_condition` (`condition_id`),
KEY `idx_ordering` (`condition_id`, `ordering`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_rules` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`group_id` INT UNSIGNED NOT NULL,
`type` VARCHAR(50) NOT NULL DEFAULT '',
`exclude` TINYINT(1) NOT NULL DEFAULT 0,
`params` TEXT NOT NULL,
`ordering` INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_group` (`group_id`),
KEY `idx_type` (`type`),
KEY `idx_ordering` (`group_id`, `ordering`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_conditions_map` (
`condition_id` INT UNSIGNED NOT NULL,
`extension` VARCHAR(50) NOT NULL DEFAULT '',
`item_id` INT UNSIGNED NOT NULL DEFAULT 0,
UNIQUE KEY `idx_unique` (`condition_id`, `item_id`, `extension`),
KEY `idx_ext_item` (`extension`, `item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================================
-- Snippets — reusable text/HTML blocks insertable via {snippet}
-- ============================================================
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_snippets` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(100) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`content` MEDIUMTEXT NOT NULL,
`params` TEXT NOT NULL,
`published` TINYINT(1) NOT NULL DEFAULT 0,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_alias` (`alias`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================================
-- ReReplacer — backend-managed string/regex replacement rules
-- ============================================================
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_replacements` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL DEFAULT '',
`search` TEXT NOT NULL,
`replace_value` TEXT NOT NULL,
`area` VARCHAR(20) NOT NULL DEFAULT 'both',
`regex` TINYINT(1) NOT NULL DEFAULT 0,
`casesensitive` TINYINT(1) NOT NULL DEFAULT 0,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`published` TINYINT(1) NOT NULL DEFAULT 0,
`description` TEXT NOT NULL,
`enable_in_admin` TINYINT(1) NOT NULL DEFAULT 0,
`color` VARCHAR(8) DEFAULT NULL,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Content Templates
--
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_content_templates` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(255) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`template_data` MEDIUMTEXT NOT NULL,
`joomla_category_id` INT NOT NULL DEFAULT 0,
`access` INT UNSIGNED NOT NULL DEFAULT 1,
`published` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_alias` (`alias`),
KEY `idx_category` (`joomla_category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -24,19 +24,18 @@ class DisplayController extends BaseController
* ACL map: view name => required permission.
*/
private const VIEW_ACL = [
'dashboard' => 'mokosuiteclient.dashboard',
'extensions' => 'mokosuiteclient.extensions',
'htaccess' => 'mokosuiteclient.htaccess',
'tickets' => 'mokosuiteclient.tickets',
'ticket' => 'mokosuiteclient.tickets',
'privacy' => 'core.admin',
'waflog' => 'core.admin',
'categories' => 'mokosuiteclient.tickets',
'canned' => 'mokosuiteclient.tickets',
'automation' => 'core.admin',
'database' => 'core.admin',
'cleanup' => 'mokosuiteclient.cache',
'ticketsettings' => 'core.admin',
'dashboard' => 'mokosuiteclient.dashboard',
'extensions' => 'mokosuiteclient.extensions',
'htaccess' => 'mokosuiteclient.htaccess',
'privacy' => 'core.admin',
'waflog' => 'mokosuiteclient.security.waflog',
'automation' => 'core.admin',
'database' => 'core.admin',
'cleanup' => 'mokosuiteclient.cache',
'snippets' => 'mokosuiteclient.snippets.manage',
'templates' => 'mokosuiteclient.templates.manage',
'replacements' => 'mokosuiteclient.replacements.manage',
'conditions' => 'mokosuiteclient.conditions.manage',
];
public function display($cachable = false, $urlparams = [])
@@ -142,6 +141,22 @@ class DisplayController extends BaseController
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
$timestamp = time();
// Discover all MokoSuite ecosystem packages for HQ
$mokoPackages = [];
try {
$pkgDb = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$pkgQuery = $pkgDb->getQuery(true)
->select([$pkgDb->quoteName('element'), $pkgDb->quoteName('manifest_cache')])
->from($pkgDb->quoteName('#__extensions'))
->where('(' . $pkgDb->quoteName('element') . ' LIKE ' . $pkgDb->quote('pkg_mokosuite%')
. ' OR ' . $pkgDb->quoteName('element') . ' LIKE ' . $pkgDb->quote('pkg_mokojoom%') . ')');
$pkgDb->setQuery($pkgQuery);
foreach ($pkgDb->loadObjectList() ?: [] as $pkg) {
$m = json_decode($pkg->manifest_cache ?? '{}');
$mokoPackages[$pkg->element] = $m->version ?? '';
}
} catch (\Throwable $e) {}
$payload = json_encode([
'token' => $healthToken,
'domain' => $domain,
@@ -150,6 +165,7 @@ class DisplayController extends BaseController
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
'php_version' => PHP_VERSION,
'timestamp' => $timestamp,
'moko_packages' => $mokoPackages,
], JSON_UNESCAPED_SLASHES);
// RSA sign the request
@@ -348,186 +364,67 @@ class DisplayController extends BaseController
}
// ==================================================================
// Tickets
// Support PIN
// ==================================================================
public function createTicket()
public function requestPin()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets.create'))
if (!$this->checkAcl('mokosuiteclient.dashboard'))
{
$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),
'contact_id' => $input->getInt('contact_id', 0),
'assign_users' => $input->get('assign_users', [], 'ARRAY'),
'assign_groups' => $input->get('assign_groups', [], 'ARRAY'),
'custom_fields' => $input->get('custom_fields', [], 'ARRAY'),
]));
}
public function addTicketReply()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.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('mokosuiteclient.tickets'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->updateStatus(
$input->getInt('ticket_id', 0),
$input->getInt('status', 0)
));
}
// ==================================================================
// Ticket Settings — Status/Priority CRUD
// ==================================================================
public function saveStatus()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->saveStatus([
'id' => $input->getInt('id', 0),
'title' => $input->getString('title', ''),
'alias' => $input->getString('alias', ''),
'color' => $input->getString('color', 'bg-secondary'),
'is_default' => $input->getInt('is_default', 0),
'is_closed' => $input->getInt('is_closed', 0),
'ordering' => $input->getInt('ordering', 0),
]));
}
public function deleteStatus()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->jsonResponse($this->getModel('Tickets')->deleteStatus($id));
}
public function savePriority()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$input = Factory::getApplication()->getInput();
$this->jsonResponse($this->getModel('Tickets')->savePriority([
'id' => $input->getInt('id', 0),
'title' => $input->getString('title', ''),
'alias' => $input->getString('alias', ''),
'color' => $input->getString('color', 'bg-secondary'),
'is_default' => $input->getInt('is_default', 0),
'ordering' => $input->getInt('ordering', 0),
]));
}
public function deletePriority()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$this->jsonResponse($this->getModel('Tickets')->deletePriority($id));
}
// ==================================================================
// KB Search
// ==================================================================
public function searchKb()
{
$query = Factory::getApplication()->getInput()->getString('q', '');
if (strlen($query) < 3)
{
$this->jsonResponse(['results' => []]);
return;
}
try
{
$db = Factory::getDbo();
$escaped = $db->quote('%' . $db->escape($query, true) . '%');
$results = $db->setQuery(
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$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() ?: [];
->select([$db->quoteName('extension_id'), $db->quoteName('params')])
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
);
$ext = $db->loadObject();
foreach ($results as $r)
if (!$ext)
{
$r->description = mb_substr(strip_tags($r->description ?? ''), 0, 150);
$this->jsonResponse(['success' => false, 'message' => 'Core plugin not found.']);
return;
}
$this->jsonResponse(['results' => $results]);
$params = json_decode($ext->params, true) ?: [];
$token = $params['health_api_token'] ?? '';
if (empty($token))
{
$this->jsonResponse(['success' => false, 'message' => 'Health token not configured.']);
return;
}
$now = time();
$params['support_pin_requested_at'] = $now;
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
->where($db->quoteName('extension_id') . ' = ' . (int) $ext->extension_id)
)->execute();
$pinTtl = 72 * 3600;
$window = floor($now / $pinTtl);
$hash = hash_hmac('sha256', (string) $window, $token);
$pin = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
$this->jsonResponse(['success' => true, 'pin' => $pin, 'message' => 'PIN generated — valid for 72 hours.']);
}
catch (\Throwable $e)
{
Log::add('KB search failed: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
$this->jsonResponse(['results' => [], 'error' => 'Search unavailable']);
$this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
}
}
@@ -568,218 +465,6 @@ class DisplayController extends BaseController
$this->jsonResponse($model->cleanDirectory($dirKey));
}
// ==================================================================
// Helpdesk CRUD (#137, #138, #139)
// ==================================================================
public function saveCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$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('#__mokosuiteclient_ticket_categories', $data, 'id');
} else {
$data->ordering = 0;
$db->insertObject('#__mokosuiteclient_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('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
}
public function reorderCategory()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
$db = Factory::getDbo();
foreach ($order as $i => $id) {
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_ticket_categories') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
}
public function saveCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$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('#__mokosuiteclient_ticket_canned', $data, 'id'); }
else { $db->insertObject('#__mokosuiteclient_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('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
}
public function reorderCanned()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
$db = Factory::getDbo();
foreach ($order as $i => $id) {
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_ticket_canned') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
}
public function uploadAttachment()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput();
$ticketId = $input->getInt('ticket_id', 0);
$replyId = $input->getInt('reply_id', 0) ?: null;
if (!$ticketId) { $this->jsonResponse(['success' => false, 'message' => 'Missing ticket_id']); return; }
$files = $input->files->get('attachments', [], 'raw');
if (empty($files) || empty($files['name'])) { $this->jsonResponse(['success' => false, 'message' => 'No files uploaded']); return; }
$saved = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::upload($ticketId, $replyId, $files);
$this->jsonResponse(['success' => true, 'message' => count($saved) . ' file(s) uploaded', 'count' => count($saved)]);
}
public function downloadAttachment()
{
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_ticket_attachments')->where('id = ' . $id));
$att = $db->loadObject();
if (!$att) { throw new \RuntimeException('Attachment not found', 404); }
$path = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::getAbsolutePath($att);
if (!file_exists($path)) { throw new \RuntimeException('File not found', 404); }
$app = Factory::getApplication();
$app->setHeader('Content-Type', $att->mimetype ?: 'application/octet-stream');
$safeName = str_replace(['"', "\r", "\n"], '', $att->filename);
$app->setHeader('Content-Disposition', 'attachment; filename="' . $safeName . '"');
$app->setHeader('Content-Length', (string) filesize($path));
$app->sendHeaders();
readfile($path);
$app->close();
}
public function deleteAttachment()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$ok = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::delete($id);
$this->jsonResponse(['success' => $ok, 'message' => $ok ? 'Attachment deleted' : 'Not found']);
}
public function rateTicket()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets')) { $this->jsonForbidden(); return; }
$input = Factory::getApplication()->getInput();
$ticketId = $input->getInt('ticket_id', 0);
$rating = $input->getInt('rating', 0);
$feedback = $input->getString('feedback', '');
if (!$ticketId || $rating < 1 || $rating > 5) {
$this->jsonResponse(['success' => false, 'message' => 'Invalid rating (1-5)']);
return;
}
$db = Factory::getDbo();
$db->setQuery(
'UPDATE ' . $db->quoteName('#__mokosuiteclient_tickets')
. ' SET satisfaction_rating = ' . $rating
. ', satisfaction_feedback = ' . $db->quote($feedback)
. ', satisfaction_rated_at = ' . $db->quote(Factory::getDate()->toSql())
. ' WHERE id = ' . $ticketId
)->execute();
$this->jsonResponse(['success' => true, 'message' => 'Thank you for your feedback!']);
}
public function saveAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$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', '[]'),
'behavior' => $input->getString('behavior', 'append'),
'enabled' => 1,
'ordering' => 0,
];
$id = $input->getInt('id', 0);
if ($id) { $data->id = $id; $db->updateObject('#__mokosuiteclient_ticket_automation', $data, 'id'); }
else { $db->insertObject('#__mokosuiteclient_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(); return; }
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->delete('#__mokosuiteclient_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(); return; }
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$db->setQuery($db->getQuery(true)->update('#__mokosuiteclient_ticket_automation')
->set('enabled = ' . $input->getInt('enabled', 0))
->where('id = ' . $input->getInt('id', 0)))->execute();
$this->jsonResponse(['success' => true, 'message' => 'Rule updated.']);
}
public function reorderAutomation()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
$order = json_decode(Factory::getApplication()->getInput()->getRaw('order', '[]'), true);
if (!is_array($order)) { $this->jsonResponse(['success' => false, 'message' => 'Invalid order']); return; }
$db = Factory::getDbo();
foreach ($order as $i => $id) {
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_ticket_automation') . ' SET ordering = ' . (int) $i . ' WHERE id = ' . (int) $id)->execute();
}
$this->jsonResponse(['success' => true, 'message' => 'Order saved.']);
}
// ==================================================================
// Settings Import/Export (#132)
// ==================================================================
@@ -891,7 +576,7 @@ class DisplayController extends BaseController
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
if (!$this->checkAcl('mokosuiteclient.security.waflog'))
{
$this->jsonForbidden();
return;
@@ -907,7 +592,7 @@ class DisplayController extends BaseController
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('core.admin'))
if (!$this->checkAcl('mokosuiteclient.security.waflog'))
{
$this->jsonForbidden();
return;
@@ -991,19 +676,6 @@ class DisplayController extends BaseController
// Importers
// ==================================================================
public function importAts()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
if (!$this->checkAcl('mokosuiteclient.tickets'))
{
$this->jsonForbidden();
return;
}
$this->jsonResponse($this->getModel('Import')->importAts());
}
public function importAdminTools()
{
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
@@ -0,0 +1,525 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Helper;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
/**
* Conditions Engine — evaluates rule-based display conditions.
*
* Supports nested groups of rules with AND/OR logic and per-rule exclusion.
*
* @since 02.48.00
*/
class ConditionsHelper
{
/**
* Runtime evaluation cache keyed by condition ID.
*
* @var array<int, bool>
*/
private static array $cache = [];
/**
* Check whether a condition set passes.
*
* @param int $conditionId The condition record ID.
*
* @return bool True when the condition passes (content should display).
*/
public static function pass(int $conditionId): bool
{
if (isset(self::$cache[$conditionId])) {
return self::$cache[$conditionId];
}
$condition = self::load($conditionId);
if ($condition === null || !(int) $condition->published) {
self::$cache[$conditionId] = false;
return false;
}
$groups = $condition->groups ?? [];
if (empty($groups)) {
// No groups means no restrictions — pass.
self::$cache[$conditionId] = true;
return true;
}
$matchAll = (bool) $condition->match_all;
foreach ($groups as $group) {
$groupResult = self::passGroup($group);
if ($matchAll && !$groupResult) {
self::$cache[$conditionId] = false;
return false;
}
if (!$matchAll && $groupResult) {
self::$cache[$conditionId] = true;
return true;
}
}
// match_all: all passed; match_any: none passed.
$result = $matchAll;
self::$cache[$conditionId] = $result;
return $result;
}
/**
* Load a condition with its groups and rules from the database.
*
* @param int $conditionId The condition record ID.
*
* @return object|null The condition object with nested groups/rules, or null.
*/
public static function load(int $conditionId): ?object
{
$db = Factory::getContainer()->get('DatabaseDriver');
// Load the condition record.
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_conditions'))
->where($db->quoteName('id') . ' = :id')
->bind(':id', $conditionId, \Joomla\Database\ParameterType::INTEGER);
$condition = $db->setQuery($query)->loadObject();
if ($condition === null) {
return null;
}
// Load groups.
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_conditions_groups'))
->where($db->quoteName('condition_id') . ' = :cid')
->bind(':cid', $conditionId, \Joomla\Database\ParameterType::INTEGER)
->order($db->quoteName('ordering') . ' ASC');
$groups = $db->setQuery($query)->loadObjectList();
// Load rules for each group.
foreach ($groups as $group) {
$groupId = (int) $group->id;
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_conditions_rules'))
->where($db->quoteName('group_id') . ' = :gid')
->bind(':gid', $groupId, \Joomla\Database\ParameterType::INTEGER)
->order($db->quoteName('ordering') . ' ASC');
$group->rules = $db->setQuery($query)->loadObjectList();
// Decode params JSON on each rule.
foreach ($group->rules as $rule) {
$rule->params = json_decode($rule->params ?: '{}');
}
}
$condition->groups = $groups;
return $condition;
}
/**
* Evaluate a single group (AND/OR its rules).
*
* @param object $group The group object with a rules array.
*
* @return bool
*/
private static function passGroup(object $group): bool
{
$rules = $group->rules ?? [];
if (empty($rules)) {
return true;
}
$matchAll = (bool) $group->match_all;
foreach ($rules as $rule) {
$ruleResult = self::passRule($rule);
// If the rule is an exclusion, invert the result.
if ((int) $rule->exclude) {
$ruleResult = !$ruleResult;
}
if ($matchAll && !$ruleResult) {
return false;
}
if (!$matchAll && $ruleResult) {
return true;
}
}
return $matchAll;
}
/**
* Evaluate a single rule by dispatching to the right type handler.
*
* @param object $rule The rule object (type, params decoded).
*
* @return bool
*/
private static function passRule(object $rule): bool
{
$params = $rule->params ?? new \stdClass();
return match ($rule->type) {
'menu__menu_item' => self::evalMenuMenuItem($params),
'menu__home_page' => self::evalMenuHomePage($params),
'visitor__user_group' => self::evalVisitorUserGroup($params),
'visitor__access_level' => self::evalVisitorAccessLevel($params),
'date__date' => self::evalDateDate($params),
'date__day' => self::evalDateDay($params),
'other__url' => self::evalOtherUrl($params),
default => false,
};
}
// ------------------------------------------------------------------
// Rule type evaluators
// ------------------------------------------------------------------
/**
* menu__menu_item — check if current menu item ID is in selection.
*/
private static function evalMenuMenuItem(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$app = Factory::getApplication();
$itemId = (int) $app->getInput()->getInt('Itemid', 0);
return \in_array($itemId, $selection, true);
}
/**
* menu__home_page — check if current page is the site home page.
*/
private static function evalMenuHomePage(object $params): bool
{
$app = Factory::getApplication();
$menu = $app->getMenu();
if ($menu === null) {
return false;
}
$active = $menu->getActive();
$default = $menu->getDefault($app->getLanguage()->getTag());
$isHome = ($active !== null && $default !== null && $active->id === $default->id);
// params->selection can be [1] for "is home" or [0] for "is not home".
$want = (bool) ($params->selection[0] ?? true);
return $isHome === $want;
}
/**
* visitor__user_group — check if current user belongs to specified groups.
*/
private static function evalVisitorUserGroup(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$user = Factory::getApplication()->getIdentity();
$userGroups = $user ? $user->getAuthorisedGroups() : [];
$comparison = $params->comparison ?? 'any';
if ($comparison === 'all') {
return empty(array_diff($selection, $userGroups));
}
// Default: any
return !empty(array_intersect($selection, $userGroups));
}
/**
* visitor__access_level — check if current user has specified access levels.
*/
private static function evalVisitorAccessLevel(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$user = Factory::getApplication()->getIdentity();
$accessLevels = $user ? $user->getAuthorisedViewLevels() : [];
$comparison = $params->comparison ?? 'any';
if ($comparison === 'all') {
return empty(array_diff($selection, $accessLevels));
}
return !empty(array_intersect($selection, $accessLevels));
}
/**
* date__date — check if current date is before/after/between specified dates.
*
* params->comparison: 'before', 'after', 'between'
* params->selection: [start_date] or [start_date, end_date]
*/
private static function evalDateDate(object $params): bool
{
$comparison = $params->comparison ?? 'after';
$selection = (array) ($params->selection ?? []);
if (empty($selection)) {
return true;
}
$now = Factory::getDate()->toUnix();
return match ($comparison) {
'before' => $now < strtotime($selection[0]),
'after' => $now > strtotime($selection[0]),
'between' => isset($selection[1])
&& $now >= strtotime($selection[0])
&& $now <= strtotime($selection[1]),
default => false,
};
}
/**
* date__day — check if current day of week matches selection.
*
* params->selection: array of day numbers (1=Monday .. 7=Sunday, ISO-8601).
*/
private static function evalDateDay(object $params): bool
{
$selection = self::toIntArray($params->selection ?? []);
if (empty($selection)) {
return true;
}
$today = (int) Factory::getDate()->format('N'); // 1=Mon, 7=Sun
return \in_array($today, $selection, true);
}
/**
* other__url — check if current URL matches a regex pattern.
*
* params->selection: array of regex patterns (without delimiters).
*/
private static function evalOtherUrl(object $params): bool
{
$patterns = (array) ($params->selection ?? []);
if (empty($patterns)) {
return true;
}
$url = Uri::getInstance()->toString();
foreach ($patterns as $pattern) {
$pattern = trim($pattern);
if ($pattern === '') {
continue;
}
// Wrap in delimiters, escape internal delimiter.
$safePattern = str_replace('#', '\\#', $pattern);
if (@preg_match('#' . $safePattern . '#i', $url)) {
return true;
}
}
return false;
}
// ------------------------------------------------------------------
// Mapping helpers
// ------------------------------------------------------------------
/**
* Get all condition IDs mapped to a specific extension/item pair.
*
* @param string $extension The extension identifier (e.g. 'mod_custom').
* @param int $itemId The item ID within that extension.
*
* @return int[] Array of condition IDs.
*/
public static function getConditionsForItem(string $extension, int $itemId): array
{
$db = Factory::getContainer()->get('DatabaseDriver');
$query = $db->getQuery(true)
->select($db->quoteName('condition_id'))
->from($db->quoteName('#__mokosuiteclient_conditions_map'))
->where($db->quoteName('extension') . ' = :ext')
->where($db->quoteName('item_id') . ' = :iid')
->bind(':ext', $extension)
->bind(':iid', $itemId, \Joomla\Database\ParameterType::INTEGER);
return $db->setQuery($query)->loadColumn();
}
/**
* Check if an item should display based on its mapped conditions.
*
* If no conditions are mapped, the item displays (returns true).
* If conditions are mapped, ALL must pass for the item to display.
*
* @param string $extension The extension identifier.
* @param int $itemId The item ID.
*
* @return bool
*/
public static function shouldDisplay(string $extension, int $itemId): bool
{
$conditionIds = self::getConditionsForItem($extension, $itemId);
if (empty($conditionIds)) {
return true;
}
foreach ($conditionIds as $conditionId) {
if (!self::pass((int) $conditionId)) {
return false;
}
}
return true;
}
/**
* Evaluate a condition by its alias string.
*
* @param string $alias The condition alias.
*
* @return bool True when the condition passes.
*
* @since 02.48.00
*/
public static function passByAlias(string $alias): bool
{
$id = self::resolveAlias($alias);
if ($id === null) {
return false;
}
return self::pass($id);
}
/**
* Resolve a condition reference that may be an integer ID or an alias string.
*
* @param string $ref The reference (numeric ID or alias).
*
* @return int|null The condition ID, or null if not found.
*
* @since 02.48.00
*/
public static function resolveAlias(string $ref): ?int
{
if (is_numeric($ref)) {
return (int) $ref;
}
$db = Factory::getContainer()->get('DatabaseDriver');
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__mokosuiteclient_conditions'))
->where($db->quoteName('alias') . ' = :alias')
->bind(':alias', $ref);
$id = $db->setQuery($query)->loadResult();
return $id !== null ? (int) $id : null;
}
/**
* Evaluate a single inline rule (public wrapper around passRule).
*
* @param string $type The rule type (e.g. 'visitor__access_level').
* @param object $params The rule params object.
*
* @return bool
*
* @since 02.48.00
*/
public static function evaluateInlineRule(string $type, object $params): bool
{
$rule = (object) [
'type' => $type,
'params' => $params,
];
return self::passRule($rule);
}
/**
* Clear the evaluation cache (useful between requests in testing).
*
* @return void
*/
public static function clearCache(): void
{
self::$cache = [];
}
// ------------------------------------------------------------------
// Internal utilities
// ------------------------------------------------------------------
/**
* Normalize a mixed selection value into an array of integers.
*
* @param mixed $value Scalar, array, or object.
*
* @return int[]
*/
private static function toIntArray(mixed $value): array
{
if (\is_object($value)) {
$value = (array) $value;
}
if (!\is_array($value)) {
$value = [$value];
}
return array_map('intval', array_values($value));
}
}
@@ -213,30 +213,46 @@ class DashboardModel extends BaseDatabaseModel
}
/**
* Get installed MokoSuiteClient component and modules with versions.
* Discover all installed MokoSuite ecosystem extensions.
*
* @return array Array of extension objects with name, element, type, version.
* Fuzzy-matches packages, components, modules, plugins, and libraries
* by element name containing "mokosuite", "mokosuiteclient", "mokojoom",
* or "moko" prefix patterns.
*
* @return array Extension objects with name, element, type, version, enabled, family.
*/
public function getMokoExtensions(): array
{
$db = $this->getDatabase();
$el = $db->quoteName('element');
// Fuzzy match: any extension whose element contains moko patterns
$patterns = [
$el . ' LIKE ' . $db->quote('pkg_mokosuite%'),
$el . ' LIKE ' . $db->quote('com_mokosuite%'),
$el . ' LIKE ' . $db->quote('mod_mokosuite%'),
$el . ' LIKE ' . $db->quote('mokosuite%'),
$el . ' LIKE ' . $db->quote('mokosuiteclient%'),
$el . ' LIKE ' . $db->quote('pkg_mokojoom%'),
$el . ' LIKE ' . $db->quote('com_mokojoom%'),
$el . ' LIKE ' . $db->quote('mod_mokojoom%'),
$el . ' LIKE ' . $db->quote('mokojoom%'),
$el . ' LIKE ' . $db->quote('plg_%_mokosuite%'),
$el . ' LIKE ' . $db->quote('plg_%_mokojoom%'),
];
$query = $db->getQuery(true)
->select([
$db->quoteName('extension_id'),
$db->quoteName('element'),
$db->quoteName('name'),
$db->quoteName('type'),
$db->quoteName('folder'),
$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_mokosuiteclient') . ')'
// Admin modules
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module')
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokosuiteclient%') . ')'
. ')')
->where('(' . implode(' OR ', $patterns) . ')')
->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC');
$db->setQuery($query);
@@ -248,12 +264,27 @@ class DashboardModel extends BaseDatabaseModel
{
$manifest = json_decode($row->manifest_cache ?? '{}');
// Determine product family from element name
$family = 'mokosuite';
if (stripos($row->element, 'mokosuiteclient') !== false) {
$family = 'mokosuiteclient';
} elseif (stripos($row->element, 'mokosuitehq') !== false) {
$family = 'mokosuitehq';
} elseif (stripos($row->element, 'mokosuitecrm') !== false) {
$family = 'mokosuitecrm';
} elseif (stripos($row->element, 'mokojoom') !== false) {
$family = 'mokojoom';
}
$extensions[] = (object) [
'element' => $row->element,
'name' => $manifest->name ?? $row->name,
'type' => $row->type,
'version' => $manifest->version ?? '',
'enabled' => (int) $row->enabled,
'extension_id' => (int) $row->extension_id,
'element' => $row->element,
'name' => $manifest->name ?? $row->name,
'type' => $row->type,
'folder' => $row->folder ?? '',
'version' => $manifest->version ?? '',
'enabled' => (int) $row->enabled,
'family' => $family,
];
}
@@ -112,7 +112,7 @@ class ExtensionsModel extends BaseDatabaseModel
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);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$data = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
@@ -221,7 +221,7 @@ class ExtensionsModel extends BaseDatabaseModel
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);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
@@ -238,8 +238,15 @@ class ExtensionsModel extends BaseDatabaseModel
return [];
}
// Determine site's update channel preference
$channel = 'dev'; // default to dev — show everything
// Dev channel only available on Moko domains; all others forced to stable
$isMokoDomain = (bool) preg_match('/\.mokoconsulting\.tech$/i', $_SERVER['HTTP_HOST'] ?? '');
$channel = 'stable';
if ($isMokoDomain) {
try {
$channel = \Joomla\CMS\Component\ComponentHelper::getParams('com_installer')
->get('update_channel', 'stable') ?: 'stable';
} catch (\Throwable $e) {}
}
$hasStable = false;
$hasDev = false;
@@ -269,7 +276,18 @@ class ExtensionsModel extends BaseDatabaseModel
$hasDev = true;
}
if ($ver === '' || version_compare($ver, $bestVersion, '<='))
if ($ver === '')
{
continue;
}
// Respect update channel: stable channel skips dev-tagged versions
if ($channel === 'stable' && $tag === 'dev')
{
continue;
}
if (version_compare($ver, $bestVersion, '<='))
{
continue;
}
@@ -1,183 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\File;
use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Log\Log;
class AttachmentService
{
private const STORAGE_DIR = JPATH_ROOT . '/media/com_mokosuiteclient/attachments';
private const ALLOWED_EXTENSIONS = [
'jpg', 'jpeg', 'png', 'gif', 'webp',
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'txt', 'rtf',
'zip', 'gz', 'tar',
];
private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
/**
* Upload file(s) for a ticket or reply.
*
* @param int $ticketId Ticket ID
* @param int|null $replyId Reply ID (null for ticket-level attachments)
* @param array $files $_FILES array entry (single or multi)
* @return array Saved attachment records
*/
public static function upload(int $ticketId, ?int $replyId, array $files): array
{
$saved = [];
// Normalize single file to array format
if (!is_array($files['name'])) {
$files = [
'name' => [$files['name']],
'type' => [$files['type']],
'tmp_name' => [$files['tmp_name']],
'error' => [$files['error']],
'size' => [$files['size']],
];
}
$ticketDir = self::STORAGE_DIR . '/' . $ticketId;
if (!is_dir($ticketDir) && !Folder::create($ticketDir)) {
Log::add("Failed to create attachment directory: {$ticketDir}", Log::ERROR, 'mokosuiteclient');
return [];
}
$userId = (int) Factory::getUser()->id;
$db = Factory::getDbo();
for ($i = 0, $count = count($files['name']); $i < $count; $i++)
{
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
Log::add("Attachment upload error for '{$files['name'][$i]}': PHP error code {$files['error'][$i]}", Log::WARNING, 'mokosuiteclient');
continue;
}
$originalName = File::makeSafe($files['name'][$i]);
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
// Validate extension
if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
Log::add("Attachment rejected: disallowed extension .{$ext}", Log::WARNING, 'mokosuiteclient');
continue;
}
// Validate size
if ($files['size'][$i] > self::MAX_FILE_SIZE) {
Log::add("Attachment rejected: file too large ({$files['size'][$i]} bytes)", Log::WARNING, 'mokosuiteclient');
continue;
}
// Generate unique filename to prevent overwrites
$storedName = uniqid('att_', true) . '.' . $ext;
$destPath = $ticketDir . '/' . $storedName;
if (!File::upload($files['tmp_name'][$i], $destPath)) {
Log::add("Attachment upload failed: {$originalName}", Log::ERROR, 'mokosuiteclient');
continue;
}
$record = (object) [
'ticket_id' => $ticketId,
'reply_id' => $replyId,
'filename' => $originalName,
'filepath' => $ticketId . '/' . $storedName,
'filesize' => $files['size'][$i],
'mimetype' => mime_content_type($destPath) ?: 'application/octet-stream',
'uploaded_by' => $userId,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_attachments', $record, 'id');
$saved[] = $record;
}
return $saved;
}
/**
* Get attachments for a ticket.
*/
public static function getForTicket(int $ticketId): array
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('a.*, u.name AS uploader_name')
->from($db->quoteName('#__mokosuiteclient_ticket_attachments', 'a'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = a.uploaded_by')
->where($db->quoteName('a.ticket_id') . ' = ' . $ticketId)
->order('a.created ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Get the absolute filesystem path for an attachment.
*/
public static function getAbsolutePath(object $attachment): ?string
{
$path = realpath(self::STORAGE_DIR . '/' . $attachment->filepath);
if ($path === false || !str_starts_with($path, realpath(self::STORAGE_DIR))) {
return null;
}
return $path;
}
/**
* Delete an attachment (file + DB record).
*/
public static function delete(int $attachmentId): bool
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from('#__mokosuiteclient_ticket_attachments')
->where('id = ' . $attachmentId)
);
$att = $db->loadObject();
if (!$att) {
return false;
}
$path = self::STORAGE_DIR . '/' . $att->filepath;
if (file_exists($path)) {
File::delete($path);
}
$db->setQuery(
$db->getQuery(true)
->delete('#__mokosuiteclient_ticket_attachments')
->where('id = ' . $attachmentId)
)->execute();
return true;
}
/**
* Format file size for display.
*/
public static function formatSize(int $bytes): string
{
if ($bytes < 1024) return $bytes . ' B';
if ($bytes < 1048576) return round($bytes / 1024, 1) . ' KB';
return round($bytes / 1048576, 1) . ' MB';
}
}
@@ -1,280 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Service;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Log\Log;
/**
* Automation rule engine — evaluates trigger/condition/action rules.
*
* Called from event hooks (system plugin, task plugin) whenever
* a triggering event occurs. Loads matching rules, checks conditions,
* and executes actions.
*
* @since 02.35.00
*/
class AutomationEngine
{
/**
* Fire all matching rules for a given trigger event.
*
* @param string $triggerEvent Event name (ticket_created, user_login, etc.)
* @param array $context Context data (ticket object, user data, etc.)
*/
public static function fire(string $triggerEvent, array $context = []): void
{
try
{
$rules = self::getActiveRules($triggerEvent);
foreach ($rules as $rule)
{
$conditions = json_decode($rule->conditions, true) ?: [];
$actions = json_decode($rule->actions, true) ?: [];
if (self::evaluateConditions($conditions, $context))
{
self::executeActions($actions, $rule, $context);
}
}
}
catch (\Throwable $e)
{
Log::add('Automation engine error: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
}
}
/**
* Get active automation rules for a trigger event.
*/
private static function getActiveRules(string $event): array
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->select('*')
->from('#__mokosuiteclient_ticket_automation')
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
->where($db->quoteName('enabled') . ' = 1')
->order('ordering ASC')
);
return $db->loadObjectList() ?: [];
}
/**
* Evaluate all conditions (AND logic).
*/
private static function evaluateConditions(array $conditions, array $context): bool
{
foreach ($conditions as $c)
{
$field = $c['field'] ?? '';
$op = $c['op'] ?? 'eq';
$expected = $c['value'] ?? '';
$actual = $context[$field] ?? '';
switch ($op)
{
case 'eq': if ((string) $actual !== (string) $expected) return false; break;
case 'neq': if ((string) $actual === (string) $expected) return false; break;
case 'gt': if ((float) $actual <= (float) $expected) return false; break;
case 'lt': if ((float) $actual >= (float) $expected) return false; break;
case 'in':
$values = array_map('trim', explode(',', $expected));
if (!in_array((string) $actual, $values, true)) return false;
break;
case 'not_in':
$values = array_map('trim', explode(',', $expected));
if (in_array((string) $actual, $values, true)) return false;
break;
}
}
return true;
}
/**
* Execute actions for a matched rule.
*/
private static function executeActions(array $actions, object $rule, array $context): void
{
$db = Factory::getDbo();
$ticketId = (int) ($context['ticket_id'] ?? $context['id'] ?? 0);
foreach ($actions as $action)
{
$type = $action['type'] ?? '';
$value = $action['value'] ?? '';
try
{
switch ($type)
{
case 'set_status':
if ($ticketId) {
$statusId = self::resolveStatusId($db, $value);
$sets = "status = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())}";
if ($statusId) { $sets .= ", status_id = {$statusId}"; }
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
}
break;
case 'set_priority':
if ($ticketId) {
$priorityId = self::resolvePriorityId($db, $value);
$sets = "priority = {$db->quote($value)}, modified = {$db->quote(Factory::getDate()->toSql())}";
if ($priorityId) { $sets .= ", priority_id = {$priorityId}"; }
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
}
break;
case 'assign':
$assignId = (int) $value;
if ($ticketId && $assignId > 0) {
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET assigned_to = {$assignId}, modified = {$db->quote(Factory::getDate()->toSql())} WHERE id = {$ticketId}")->execute();
}
break;
case 'add_note':
if ($ticketId) {
$note = (object) [
'ticket_id' => $ticketId,
'user_id' => 0,
'body' => $value ?: '[Automation: ' . ($rule->title ?? '') . ']',
'is_internal' => 1,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_replies', $note);
}
break;
case 'send_email':
NotificationService::securityAlert(
'automation',
'Automation: ' . ($rule->title ?? ''),
$value ?: 'Rule triggered for ticket #' . $ticketId
);
break;
case 'send_ntfy':
NotificationService::pushNtfySecurity(
'automation',
'Automation: ' . ($rule->title ?? ''),
$value ?: 'Rule triggered for ticket #' . $ticketId
);
break;
case 'close':
if ($ticketId) {
$closedId = self::resolveClosedStatusId($db);
$sets = "status = 'closed', closed = {$db->quote(Factory::getDate()->toSql())}, modified = {$db->quote(Factory::getDate()->toSql())}";
if ($closedId) { $sets .= ", status_id = {$closedId}"; }
$db->setQuery("UPDATE {$db->quoteName('#__mokosuiteclient_tickets')} SET {$sets} WHERE id = {$ticketId}")->execute();
}
break;
case 'create_ticket':
self::createTicketFromAutomation($rule, $context, $value);
break;
}
}
catch (\Throwable $e)
{
Log::add("Automation action '{$type}' failed for rule #{$rule->id}: " . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
}
}
}
/**
* Create a ticket from automation (with behavior: append/always_new/skip_if_open).
*/
private static function resolveStatusId($db, string $alias): int
{
return (int) $db->setQuery(
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('alias') . ' = ' . $db->quote($alias)), 0, 1
)->loadResult();
}
private static function resolvePriorityId($db, string $alias): int
{
return (int) $db->setQuery(
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
->where($db->quoteName('alias') . ' = ' . $db->quote($alias)), 0, 1
)->loadResult();
}
private static function resolveClosedStatusId($db): int
{
return (int) $db->setQuery(
$db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('is_closed') . ' = 1'), 0, 1
)->loadResult();
}
private static function createTicketFromAutomation(object $rule, array $context, string $subject): void
{
$db = Factory::getDbo();
$behavior = $rule->behavior ?? 'append';
$userId = (int) ($context['user_id'] ?? 0);
$catId = (int) ($context['category_id'] ?? 0);
if ($behavior !== 'always_new' && $userId > 0)
{
// Check for existing open ticket (check both status ENUM and status_id)
$query = $db->getQuery(true)
->select('t.id')
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->join('LEFT', $db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON t.status_id = s.id')
->where('t.created_by = ' . $userId)
->where("(s.id IS NULL AND t.status NOT IN ('closed', 'resolved')) OR (s.id IS NOT NULL AND s.is_closed = 0)");
if ($catId > 0) {
$query->where('category_id = ' . $catId);
}
$db->setQuery($query, 0, 1);
$existingId = (int) $db->loadResult();
if ($existingId > 0)
{
if ($behavior === 'skip_if_open') return;
// append — add reply to existing ticket
$reply = (object) [
'ticket_id' => $existingId,
'user_id' => 0,
'body' => $subject ?: '[Automation: ' . ($rule->title ?? '') . ']',
'is_internal' => 1,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_replies', $reply);
return;
}
}
// Create new ticket
$openStatusId = self::resolveStatusId($db, 'open') ?: null;
$normalPriorityId = self::resolvePriorityId($db, $context['priority'] ?? 'normal') ?: null;
$ticket = (object) [
'subject' => $subject ?: 'Automation: ' . ($rule->title ?? ''),
'body' => $context['body'] ?? '',
'status' => 'open',
'status_id' => $openStatusId,
'priority' => $context['priority'] ?? 'normal',
'priority_id' => $normalPriorityId,
'category_id' => $catId ?: null,
'created_by' => $userId,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id');
}
}
@@ -1,581 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\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, 'mokosuiteclient');
}
}
// Push notification via ntfy
self::pushNtfy($event, $ticket, $subject);
}
catch (\Throwable $e)
{
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* 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_mokosuiteclient&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 MokoSuiteClient';
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)
{
Log::add('Failed to look up email for user ID ' . $userId . ': ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
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_mokosuiteclient'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
);
$params = json_decode($db->loadResult() ?? '{}', true);
return $params['notifications'] ?? [];
}
catch (\Throwable $e)
{
Log::add('Failed to load notification config: ' . $e->getMessage(), Log::ERROR, 'mokosuiteclient');
return [];
}
}
// ==================================================================
// Ntfy Push Notifications (#205)
// ==================================================================
/**
* Send a push notification via ntfy for ticket events.
*/
private static function pushNtfy(string $event, object $ticket, string $title): void
{
$config = self::getNotificationConfig();
$ntfyEnabled = $config['ntfy_enabled'] ?? '0';
if (!$ntfyEnabled)
{
return;
}
$ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/');
$ntfyTopic = $config['ntfy_topic'] ?? 'mokosuiteclient-tickets';
$ntfyToken = $config['ntfy_token'] ?? '';
$tagMap = [
'ticket_created' => 'ticket,new',
'ticket_replied' => 'speech_balloon',
'status_changed' => 'arrows_counterclockwise',
'ticket_assigned' => 'bust_in_silhouette',
];
$priorityMap = [
'ticket_created' => '4',
'ticket_replied' => '3',
'status_changed' => '3',
'ticket_assigned' => '3',
];
$siteUrl = rtrim(Uri::root(), '/');
$ticketUrl = $siteUrl . '/administrator/index.php?option=com_mokosuiteclient&view=ticket&id=' . ($ticket->id ?? 0);
$message = self::buildNtfyMessage($event, $ticket);
$headers = [
'Title: ' . $title,
'Priority: ' . ($priorityMap[$event] ?? '3'),
'Tags: ' . ($tagMap[$event] ?? 'ticket'),
'Click: ' . $ticketUrl,
];
if ($ntfyToken !== '')
{
$headers[] = 'Authorization: Bearer ' . $ntfyToken;
}
$url = $ntfyServer . '/' . $ntfyTopic;
try
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $message);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false)
{
Log::add("Ntfy push connection failed for event {$event}: " . $curlError, Log::WARNING, 'mokosuiteclient');
}
elseif ($httpCode < 200 || $httpCode >= 300)
{
Log::add("Ntfy push failed (HTTP {$httpCode}) for event {$event}", Log::WARNING, 'mokosuiteclient');
}
}
catch (\Throwable $e)
{
Log::add('Ntfy push error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* Build a short ntfy message body for ticket events.
*/
private static function buildNtfyMessage(string $event, object $ticket): string
{
$subject = $ticket->subject ?? 'Ticket #' . ($ticket->id ?? '?');
switch ($event)
{
case 'ticket_created':
$priority = ucfirst($ticket->priority ?? 'normal');
return "New ticket: {$subject}\nPriority: {$priority}";
case 'ticket_replied':
return "Reply on: {$subject}";
case 'status_changed':
$status = ucwords(str_replace('_', ' ', $ticket->status ?? ''));
return "Status → {$status}: {$subject}";
case 'ticket_assigned':
return "Assigned to you: {$subject}";
default:
return $subject;
}
}
/**
* Send a push notification via ntfy for security events.
*/
public static function pushNtfySecurity(string $event, string $title, string $body): void
{
$config = self::getNotificationConfig();
$ntfyEnabled = $config['ntfy_enabled'] ?? '0';
if (!$ntfyEnabled)
{
return;
}
$ntfyServer = rtrim($config['ntfy_server'] ?? 'https://ntfy.mokoconsulting.tech', '/');
$ntfyTopic = $config['ntfy_security_topic'] ?? $config['ntfy_topic'] ?? 'mokosuiteclient-security';
$ntfyToken = $config['ntfy_token'] ?? '';
$headers = [
'Title: [Security] ' . $title,
'Priority: 5',
'Tags: warning,shield',
];
if ($ntfyToken !== '')
{
$headers[] = 'Authorization: Bearer ' . $ntfyToken;
}
$url = $ntfyServer . '/' . $ntfyTopic;
try
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_exec($ch);
curl_close($ch);
}
catch (\Throwable $e)
{
Log::add('Ntfy security push error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
// ==================================================================
// 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 . ' | MokoSuiteClient 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, 'mokosuiteclient');
}
}
// Also push via ntfy
self::pushNtfySecurity($event, $subject, $body);
}
catch (\Throwable $e)
{
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
}
@@ -1,33 +0,0 @@
<?php
namespace Moko\Component\MokoSuiteClient\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 #__mokosuiteclient_ticket_canned ORDER BY ordering ASC');
$this->responses = $db->loadObjectList() ?: [];
$db->setQuery('SELECT id, title FROM #__mokosuiteclient_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_mokosuiteclient&view=tickets');
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
}
@@ -27,6 +27,7 @@ class HtmlView extends BaseHtmlView
protected $loginChartData = [];
protected $mokoExtensions = [];
public $supportPin = '';
public $supportPinAvailable = false;
public function display($tpl = null)
{
@@ -47,12 +48,21 @@ class HtmlView extends BaseHtmlView
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
);
$token = (json_decode((string) $db->loadResult()))->health_api_token ?? '';
$coreParams = json_decode((string) $db->loadResult());
$healthToken = $coreParams->health_api_token ?? '';
$this->supportPinAvailable = !empty($healthToken);
if (!empty($token))
if (!empty($healthToken))
{
$hash = hash_hmac('sha256', gmdate('Y-m-d'), $token);
$this->supportPin = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
$pinRequestedAt = $coreParams->support_pin_requested_at ?? '';
$pinTtl = 72 * 3600;
if (!empty($pinRequestedAt) && (time() - (int) $pinRequestedAt) < $pinTtl)
{
$window = floor((int) $pinRequestedAt / $pinTtl);
$hash = hash_hmac('sha256', (string) $window, $healthToken);
$this->supportPin = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
}
}
}
catch (\Throwable $e) {}
@@ -1,227 +0,0 @@
<?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_mokosuiteclient&task=display.saveCanned&format=json');
$deleteUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.deleteCanned&format=json');
$reorderUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.reorderCanned&format=json');
// Build category map for filter display
$catMap = [0 => 'All Categories'];
foreach ($categories as $cat)
{
$catMap[$cat->id] = $cat->title;
}
?>
<div id="mokosuiteclient-canned">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-3">
<h4 class="mb-0"><?php echo count($responses); ?> Canned Responses</h4>
<select id="canned-filter-category" class="form-select form-select-sm" style="width:auto;">
<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>
<button type="button" class="btn btn-primary btn-sm" onclick="openCannedModal(0)">
<span class="icon-plus"></span> Add Response
</button>
</div>
<div id="canned-list">
<?php foreach ($responses as $r): ?>
<div class="card mb-2 canned-card" data-id="<?php echo $r->id; ?>" data-category="<?php echo (int) $r->category_id; ?>" style="cursor:grab;">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1" style="cursor:pointer;" onclick="openCannedModal(<?php echo $r->id; ?>)">
<div class="d-flex align-items-center gap-2">
<span class="icon-menu text-muted" style="cursor:grab;" title="Drag to reorder"></span>
<strong><?php echo htmlspecialchars($r->title); ?></strong>
<?php if (!empty($r->category_id) && isset($catMap[$r->category_id])): ?>
<span class="badge bg-secondary"><?php echo htmlspecialchars($catMap[$r->category_id]); ?></span>
<?php endif; ?>
</div>
<p class="text-muted small mb-0 mt-1 ms-4"><?php echo htmlspecialchars(mb_substr(strip_tags($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" id="canned-empty">No canned responses yet. Click "Add Response" to create one.</div>
<?php endif; ?>
</div>
</div>
<!-- Canned Response Modal (create + edit) -->
<div class="modal fade" id="cannedModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 id="cannedModalTitle">Add Canned Response</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="canned-id" value="0">
<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="">No 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="mb-3">
<label class="form-label">Response Text</label>
<textarea id="canned-body" class="form-control" rows="8" 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 tokenKey = '<?php echo $token; ?>';
// ── Response data store (for edit modal) ────────────────────
var responseData = {};
<?php foreach ($responses as $r): ?>
responseData[<?php echo $r->id; ?>] = {
title: <?php echo json_encode($r->title); ?>,
body: <?php echo json_encode($r->body); ?>,
category_id: <?php echo json_encode($r->category_id ?? ''); ?>
};
<?php endforeach; ?>
// ── Open modal for create (id=0) or edit ────────────────────
window.openCannedModal = function(id) {
document.getElementById('canned-id').value = id;
if (id > 0 && responseData[id]) {
document.getElementById('cannedModalTitle').textContent = 'Edit Canned Response';
document.getElementById('canned-title').value = responseData[id].title;
document.getElementById('canned-body').value = responseData[id].body;
document.getElementById('canned-category').value = responseData[id].category_id || '';
} else {
document.getElementById('cannedModalTitle').textContent = 'Add Canned Response';
document.getElementById('canned-title').value = '';
document.getElementById('canned-body').value = '';
document.getElementById('canned-category').value = '';
}
new bootstrap.Modal(document.getElementById('cannedModal')).show();
};
// ── Save (create or update) ─────────────────────────────────
document.getElementById('btn-save-canned').addEventListener('click', function() {
var title = document.getElementById('canned-title').value.trim();
if (!title) { Joomla.renderMessages({error:['Title is required']}); return; }
var fd = new FormData();
fd.append('id', document.getElementById('canned-id').value);
fd.append('title', title);
fd.append('body', document.getElementById('canned-body').value);
fd.append('category_id', document.getElementById('canned-category').value);
fd.append(tokenKey, '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]});
});
});
// ── Delete ──────────────────────────────────────────────────
document.querySelectorAll('.btn-delete-canned').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
if (!confirm('Delete this canned response?')) return;
var card = this.closest('.card');
var fd = new FormData();
fd.append('id', this.dataset.id);
fd.append(tokenKey, '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]}); });
});
});
// ── Category filter ─────────────────────────────────────────
document.getElementById('canned-filter-category').addEventListener('change', function() {
var catId = this.value;
document.querySelectorAll('.canned-card').forEach(function(card) {
if (!catId || card.dataset.category === catId) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
});
// ── Drag-and-drop reorder ───────────────────────────────────
var list = document.getElementById('canned-list');
var dragCard = null;
list.addEventListener('dragstart', function(e) {
dragCard = e.target.closest('.canned-card');
if (dragCard) {
dragCard.style.opacity = '0.5';
e.dataTransfer.effectAllowed = 'move';
}
});
list.addEventListener('dragend', function() {
if (dragCard) dragCard.style.opacity = '';
dragCard = null;
});
list.addEventListener('dragover', function(e) {
e.preventDefault();
var target = e.target.closest('.canned-card');
if (target && target !== dragCard) {
var rect = target.getBoundingClientRect();
var after = (e.clientY - rect.top) > rect.height / 2;
if (after) {
target.parentNode.insertBefore(dragCard, target.nextSibling);
} else {
target.parentNode.insertBefore(dragCard, target);
}
}
});
list.addEventListener('drop', function(e) {
e.preventDefault();
// Persist new order
var ids = [];
document.querySelectorAll('.canned-card').forEach(function(c) { ids.push(c.dataset.id); });
var fd = new FormData();
fd.append('order', JSON.stringify(ids));
fd.append(tokenKey, '1');
fetch('<?php echo $reorderUrl; ?>', {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}});
});
// Make cards draggable
document.querySelectorAll('.canned-card').forEach(function(card) {
card.setAttribute('draggable', 'true');
});
});
</script>
@@ -25,6 +25,9 @@ $atsAvail = $this->atsAvailable ?? null;
$checkedOut = $this->checkedOutItems;
$wafBlocks = $this->wafBlocks;
$token = Session::getFormToken();
$user = \Joomla\CMS\Factory::getApplication()->getIdentity();
$canWafLog = $user->authorise('mokosuiteclient.security.waflog', 'com_mokosuiteclient')
|| $user->authorise('core.admin', 'com_mokosuiteclient');
// Group plugins by category
$grouped = [];
@@ -48,77 +51,43 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
</div>
<?php endif; ?>
<!-- Site Info Bar -->
<div class="mokosuiteclient-info-bar card mb-4">
<div class="card-body">
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label"><?php echo Text::_('COM_MOKOSUITECLIENT_SITE'); ?></span>
<span class="mokosuiteclient-info-value fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
</div>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label">MokoSuite</span>
<span class="mokosuiteclient-info-value"><span class="badge bg-primary"><?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span></span>
</div>
<div class="card mb-4">
<div class="card-body d-flex flex-wrap align-items-center gap-2" style="padding:0.75rem 1.25rem;font-size:0.85rem;">
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem;color:#1a2744"></span>
<span class="fw-bold"><?php echo $this->escape($siteInfo->sitename); ?></span>
<span class="badge bg-primary">MokoSuite <?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span>
<?php if (!empty($this->supportPin)): ?>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label">Support PIN</span>
<span class="mokosuiteclient-info-value"><span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span></span>
</div>
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Support PIN — valid for 72 hours"><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span>
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-1" id="mokosuiteclient-btn-heartbeat-pin"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.sendHeartbeat&format=json'); ?>"
data-token="<?php echo $token; ?>"
title="Send heartbeat with PIN to MokoSuiteHQ">
<span class="icon-upload" aria-hidden="true"></span>
</button>
<?php elseif (!empty($this->supportPinAvailable)): ?>
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2" id="mokosuiteclient-request-pin"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json'); ?>"
data-token="<?php echo $token; ?>"
style="font-size:0.75rem;" title="Request a support PIN (valid 72 hours)">
<span class="icon-key" aria-hidden="true"></span> Request PIN
</button>
<?php endif; ?>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label">Joomla</span>
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->joomla_version); ?></span></span>
</div>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label">PHP</span>
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->php_version); ?></span></span>
</div>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-label"><?php echo Text::_('COM_MOKOSUITECLIENT_DATABASE'); ?></span>
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span></span>
</div>
<span class="badge bg-secondary">Joomla <?php echo $this->escape($siteInfo->joomla_version); ?></span>
<span class="badge bg-secondary">PHP <?php echo $this->escape($siteInfo->php_version); ?></span>
<span class="badge bg-secondary"><?php echo $this->escape($siteInfo->db_type); ?></span>
<?php if ($siteInfo->debug): ?>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-value"><span class="badge bg-warning text-dark"><?php echo Text::_('COM_MOKOSUITECLIENT_DEBUG_ON'); ?></span></span>
</div>
<span class="badge bg-warning text-dark">Debug ON</span>
<?php endif; ?>
<?php if ($siteInfo->offline): ?>
<div class="mokosuiteclient-info-item">
<span class="mokosuiteclient-info-value"><span class="badge bg-danger"><?php echo Text::_('COM_MOKOSUITECLIENT_OFFLINE'); ?></span></span>
</div>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
<div class="mokosuiteclient-info-item ms-auto">
<span class="ms-auto d-flex align-items-center gap-2">
<span class="icon-globe" aria-hidden="true"></span>
<code><?php echo $this->escape($_SERVER['REMOTE_ADDR'] ?? ''); ?></code>
</div>
</span>
</div>
</div>
<?php if (!empty($mokoExts)): ?>
<!-- Moko Component & Module Versions -->
<div class="row g-0 mb-4 border rounded overflow-hidden">
<?php
$extIcons = [
'com_mokosuiteclient' => 'icon-cogs',
'mod_mokosuiteclient_cpanel' => 'icon-tachometer-alt',
'mod_mokosuiteclient_menu' => 'icon-bars',
'mod_mokosuiteclient_cache' => 'icon-bolt',
'mod_mokosuiteclient_categories' => 'icon-folder',
];
$extCount = count($mokoExts);
$colClass = $extCount > 0 ? 'col-' . max(1, (int) floor(12 / $extCount)) : 'col';
foreach ($mokoExts as $ext):
$icon = $extIcons[$ext->element] ?? 'icon-puzzle-piece';
$label = str_replace(['mod_mokosuiteclient_', 'com_mokosuiteclient'], ['', 'Component'], $ext->element);
$label = ucfirst($label ?: 'Component');
?>
<div class="<?php echo $colClass; ?> d-flex align-items-center justify-content-center gap-2 py-2 bg-white border-end" style="font-size:0.82rem;">
<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 -->
@@ -219,7 +188,7 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
</h3>
<div class="mokosuiteclient-plugin-grid row g-3 mb-4">
<?php foreach ($catPlugins as $plugin): ?>
<div class="col-12 col-md-6">
<div class="col-12 <?php echo $catKey === 'core' ? '' : 'col-md-6 col-lg-4'; ?>">
<div class="card mokosuiteclient-plugin-card h-100 <?php echo $plugin->enabled ? '' : 'mokosuiteclient-plugin-disabled'; ?>"
data-extension-id="<?php echo $plugin->extension_id; ?>">
<div class="card-body d-flex flex-column">
@@ -236,11 +205,7 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
<?php if ($plugin->protected): ?>
<span class="badge bg-dark"><?php echo Text::_('COM_MOKOSUITECLIENT_PROTECTED'); ?></span>
<?php elseif ($plugin->configure_only): ?>
<span class="badge bg-<?php echo $plugin->enabled ? 'success' : 'secondary'; ?>">
<?php echo $plugin->enabled ? Text::_('COM_MOKOSUITECLIENT_ENABLED') : Text::_('COM_MOKOSUITECLIENT_DISABLED'); ?>
</span>
<?php else: ?>
<?php elseif ($plugin->extension_id): ?>
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input mokosuiteclient-toggle" role="switch"
id="toggle-<?php echo $plugin->extension_id; ?>"
@@ -253,7 +218,7 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
</label>
</div>
<?php endif; ?>
<?php if ($plugin->type === 'plugin'): ?>
<?php if ($plugin->extension_id && $plugin->type === 'plugin'): ?>
<a href="<?php echo Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $plugin->extension_id); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-cog" aria-hidden="true"></span> <?php echo Text::_('COM_MOKOSUITECLIENT_CONFIGURE'); ?>
</a>
@@ -270,6 +235,7 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
<!-- Right: Charts & Information (4 cols) -->
<div class="col-12 col-xl-4" style="border-left:1px solid var(--gray-300, #dee2e6);padding-left:1.5rem;">
<?php if ($canWafLog): ?>
<!-- WAF Activity Chart -->
<div class="card mb-3">
<div class="card-header">
@@ -279,6 +245,7 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
<canvas id="mokosuiteclient-chart-waf" height="140"></canvas>
</div>
</div>
<?php endif; ?>
<!-- Login Activity Chart -->
<div class="card mb-3">
@@ -349,6 +316,7 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
<?php endif; ?>
</div>
<?php if ($canWafLog): ?>
<!-- WAF Blocks -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
@@ -376,6 +344,7 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Recent Logins -->
<div class="card mb-3">
@@ -1,313 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\Api\Controller;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\BaseController;
/**
* Helpdesk Tickets REST API controller.
*
* GET /api/index.php/v1/mokosuiteclient/tickets - list tickets
* GET /api/index.php/v1/mokosuiteclient/tickets/{id} - get single ticket with replies
* POST /api/index.php/v1/mokosuiteclient/tickets - create ticket
* PATCH /api/index.php/v1/mokosuiteclient/tickets/{id} - update ticket fields
* POST /api/index.php/v1/mokosuiteclient/tickets/{id}/reply - add reply
*
* @since 02.35.00
*/
class TicketsController extends BaseController
{
/**
* GET /tickets — list tickets with optional filters.
*/
public function displayList(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$app = Factory::getApplication();
$db = Factory::getDbo();
$input = $app->getInput();
$query = $db->getQuery(true)
->select('t.*, s.title AS status_title, p.title AS priority_title, c.title AS category_title, u.name AS created_by_name')
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON s.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->order('t.created DESC');
// Filters
$status = $input->getString('status', '');
if ($status) {
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($status));
}
$categoryId = $input->getInt('category_id', 0);
if ($categoryId) {
$query->where($db->quoteName('t.category_id') . ' = ' . $categoryId);
}
$assignedTo = $input->getInt('assigned_to', 0);
if ($assignedTo) {
$query->where($db->quoteName('t.assigned_to') . ' = ' . $assignedTo);
}
$limit = min($input->getInt('limit', 25), 100);
$offset = $input->getInt('offset', 0);
$db->setQuery($query, $offset, $limit);
$tickets = $db->loadObjectList() ?: [];
// Total count (with same filters applied)
$countQuery = clone $query;
$countQuery->clear('select')->clear('order')->select('COUNT(*)');
$db->setQuery($countQuery);
$total = (int) $db->loadResult();
$this->sendJson(200, [
'tickets' => $tickets,
'total' => $total,
'limit' => $limit,
'offset' => $offset,
]);
}
/**
* GET /tickets/{id} — single ticket with replies and attachments.
*/
public function displayItem(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$id = Factory::getApplication()->getInput()->getInt('id', 0);
$db = Factory::getDbo();
// Ticket
$db->setQuery(
$db->getQuery(true)
->select('t.*, s.title AS status_title, p.title AS priority_title, c.title AS category_title, u.name AS created_by_name')
->from($db->quoteName('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_statuses', 's') . ' ON s.id = t.status_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_priorities', 'p') . ' ON p.id = t.priority_id')
->leftJoin($db->quoteName('#__mokosuiteclient_ticket_categories', 'c') . ' ON c.id = t.category_id')
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
->where('t.id = ' . $id)
);
$ticket = $db->loadObject();
if (!$ticket) {
$this->sendJson(404, ['error' => 'Ticket not found']);
return;
}
// Replies
$db->setQuery(
$db->getQuery(true)
->select('r.*, u.name AS user_name')
->from($db->quoteName('#__mokosuiteclient_ticket_replies', 'r'))
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
->where('r.ticket_id = ' . $id)
->order('r.created ASC')
);
$ticket->replies = $db->loadObjectList() ?: [];
// Attachments
$ticket->attachments = \Moko\Component\MokoSuiteClient\Administrator\Service\AttachmentService::getForTicket($id);
$this->sendJson(200, $ticket);
}
/**
* POST /tickets — create a new ticket.
*/
public function create(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$input = Factory::getApplication()->getInput();
$db = Factory::getDbo();
$subject = $input->getString('subject', '');
$body = $input->getRaw('body', '');
if (empty($subject)) {
$this->sendJson(400, ['error' => 'Subject is required']);
return;
}
$statusId = $input->getInt('status_id', 0) ?: null;
$priorityId = $input->getInt('priority_id', 0) ?: null;
$status = $input->getString('status', 'open');
$priority = $input->getString('priority', 'normal');
// Resolve status_id from alias if not provided
if (!$statusId && $status) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('alias') . ' = ' . $db->quote($status));
$statusId = (int) $db->setQuery($q, 0, 1)->loadResult() ?: null;
}
if (!$priorityId && $priority) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
->where($db->quoteName('alias') . ' = ' . $db->quote($priority));
$priorityId = (int) $db->setQuery($q, 0, 1)->loadResult() ?: null;
}
$ticket = (object) [
'subject' => $subject,
'body' => $body,
'status' => $status,
'status_id' => $statusId,
'priority' => $priority,
'priority_id' => $priorityId,
'category_id' => $input->getInt('category_id', 0) ?: null,
'created_by' => (int) Factory::getUser()->id,
'assigned_to' => $input->getInt('assigned_to', 0) ?: null,
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_tickets', $ticket, 'id');
// Trigger notification
\Moko\Component\MokoSuiteClient\Administrator\Service\NotificationService::notify('ticket_created', $ticket);
$this->sendJson(201, ['id' => (int) $ticket->id, 'message' => 'Ticket created']);
}
/**
* PATCH /tickets/{id} — update ticket fields.
*/
public function update(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$input = Factory::getApplication()->getInput();
$id = $input->getInt('id', 0);
$db = Factory::getDbo();
// Type-safe input extraction
$fields = [];
$intFields = ['status_id', 'priority_id', 'category_id', 'assigned_to'];
$strFields = ['status', 'priority'];
foreach ($intFields as $field) {
$value = $input->getInt($field, 0);
if ($value > 0) { $fields[$field] = $value; }
}
foreach ($strFields as $field) {
$value = $input->getString($field, '');
if ($value !== '') { $fields[$field] = $value; }
}
if (empty($fields)) {
$this->sendJson(400, ['error' => 'No fields to update']);
return;
}
// Sync status/status_id if only one is provided
if (isset($fields['status']) && !isset($fields['status_id'])) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_statuses')
->where($db->quoteName('alias') . ' = ' . $db->quote($fields['status']));
$resolved = (int) $db->setQuery($q, 0, 1)->loadResult();
if ($resolved) { $fields['status_id'] = $resolved; }
} elseif (isset($fields['status_id']) && !isset($fields['status'])) {
$q = $db->getQuery(true)->select('alias')->from('#__mokosuiteclient_ticket_statuses')
->where('id = ' . (int) $fields['status_id']);
$alias = $db->setQuery($q, 0, 1)->loadResult();
if ($alias) { $fields['status'] = $alias; }
}
if (isset($fields['priority']) && !isset($fields['priority_id'])) {
$q = $db->getQuery(true)->select('id')->from('#__mokosuiteclient_ticket_priorities')
->where($db->quoteName('alias') . ' = ' . $db->quote($fields['priority']));
$resolved = (int) $db->setQuery($q, 0, 1)->loadResult();
if ($resolved) { $fields['priority_id'] = $resolved; }
} elseif (isset($fields['priority_id']) && !isset($fields['priority'])) {
$q = $db->getQuery(true)->select('alias')->from('#__mokosuiteclient_ticket_priorities')
->where('id = ' . (int) $fields['priority_id']);
$alias = $db->setQuery($q, 0, 1)->loadResult();
if ($alias) { $fields['priority'] = $alias; }
}
$sets = [];
foreach ($fields as $k => $v) {
$sets[] = $db->quoteName($k) . ' = ' . (is_int($v) ? $v : $db->quote($v));
}
$sets[] = 'modified = ' . $db->quote(Factory::getDate()->toSql());
$db->setQuery('UPDATE ' . $db->quoteName('#__mokosuiteclient_tickets') . ' SET ' . implode(', ', $sets) . ' WHERE id = ' . $id)->execute();
if ($db->getAffectedRows() === 0) {
$this->sendJson(404, ['error' => 'Ticket not found']);
return;
}
$this->sendJson(200, ['id' => $id, 'message' => 'Ticket updated', 'updated' => array_keys($fields)]);
}
/**
* POST /tickets/{id}/reply — add a reply.
*/
public function reply(): void
{
$this->requireAuth('core.manage', 'com_mokosuiteclient');
$input = Factory::getApplication()->getInput();
$ticketId = $input->getInt('id', 0);
$body = $input->getRaw('body', '');
if (!$ticketId || empty($body)) {
$this->sendJson(400, ['error' => 'ticket_id and body are required']);
return;
}
$db = Factory::getDbo();
$reply = (object) [
'ticket_id' => $ticketId,
'user_id' => (int) Factory::getUser()->id,
'body' => $body,
'is_internal' => $input->getInt('is_internal', 0),
'created' => Factory::getDate()->toSql(),
];
$db->insertObject('#__mokosuiteclient_ticket_replies', $reply, 'id');
// Notify
$db->setQuery($db->getQuery(true)->select('*')->from('#__mokosuiteclient_tickets')->where('id = ' . $ticketId));
$ticket = $db->loadObject();
if ($ticket) {
\Moko\Component\MokoSuiteClient\Administrator\Service\NotificationService::notify('ticket_replied', $ticket, ['reply_body' => $body]);
}
$this->sendJson(201, ['reply_id' => (int) $reply->id, 'message' => 'Reply added']);
}
// ── Helpers ──────────────────────────────────────────────────
private function requireAuth(string $action, string $asset): void
{
$user = Factory::getUser();
if (!$user->authorise($action, $asset)) {
$this->sendJson(403, ['error' => 'Not authorized']);
throw new \RuntimeException('Not authorized', 403);
}
}
private function sendJson(int $code, $payload): void
{
$app = Factory::getApplication();
$app->setHeader('Content-Type', 'application/json', true);
$app->setHeader('Status', (string) $code, true);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$app->close();
}
}
@@ -110,6 +110,77 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
// Heartbeat + PIN send button
var hbBtn = document.getElementById('mokosuiteclient-btn-heartbeat-pin');
if (hbBtn) {
hbBtn.addEventListener('click', function () {
var btn = this;
var url = btn.dataset.url;
var token = btn.dataset.token;
var icon = btn.querySelector('span');
btn.disabled = true;
if (icon) icon.className = 'icon-spinner icon-spin';
var fd = new FormData();
fd.append(token, '1');
fetch(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 || 'Heartbeat sent to HQ.']});
} else {
Joomla.renderMessages({error: [d.message || 'Heartbeat failed.']});
}
})
.catch(function () {
Joomla.renderMessages({error: ['Network error sending heartbeat.']});
})
.finally(function () {
btn.disabled = false;
if (icon) icon.className = 'icon-upload';
});
});
}
// Request PIN button
var pinBtn = document.getElementById('mokosuiteclient-request-pin');
if (pinBtn) {
pinBtn.addEventListener('click', function () {
var btn = this;
btn.disabled = true;
btn.textContent = '...';
var fd = new FormData();
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 && d.pin) {
var badge = document.createElement('span');
badge.className = 'badge bg-dark';
badge.style.cssText = 'font-family:monospace;letter-spacing:0.08em;cursor:help;';
badge.title = 'Support PIN — valid for 72 hours';
badge.textContent = d.pin;
var icon = document.createElement('span');
icon.className = 'icon-key small me-1';
icon.setAttribute('aria-hidden', 'true');
badge.prepend(icon);
btn.replaceWith(badge);
} else {
Joomla.renderMessages({error: [d.message || 'Failed to generate PIN']});
btn.disabled = false;
btn.textContent = 'Request PIN';
}
})
.catch(function () {
Joomla.renderMessages({error: ['Network error']});
btn.disabled = false;
btn.textContent = 'Request PIN';
});
});
}
// Akeeba import buttons
['btn-import-admintools', 'btn-import-ats-dash'].forEach(function(id) {
var btn = document.getElementById(id);
@@ -20,7 +20,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>MokoSuiteClient admin dashboard and REST API. Provides a control panel for managing MokoSuiteClient feature plugins, site health monitoring, and remote management endpoints.</description>
<namespace path="src">Moko\Component\MokoSuiteClient</namespace>
@@ -42,7 +42,6 @@
<submenu>
<menu link="option=com_mokosuiteclient" img="class:cogs">COM_MOKOSUITECLIENT_MENU_DASHBOARD</menu>
<menu link="option=com_mokosuiteclient&amp;view=extensions" img="class:puzzle-piece">COM_MOKOSUITECLIENT_MENU_EXTENSIONS</menu>
<menu link="option=com_mokosuiteclient&amp;view=tickets" img="class:headphones">COM_MOKOSUITECLIENT_MENU_TICKETS</menu>
<menu link="option=com_mokosuiteclient&amp;view=htaccess" img="class:file-code">COM_MOKOSUITECLIENT_MENU_HTACCESS</menu>
<menu link="option=com_mokosuiteclient&amp;view=privacy" img="class:lock">COM_MOKOSUITECLIENT_MENU_PRIVACY</menu>
<menu link="option=com_mokosuiteclient&amp;view=waflog" img="class:shield-alt">COM_MOKOSUITECLIENT_MENU_WAFLOG</menu>
@@ -1,84 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\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('mokosuiteclient.tickets', 'com_mokosuiteclient');
$this->canAssign = $user->authorise('core.admin') || $user->authorise('mokosuiteclient.tickets.assign', 'com_mokosuiteclient');
// 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('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_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_mokosuiteclient&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('#__mokosuiteclient_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);
}
}
@@ -1,75 +0,0 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage com_mokosuiteclient.site
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Moko\Component\MokoSuiteClient\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('mokosuiteclient.tickets', 'com_mokosuiteclient');
// 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('#__mokosuiteclient_tickets', 't'))
->leftJoin($db->quoteName('#__mokosuiteclient_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('#__mokosuiteclient_ticket_categories'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->categories = $db->loadObjectList() ?: [];
parent::display($tpl);
}
}
@@ -1,241 +0,0 @@
<?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="mokosuiteclient-portal-ticket">
<div class="mb-3">
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&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_mokosuiteclient&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_mokosuiteclient&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_mokosuiteclient&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_mokosuiteclient&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>
@@ -1,83 +0,0 @@
<?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="mokosuiteclient-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_mokosuiteclient&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_mokosuiteclient">
<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_mokosuiteclient&view=ticket&id=' . $t->id); ?>"><?php echo $t->id; ?></a></td>
<td><a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&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>
@@ -1,204 +0,0 @@
<?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_mokosuiteclient&task=display.searchKb&format=json');
$submitUrl = Route::_('index.php?option=com_mokosuiteclient&task=display.submitTicket&format=json');
$ticketUrl = Route::_('index.php?option=com_mokosuiteclient&view=ticket&id=');
$ticketsUrl = Route::_('index.php?option=com_mokosuiteclient&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="mokosuiteclient-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>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>MOD_MOKOSUITECLIENT_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCache</namespace>
@@ -11,34 +11,25 @@ defined('_JEXEC') or die;
use Joomla\CMS\Session\Session;
$token = Session::getFormToken();
$cacheUrl = 'index.php?option=com_mokosuiteclient&task=clearCache&format=json';
$tempUrl = 'index.php?option=com_mokosuiteclient&task=clearTemp&format=json';
$cacheUrl = 'index.php?option=com_mokosuiteclient&task=display.clearCache&format=json';
$tempUrl = 'index.php?option=com_mokosuiteclient&task=display.clearTemp&format=json';
$domain = $domain ?? '';
?>
<style>
.mokosuiteclient-cleaner { display:flex; align-items:center; gap:0; padding:0 0.25rem; }
.mokosuiteclient-cleaner-label { font-size:0.8rem; color:var(--template-text-dark,#495057); white-space:nowrap; padding-inline-end:0.35rem; }
.mokosuiteclient-cleaner-btn { cursor:pointer; padding:0.2rem 0.5rem; font-size:0.8rem; border-radius:3px; text-decoration:none; color:var(--template-text-dark,#495057); transition:background 0.15s; white-space:nowrap; }
.mokosuiteclient-cleaner-btn:hover { background:rgba(0,0,0,0.08); color:var(--template-text-dark,#212529); text-decoration:none; }
.mokosuiteclient-cleaner-sep { color:var(--template-text-dark,#adb5bd); padding:0 0.1rem; font-size:0.8rem; }
.mokosuiteclient-domain { font-family:monospace; font-size:0.75rem; color:var(--template-text-dark,#6c757d); cursor:pointer; padding:0.15rem 0.4rem; border-radius:3px; transition:background 0.15s; }
.mokosuiteclient-domain:hover { background:rgba(0,0,0,0.06); }
</style>
<div class="header-item-content mokosuiteclient-cleaner">
<?php if ($domain): ?>
<span class="mokosuiteclient-domain" id="mokosuiteclient-domain" title="Support key — click to copy"><?php echo htmlspecialchars($domain); ?></span>
<span class="mokosuiteclient-cleaner-sep">|</span>
<?php endif; ?>
<span class="mokosuiteclient-cleaner-label">Clear:</span>
<a href="#" class="mokosuiteclient-cleaner-btn" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache">
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache
</a>
<span class="mokosuiteclient-cleaner-sep">|</span>
<a href="#" class="mokosuiteclient-cleaner-btn" id="mokosuiteclient-clear-temp" title="Clear temp directory">
<span class="icon-trash" aria-hidden="true" id="mokosuiteclient-temp-icon"></span> Temp
</a>
<div class="header-item">
<div class="header-item-content d-flex align-items-center gap-0" style="padding:0;">
<?php if ($domain): ?>
<a href="#" class="btn btn-sm btn-outline-secondary rounded-0 rounded-start border-end-0 d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-domain" title="Support key — click to copy" style="font-size:0.8rem;">
<span class="icon-key" aria-hidden="true"></span> <?php echo htmlspecialchars($domain); ?>
</a>
<?php endif; ?>
<a href="#" class="btn btn-sm btn-outline-primary <?php echo $domain ? 'rounded-0 border-end-0' : 'rounded-0 rounded-start border-end-0'; ?> d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache" style="font-size:0.8rem;">
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache
</a>
<a href="#" class="btn btn-sm btn-outline-danger rounded-0 rounded-end d-flex align-items-center gap-1 px-3 py-2" id="mokosuiteclient-clear-temp" title="Clear temp directory" style="font-size:0.8rem;">
<span class="icon-trash" aria-hidden="true" id="mokosuiteclient-temp-icon"></span> Temp
</a>
</div>
</div>
<script>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>MOD_MOKOSUITECLIENT_CATEGORIES_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCategories</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>MOD_MOKOSUITECLIENT_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCpanel</namespace>
@@ -47,8 +47,9 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
$data['currentIp'] = $helper->getCurrentIp();
$data['ssl'] = $helper->getSslStatus();
// Daily support PIN derived from health token + today's date (UTC)
// Support PIN — only shown if requested within last 72 hours
$data['supportPin'] = '';
$data['supportPinAvailable'] = false;
try
{
@@ -65,9 +66,17 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
if (!empty($token))
{
$date = gmdate('Y-m-d');
$hash = hash_hmac('sha256', $date, $token);
$data['supportPin'] = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
$data['supportPinAvailable'] = true;
$pinRequestedAt = $coreParams->support_pin_requested_at ?? '';
$pinTtl = 72 * 3600; // 72 hours
if (!empty($pinRequestedAt) && (time() - (int) $pinRequestedAt) < $pinTtl)
{
// PIN is active — generate from the request timestamp (stable for 72h window)
$window = floor((int) $pinRequestedAt / $pinTtl);
$hash = hash_hmac('sha256', (string) $window, $token);
$data['supportPin'] = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
}
}
}
catch (\Throwable $e) {}
@@ -32,9 +32,11 @@ class CpanelHelper
$pkgCache = json_decode($db->loadResult() ?? '{}');
return (object) [
'sitename' => $config->get('sitename', ''),
'mokosuiteclient_version' => $pkgCache->version ?? '',
'joomla_version' => (new Version())->getShortVersion(),
'php_version' => PHP_VERSION,
'db_type' => $config->get('dbtype', 'mysql'),
'debug' => (bool) $config->get('debug'),
'offline' => (bool) $config->get('offline'),
];
@@ -8,6 +8,7 @@
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
@@ -60,175 +61,70 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
?>
<div class="mod-mokosuiteclient-cpanel card p-3 mb-4">
<!-- Header row -->
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-link p-0 text-muted" data-bs-toggle="collapse" data-bs-target="#mokosuiteclient-cpanel-body" aria-expanded="<?php echo $collapsed ? 'false' : 'true'; ?>" aria-controls="mokosuiteclient-cpanel-body" id="mokosuiteclient-cpanel-toggle" style="font-size:1rem;line-height:1;width:1.5rem;">
<span class="fa-solid fa-caret-<?php echo $collapsed ? 'right' : 'down'; ?>" aria-hidden="true" id="mokosuiteclient-cpanel-caret"></span>
</button>
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span>
<strong>MokoSuite</strong>
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokosuiteclient_version ?? ''); ?></span>
<?php if (!empty($supportPin)): ?>
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;" title="Support PIN"><?php echo htmlspecialchars($supportPin); ?></span>
<div class="d-flex flex-wrap align-items-center gap-2" style="font-size:0.85rem;">
<?php $canDashboard = Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokosuiteclient'); ?>
<?php if ($canDashboard): ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient'); ?>" style="color:#1a2744;text-decoration:none;" title="MokoSuite Dashboard"><span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem"></span></a>
<?php else: ?>
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.1rem;color:#1a2744"></span>
<?php endif; ?>
<span class="fw-bold"><?php echo htmlspecialchars($siteInfo->sitename ?? ''); ?></span>
<span class="badge bg-primary">MokoSuite <?php echo htmlspecialchars($siteInfo->mokosuiteclient_version ?? ''); ?></span>
<?php if (!empty($supportPin)): ?>
<span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Support PIN — valid for 72 hours"><span class="icon-key small me-1" aria-hidden="true"></span><?php echo htmlspecialchars($supportPin); ?></span>
<?php elseif (!empty($supportPinAvailable)): ?>
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2" id="mokosuiteclient-request-pin"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json'); ?>"
data-token="<?php echo Session::getFormToken(); ?>"
style="font-size:0.75rem;" title="Request a support PIN (valid 72 hours)">
<span class="icon-key" aria-hidden="true"></span> Request PIN
</button>
<?php endif; ?>
<span class="badge bg-secondary">Joomla <?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?></span>
<span class="badge bg-secondary">PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
<span class="badge bg-secondary"><?php echo htmlspecialchars($siteInfo->db_type ?? ''); ?></span>
<?php if (!empty($siteInfo->debug)): ?>
<span class="badge bg-warning text-dark">Debug</span>
<span class="badge bg-warning text-dark">Debug ON</span>
<?php endif; ?>
<?php if (!empty($siteInfo->offline)): ?>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
<?php if (($counts->moko_updates ?? 0) > 0): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-info text-decoration-none" title="MokoSuite updates available">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->moko_updates; ?> MokoSuite update<?php echo $counts->moko_updates > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<?php if ($counts->updates > 0 && $counts->updates !== ($counts->moko_updates ?? 0)): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none" title="Other updates available">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates - ($counts->moko_updates ?? 0); ?> update<?php echo ($counts->updates - ($counts->moko_updates ?? 0)) > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<span class="ms-auto">
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient'); ?>" class="btn btn-sm btn-primary">
<span class="icon-cogs" aria-hidden="true"></span>
<?php echo Text::_('MOD_MOKOSUITECLIENT_CPANEL_OPEN_DASHBOARD'); ?>
</a>
<span class="ms-auto d-flex align-items-center gap-2">
<span class="icon-globe" aria-hidden="true"></span>
<code><?php echo htmlspecialchars($currentIp); ?></code>
</span>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var target = document.getElementById('mokosuiteclient-cpanel-body');
var caret = document.getElementById('mokosuiteclient-cpanel-caret');
if (target && caret) {
target.addEventListener('show.bs.collapse', function() { caret.className = 'fa-solid fa-caret-down'; });
target.addEventListener('hide.bs.collapse', function() { caret.className = 'fa-solid fa-caret-right'; });
}
});
</script>
<!-- Collapsible body -->
<div class="collapse<?php echo $collapsed ? '' : ' show'; ?> mt-3" id="mokosuiteclient-cpanel-body">
<?php if ($showHealth && $showStats): ?>
<!-- Health + stats row -->
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<?php if ($healthOk): ?>
<span class="icon-check-circle text-success d-block" style="font-size:1.5rem"></span>
<small class="text-success fw-bold">Healthy</small>
<?php else: ?>
<span class="icon-exclamation-circle text-danger d-block" style="font-size:1.5rem"></span>
<small class="text-danger fw-bold">DB Error</small>
<?php endif; ?>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<span class="fw-bold d-block" style="font-size:1.25rem"><?php echo $counts->articles; ?></span>
<small class="text-muted">Articles</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<span class="fw-bold d-block" style="font-size:1.25rem"><?php echo $counts->users; ?></span>
<small class="text-muted">Users</small>
</div>
</div>
<div class="col-6 col-md-3">
<div class="border rounded p-2 text-center h-100">
<?php if ($counts->updates > 0): ?>
<span class="fw-bold d-block text-warning" style="font-size:1.25rem"><?php echo $counts->updates; ?></span>
<small class="text-warning">Updates</small>
<?php else: ?>
<span class="icon-check d-block text-success" style="font-size:1.25rem"></span>
<small class="text-muted">Up to date</small>
<?php endif; ?>
</div>
</div>
</div>
<!-- Info + plugins + actions (consolidated) -->
<div class="d-flex flex-wrap align-items-center gap-2">
<?php if ($showDisk && $diskPct !== null): ?>
<span class="text-muted d-inline-flex align-items-center gap-1">
<span class="icon-hdd" aria-hidden="true"></span>
<?php echo $diskPct; ?>%
<span class="progress d-inline-flex" style="width:40px;height:5px"><span class="progress-bar <?php echo $diskColor; ?>" style="width:<?php echo $diskPct; ?>%"></span></span>
<?php echo number_format(($disk->free_mb ?? 0) / 1024, 1); ?>G free
</span>
<?php endif; ?>
<?php if ($showIp && $currentIp): ?>
<span class="text-muted"><span class="icon-globe" aria-hidden="true"></span> <code><?php echo htmlspecialchars($currentIp); ?></code></span>
<?php endif; ?>
<?php $ssl = $ssl ?? null; if ($ssl): ?>
<span class="badge bg-<?php echo $ssl->critical ? 'danger' : ($ssl->warning ? 'warning text-dark' : 'success'); ?>" title="SSL expires <?php echo $ssl->expires; ?>">
<span class="icon-lock" aria-hidden="true"></span>
SSL <?php echo $ssl->days_remaining; ?>d
</span>
<?php endif; ?>
<?php if ($showVersions): ?>
<span class="text-muted">J<?php echo htmlspecialchars($siteInfo->joomla_version ?? ''); ?> / PHP <?php echo htmlspecialchars($siteInfo->php_version ?? ''); ?></span>
<?php endif; ?>
<?php if ($showPlugins && !empty($plugins)): ?>
<span class="border-start ps-2 ms-1"></span>
<?php foreach ($plugins as $p): ?>
<?php
$label = $labels[$p->element] ?? $p->element;
$badge = $p->enabled ? 'bg-success' : 'bg-secondary';
$icon = $p->enabled ? 'icon-check' : 'icon-times';
$configUrl = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . (int) $p->extension_id);
?>
<a href="<?php echo $configUrl; ?>" class="badge <?php echo $badge; ?> text-decoration-none" title="<?php echo htmlspecialchars($p->name); ?>">
<span class="<?php echo $icon; ?>" aria-hidden="true"></span> <?php echo htmlspecialchars($label); ?>
</a>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($showActions): ?>
<span class="border-start ps-2 ms-1"></span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="mokosuiteclient-cpanel-cache"
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.clearCache&format=json'); ?>"
data-token="<?php echo $token; ?>">
<span class="icon-trash" aria-hidden="true"></span> Clear Cache
</button>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="btn btn-sm btn-outline-secondary">
<span class="icon-refresh" aria-hidden="true"></span> Check Updates
</a>
<?php if ($counts->updates > 0): ?>
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none">
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates; ?> update<?php echo $counts->updates > 1 ? 's' : ''; ?>
</a>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div><!-- /.collapse -->
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('mokosuiteclient-cpanel-cache');
var btn = document.getElementById('mokosuiteclient-request-pin');
if (!btn) return;
btn.addEventListener('click', function() {
var el = this;
var url = el.dataset.url;
var token = el.dataset.token;
el.disabled = true;
var icon = el.querySelector('span');
var origClass = icon ? icon.className : '';
if (icon) icon.className = 'icon-spinner icon-spin';
el.textContent = '...';
var fd = new FormData();
fd.append(token, '1');
fetch(url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
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:['Cache cleared.']});
else Joomla.renderMessages({error:[d.message||'Failed']});
if (d.success && d.pin) {
var badge = document.createElement('span');
badge.className = 'badge bg-dark';
badge.style = 'font-family:monospace;letter-spacing:0.08em;cursor:help;';
badge.title = 'Support PIN — valid for 72 hours';
badge.innerHTML = '<span class="icon-key small me-1" aria-hidden="true"></span>' + d.pin;
el.replaceWith(badge);
} else {
Joomla.renderMessages({error:[d.message||'Failed to generate PIN']});
el.disabled = false;
el.innerHTML = '<span class="icon-key" aria-hidden="true"></span> Request PIN';
}
})
.catch(function(){Joomla.renderMessages({error:['Network error']})})
.finally(function(){
.catch(function(){
Joomla.renderMessages({error:['Network error']});
el.disabled = false;
if (icon) icon.className = origClass;
el.innerHTML = '<span class="icon-key" aria-hidden="true"></span> Request PIN';
});
});
});
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>MokoSuiteClient admin sidebar menu — renders a dedicated MokoSuiteClient section in the admin menu before Joomla's default menu.</description>
<namespace path="src">Moko\Module\MokoSuiteClientMenu</namespace>
@@ -17,17 +17,26 @@ $app = Factory::getApplication();
$currentOption = $app->getInput()->get('option', '');
$currentView = $app->getInput()->get('view', '');
// ── Static views for com_mokosuiteclient ──────────────────────────────────
$mokosuiteclientStaticViews = [
['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokosuiteclient'],
['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokosuiteclient&view=extensions'],
['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokosuiteclient&view=htaccess'],
['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokosuiteclient&view=privacy'],
['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokosuiteclient&view=waflog'],
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokosuiteclient&view=database'],
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokosuiteclient&view=cleanup'],
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuiteclient'],
// ── Static views for com_mokosuiteclient (ACL-gated) ──────────────────────
$user = $app->getIdentity();
$allViews = [
['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokosuiteclient', 'acl' => 'mokosuiteclient.dashboard'],
['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokosuiteclient&view=extensions', 'acl' => 'mokosuiteclient.extensions'],
['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokosuiteclient&view=htaccess', 'acl' => 'mokosuiteclient.htaccess'],
['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokosuiteclient&view=waflog', 'acl' => 'mokosuiteclient.security.waflog'],
['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokosuiteclient&view=privacy', 'acl' => 'core.admin'],
['icon' => 'fa-solid fa-code', 'title' => 'Snippets', 'link' => 'index.php?option=com_mokosuiteclient&view=snippets', 'acl' => 'mokosuiteclient.snippets.manage'],
['icon' => 'fa-solid fa-file-lines', 'title' => 'Templates', 'link' => 'index.php?option=com_mokosuiteclient&view=templates', 'acl' => 'mokosuiteclient.templates.manage'],
['icon' => 'fa-solid fa-right-left', 'title' => 'Replacements', 'link' => 'index.php?option=com_mokosuiteclient&view=replacements','acl' => 'mokosuiteclient.replacements.manage'],
['icon' => 'fa-solid fa-shuffle', 'title' => 'Conditions', 'link' => 'index.php?option=com_mokosuiteclient&view=conditions', 'acl' => 'mokosuiteclient.conditions.manage'],
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokosuiteclient&view=database', 'acl' => 'core.admin'],
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokosuiteclient&view=cleanup', 'acl' => 'mokosuiteclient.cache'],
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokosuiteclient', 'acl' => 'core.admin'],
];
$isSuper = $user->authorise('core.admin', 'com_mokosuiteclient');
$mokosuiteclientStaticViews = array_filter($allViews, function ($v) use ($user, $isSuper) {
return $isSuper || $user->authorise($v['acl'], 'com_mokosuiteclient');
});
// ── Auto-discover all Moko components from #__menu ──────────────────
$mokoComponents = [];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage plg_system_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.47.81
* PATH: /src/Field/ArticlesField.php
* BRIEF: List field that populates with published Joomla articles
*/
namespace Moko\Plugin\System\MokoSuiteClient\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Form field that renders a dropdown (or multi-select) of all published
* articles, grouped by category name.
*
* Usage in XML:
* <field name="related_article" type="Articles" label="Related Article" multiple="true" />
*
* @since 02.47.62
*/
class ArticlesField extends ListField
{
protected $type = 'Articles';
protected function getOptions(): array
{
$options = parent::getOptions();
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('a.id', 'value'),
$db->quoteName('a.title', 'text'),
$db->quoteName('c.title', 'category'),
])
->from($db->quoteName('#__content', 'a'))
->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid')
->where($db->quoteName('a.state') . ' = 1')
->order($db->quoteName('a.title') . ' ASC');
$db->setQuery($query);
$articles = $db->loadObjectList() ?: [];
foreach ($articles as $article) {
$label = $article->text;
if (!empty($article->category)) {
$label .= ' [' . $article->category . ']';
}
$options[] = HTMLHelper::_('select.option', $article->value, $label);
}
return $options;
}
}
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.47.27
* VERSION: 02.47.81
* PATH: /src/Field/CopyableTokenField.php
* BRIEF: Read-only token field with a copy-to-clipboard button
*/
@@ -30,7 +30,7 @@
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>MokoSuiteClient core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
<namespace path=".">Moko\Plugin\System\MokoSuiteClient</namespace>
<scriptfile>script.php</scriptfile>
@@ -99,7 +99,94 @@
description="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_DESC"
filter="url" />
<field name="monitor_signing_key" type="hidden"
<field name="auto_clear_cache" type="radio" default="0"
label="Auto-Clear Cache on Save"
description="Automatically clear Joomla cache when articles, modules, or extensions are saved."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="protect_emails" type="radio" default="0"
label="Email Protection"
description="Obfuscate email addresses in HTML output to prevent spam bot harvesting. Uses JavaScript decloaking."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="snippets_enabled" type="radio" default="0"
label="Snippets"
description="Enable {snippet alias=&quot;name&quot;} content tags. Reusable text/HTML blocks stored in the database with variable substitution support."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="content_templates_enabled" type="radio" default="0"
label="Content Templates"
description="Enable {template alias=&quot;name&quot;} content tags. Loads structured template data from the content_templates table and renders introtext + fulltext."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="articles_anywhere_enabled" type="radio" default="0"
label="Articles Anywhere"
description="Enable {article id=&quot;42&quot;}[title]{/article} content tags. Insert article data anywhere using template placeholders for title, introtext, author, category, dates, images, and more."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="users_anywhere_enabled" type="radio" default="0"
label="Users Anywhere"
description="Allow {user} tags to display user information in content. Use {user id=&quot;42&quot;}[name]{/user} for specific users or {user name} for the current logged-in user."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="users_allow_email" type="radio" default="0"
label="Users: Show Email"
description="Allow the [email] placeholder in {user} tags to display the real email address. When disabled, emails are masked."
class="btn-group btn-group-yesno"
showon="users_anywhere_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="users_allow_username" type="radio" default="1"
label="Users: Show Username"
description="Allow the [username] placeholder in {user} tags to display the real username. When disabled, usernames are masked."
class="btn-group btn-group-yesno"
showon="users_anywhere_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="replacements_enabled" type="radio" default="0"
label="ReReplacer"
description="Enable backend-managed string and regex replacement rules. Published rules from the replacements table are applied to site and/or admin content."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="sourcerer_enabled" type="radio" default="0"
label="Code Embedding (Sourcerer)"
description="Allow embedding PHP, JavaScript, and CSS code in content via {source} tags. Security restricted."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="sourcerer_forbidden_functions" type="text" default="exec,system,passthru,shell_exec,popen,proc_open,dl,eval,call_user_func,call_user_func_array,assert,array_map,array_filter,array_walk,usort,uasort,uksort,create_function,preg_replace_callback,ob_start"
label="Forbidden PHP Functions"
description="Comma-separated list of PHP functions blocked in {source} tags. Backtick operator is always blocked."
showon="sourcerer_enabled:1" />
<field name="monitor_signing_key" type="hidden"
default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ2xZNnNzOTZpeTZOOGMKTHRxbndhbnU4eEozdDcrdDhXT3hoY0Yyclc2QmlmOVhNaEpnYkw0c055N0wwV1dTT2tkMmZxalBNcDFtOFNyNAo1VnNycjE3cFc5b0FNMmtmdFdsaTZ1NkhTVEYyN2pVVUJrT3o4MHZMRklMMGNGNkJCUkpYN2JVWkRpamdUMjc1ClREb3dXZy82Zk9GeWFEelBHUkJuYXFacTljU2lEYWoyNlpSTVZIbktQUERWTG92VzRPTDQzL2gwZ3BtN25nUGIKdWJlLzFFTDRUMHFRbm1Xc2FEOFZ6VStoRXFGSDRTVUtMaDVNeklGbUxFZzRlZ0xCbTBXcWdxbzZRQVBnZDVPYgoybXhmQndta3RLVm5hcWR6eG9KSytzaTVuZkYreGpxbWRMZThUdmEyTHNuTUxlZmsrODVoQ3hxS2x1eWRta1lXCjlvUk5qcDhiQWdNQkFBRUNnZ0VBQkZOUS9NSVZaV2gxdlZUMFh3TFBvUEkyZjI4TTBrM0gzN0t4MXBxK2t5QzYKenRyK1pBczBCaEFEWjAwNHJOUmRYaG45N0QxVXBJYVdLeUJFZkNZQUEzWmxneS9WQmdGR21sR3VuMWNvdGdXUQoyYzg0SWhLdzNzVFFqL2dJWUxOelFWMTBLUTJYd0JZVHZ1MWhjRFpLeUxCUGJTQ1F4cEhQUGdVcUNRNFljR3lFClErVmc1dHJUYk8wQ2xCZ1U5bkVnYU1RakRJZ0F3WVZPV203dUxJTW84UC9nT3FuT2tmaFhzdzl3VTJVYWxFeTEKRmRZbGhMbGJ0ZS9MZ3lkYlJ2RStjNEtqZVp0Z3ptc1RneEh2dzM5YVVmZUZTclFRT0FjcXc0alNzUjdMck9UZAp5bDhpelRrZVBrTVFMamFqR0pabWdPbitkRzhtUlpMa3FKcWdGaVpqRVFLQmdRRFV0L0xlU0h5SmhvY3VFL240CkZreEpaclJoWUVsWnc2WlZJUnQzWDlPQ1Nmaklab3I1ZkZlczhvUzZySFhKdGZYeWx4QUxOSjJjTUhKTTViVnUKbUFSUFU4cThBeVc0OE03cHAyNmtVVTMxNXc2OU1SUkhzbWgyekRabEtDeG5GM1NSQ3U4YW95d3hZc3RUZ3hkTgo2bDhLNHZsS1dsN3FYblBhWjZjb3lQSU9od0tCZ1FESENuRmRRdW5SMVI2dkxGaVFZMTRiT3QwT0tzVGJYMUJyCmpvUGZySkxvRm5mSCs4VDVnNUdxYkV5T2p0WG1tRXhmTFFpcDBQVXRtc1E0YXlJRFBZYWZtU3RpK2dtQXZFd1MKZTlKcVYxYlRuazUrYnVRZ2FlOW16REpJWkxaczRJUlhrd1Q5aDZ4Q2xKeS80TGJSRHdBU3dUVGJlY01hN3A4UgpQN0p0bjdsYnpRS0JnQzNOR2FjUTFuZktGb3N1VS9FOTQ5a2VHeEtvWjhMREpLcEp3WjgzYTlRdTF6bFhFdTlhCi9ZbklnaG1yam9VSy85VG0vOVpaMHVIUmNKcnNEdCtzTGFsaThsRC9JSDBzcEhDYzAyN2Y3cmhXc3M2N3BaRTIKY2RXNmJLL2xNWUpWQTQxRFhHNVEyZkFjUklsTHZaWFNNL3FsR21ZUEJVYlRaWUNPTnVqS000dzdBb0dBU1dBdwpLcEZnWVZxUDFVUWo0aGEvdW9vWXRBQlFVZzd4TnJWektDSVdoampDTDVkQkpqcTZtSGtVUC9tb0lUcEQ3VkpNCnYwMnBGUWJaRDNOdk5vS1gvbjRZNElRTXZNaXR3cUtqRDFEalVXQXF6N0ZScUNGbGdDQUc2V2szVnl2dG5kczEKRzhISVgwTXFCaEp4VXVDVXhsVXpoelY4RjVHZ1VsdUpDNkMyVklFQ2dZQkJWSkxpZlNVOTlHWGZtK3dPd0RWcgo2bHZoUFgxOTBGVktWQXY3aVVWTXBwWXg4Y0QxYkcyUjRLT29JbnkxYTlxdjA2ZGFzeGVQOStkVjJVMWU3MWl5CkFXWDRBVHIrYitvSGk2eUk1MXRHRk54RUxiNXZYMVpYM3VNaDlWM29iYUpuSFNjYllpKzBBNjlyRmNuNEZuLzUKWXJybWxLTzRlRHFVZkswbVFJVCtwUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"
filter="raw" />
</fieldset>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.47.27
* VERSION: 02.47.81
* PATH: /src/script.php
* BRIEF: Installation script for MokoSuiteClient plugin
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.47.27
* VERSION: 02.47.81
* PATH: /src/services/provider.php
* BRIEF: Service provider for dependency injection in Joomla 5.x
* NOTE: Registers the plugin with Joomla's DI container
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientBackup</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DBIP_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDBIP</namespace>
@@ -15,6 +15,8 @@ PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DELETE_VERSIONS_LABEL="Delete All Versions"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_LABEL="Reset Download Keys"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_DLKEYS_DESC="One-shot: clear all download keys (dlid) from update sites on save. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_LABEL="Reset Tour Prompts"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_DESC="One-shot: reset all guided tour completion flags on save. Allows tours to re-trigger for all users. Automatically turns off after execution."
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES="Mirror Domains & Staging Environments"
PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_FIELDSET_ALIASES_DESC="Configure domain aliases that share this site's hosting folder. Each mirror can independently bypass offline mode and control search engine indexing."
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDevTools</namespace>
@@ -61,6 +61,14 @@
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="reset_tour_prompts" type="radio" default="0"
label="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_LABEL"
description="PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_RESET_TOURS_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="site_aliases"
@@ -118,6 +118,13 @@ class DevTools extends CMSPlugin implements SubscriberInterface
$params->set('reset_download_keys', 0);
}
// Reset tour prompts on save if toggled on
if ($params->get('reset_tour_prompts', 0))
{
$this->resetTourPrompts();
$params->set('reset_tour_prompts', 0);
}
// Reset the one-shot toggles
if ($table->params !== $params->toString())
{
@@ -160,6 +167,21 @@ class DevTools extends CMSPlugin implements SubscriberInterface
return $count;
}
private function resetTourPrompts(): int
{
$db = Factory::getDbo();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__user_profiles'))
->where($db->quoteName('profile_key') . ' LIKE ' . $db->quote('guidedtours.tour%'))
)->execute();
$count = $db->getAffectedRows();
$this->getApplication()->enqueueMessage(\sprintf('Reset %d guided tour completion flags.', $count), 'message');
return $count;
}
private function resetDownloadKeys(): int
{
$db = Factory::getDbo();
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientFirewall</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_LICENSE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientLicense</namespace>
<files><folder>src</folder><folder>services</folder><folder>language</folder></files>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_OFFLINE_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientOffline</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_TENANT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientTenant</namespace>
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_TASK_MOKOSUITECLIENTDEMO_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/DemoResetService.php
* VERSION: 02.47.27
* VERSION: 02.47.81
* BRIEF: Content-only snapshot/restore for demo site reset
*/
@@ -12,7 +12,7 @@
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>PLG_TASK_MOKOSUITECLIENTSYNC_DESC</description>
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace>
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncReceiver.php
* VERSION: 02.47.27
* VERSION: 02.47.81
* BRIEF: Receiver-side content sync applies incoming payload to local DB
*/
@@ -10,7 +10,7 @@
* INGROUP: MokoSuiteClient
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncService.php
* VERSION: 02.47.27
* VERSION: 02.47.81
* BRIEF: Sender-side content sync builds payload and pushes to remote sites
*/
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.47.27</version>
<version>02.47.81</version>
<description>Joomla Web Services API routes for MokoSuiteClient site management — health checks, cache, updates, backups, and site info.</description>
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteClient</namespace>
<files>
@@ -155,22 +155,5 @@ final class MokoSuiteClientApi extends CMSPlugin implements SubscriberInterface
)
);
// Helpdesk Tickets API (#142)
$router->createCRUDRoutes(
'v1/mokosuiteclient/tickets',
'tickets',
['component' => 'com_mokosuiteclient']
);
// Ticket reply (custom route — POST only)
$router->addRoute(
new \Joomla\Router\Route(
['POST'],
'v1/mokosuiteclient/tickets/:id/reply',
'tickets.reply',
['id' => '(\d+)'],
['component' => 'com_mokosuiteclient']
)
);
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteClient</name>
<packagename>mokosuiteclient</packagename>
<version>02.47.27</version>
<version>02.47.81</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+190 -65
View File
@@ -108,6 +108,9 @@ class Pkg_MokosuiteclientInstallerScript
// Set up MokoSuiteClient guided tours and unpublish Joomla defaults
$this->setupGuidedTours();
// Register MokoSuiteClient guided tour content (tours + steps)
$this->registerGuidedTours();
// Clean up orphaned empty-element rows and stale files from old DEFAULT '' bug
$this->cleanupEmptyElements();
@@ -1486,97 +1489,217 @@ class Pkg_MokosuiteclientInstallerScript
);
$db->execute();
// Define MokoSuiteClient tours
// Remove old-format tours (superseded by com_mokosuiteclient.* UIDs)
$oldUids = [
$db->quote('mokosuiteclient-welcome'),
$db->quote('mokosuiteclient-firewall'),
$db->quote('mokosuiteclient-extensions'),
];
// Delete orphaned steps first
$subQuery = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__guidedtours'))
->where($db->quoteName('uid') . ' IN (' . implode(',', $oldUids) . ')');
$db->setQuery($subQuery);
$oldTourIds = $db->loadColumn();
if (!empty($oldTourIds))
{
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__guidedtour_steps'))
->where($db->quoteName('tour_id') . ' IN (' . implode(',', array_map('intval', $oldTourIds)) . ')')
)->execute();
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__guidedtours'))
->where($db->quoteName('uid') . ' IN (' . implode(',', $oldUids) . ')')
)->execute();
}
// Tour registration is now handled by registerGuidedTours()
}
catch (\Throwable $e)
{
Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* Register MokoSuiteClient guided tours and their steps.
*
* Inserts tour definitions into #__guidedtours and step definitions into
* #__guidedtour_steps. Skips if the tables do not exist (pre-Joomla 4.3)
* or if a tour with the same uid already exists.
*
* @return void
*
* @since 02.47.09
*/
private function registerGuidedTours(): void
{
try
{
$db = Factory::getDbo();
// Check if #__guidedtours table exists (Joomla 4.3+)
$tables = $db->getTableList();
$prefix = $db->getPrefix();
if (!\in_array($prefix . 'guidedtours', $tables, true))
{
return;
}
$now = date('Y-m-d H:i:s');
// Define tours
$tours = [
[
'uid' => 'mokosuiteclient-welcome',
'title' => 'Welcome to MokoSuiteClient',
'desc' => 'Get started with the MokoSuiteClient Admin Tools Suite. This tour shows you the key areas of your admin dashboard.',
'url' => 'administrator/index.php?option=com_mokosuiteclient',
'steps' => [
['title' => 'MokoSuiteClient Dashboard', 'desc' => 'This is your MokoSuiteClient control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.', 'target' => '#mokosuiteclient-dashboard', 'type' => 0],
['title' => 'Site Information', 'desc' => 'The info bar shows your Joomla version, PHP version, database type, and debug/offline status at a glance.', 'target' => '.mokosuiteclient-info-bar', 'type' => 0],
['title' => 'Quick Actions', 'desc' => 'Use these buttons to clear cache, check updates, manage extensions, and perform common admin tasks with one click.', 'target' => '#mokosuiteclient-btn-cache', 'type' => 0],
['title' => 'Feature Plugins', 'desc' => 'MokoSuiteClient features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.', 'target' => '.mokosuiteclient-plugin-grid', 'type' => 0],
['title' => 'MokoSuiteClient Menu', 'desc' => 'The MokoSuiteClient sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.', 'target' => '.mokosuiteclient-admin-menu, [class*="mokosuiteclient"]', 'type' => 0],
'uid' => 'com_mokosuiteclient.welcome',
'title' => 'MokoSuite Welcome',
'description' => 'Get started with MokoSuite — configure your health token, send your first heartbeat, and set up trusted IPs.',
'extensions' => '["com_mokosuiteclient"]',
'url' => 'administrator/index.php?option=com_mokosuiteclient',
'steps' => [
[
'title' => 'Welcome to MokoSuite',
'description' => 'This is your MokoSuite control panel. Let\'s walk through the key features.',
'target' => '#mokosuiteclient-dashboard',
'type' => 2,
'position' => 'bottom',
],
[
'title' => 'Site Info Bar',
'description' => 'Your site name, version, support PIN, and system info at a glance.',
'target' => '.card.mb-4:first-child',
'type' => 2,
'position' => 'bottom',
],
[
'title' => 'Quick Actions',
'description' => 'Clear cache, check updates, manage extensions, and more.',
'target' => '#mokosuiteclient-btn-cache',
'type' => 2,
'position' => 'right',
],
[
'title' => 'Plugin Cards',
'description' => 'Enable, disable, and configure MokoSuite plugins from here.',
'target' => '.mokosuiteclient-plugin-card:first-child',
'type' => 2,
'position' => 'top',
],
],
],
[
'uid' => 'mokosuiteclient-firewall',
'title' => 'MokoSuiteClient Firewall Setup',
'desc' => 'Configure the Web Application Firewall to protect your site from common attacks.',
'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&filter[search]=mokosuiteclient_firewall',
'steps' => [
['title' => 'Firewall Plugin', 'desc' => 'The MokoSuiteClient Firewall provides 10 security shields including SQL injection, XSS, and malicious user agent detection.', 'target' => '', 'type' => 0],
['title' => 'WAF Shields', 'desc' => 'Enable or disable individual WAF shields. Each shield protects against a specific attack vector. All shields are enabled by default.', 'target' => '', 'type' => 0],
['title' => 'Security Headers', 'desc' => 'Configure HTTP security headers like X-Frame-Options, Content-Security-Policy, and HSTS to harden your site against browser-based attacks.', 'target' => '', 'type' => 0],
['title' => 'IP Blocklist', 'desc' => 'Block specific IP addresses, CIDR ranges, or wildcard patterns. The auto-ban feature automatically blocks IPs that trigger too many WAF alerts.', 'target' => '', 'type' => 0],
],
],
[
'uid' => 'mokosuiteclient-extensions',
'title' => 'Moko Extensions Manager',
'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.',
'url' => 'administrator/index.php?option=com_mokosuiteclient&view=extensions',
'steps' => [
['title' => 'Extension Catalog', 'desc' => 'Browse all available Moko Consulting extensions. Each card shows the extension name, description, install status, and current version.', 'target' => '', 'type' => 0],
['title' => 'Install Extensions', 'desc' => 'Click Install to add an extension from the Moko Consulting repository. Updates are handled through Joomla\'s standard update system.', 'target' => '', 'type' => 0],
'uid' => 'com_mokosuiteclient.firewall',
'title' => 'MokoSuite Firewall Setup',
'description' => 'Configure your Web Application Firewall — trusted IPs, WAF shields, and security headers.',
'extensions' => '["com_mokosuiteclient"]',
'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&extension_id=0',
'steps' => [
[
'title' => 'Your Current IP',
'description' => 'This shows your IP address. Copy it to add to the Trusted IPs list.',
'target' => '#mokosuiteclient-current-ip',
'type' => 2,
'position' => 'bottom',
],
[
'title' => 'Trusted IPs',
'description' => 'Add IPs that should bypass WAF checks — your office, VPN, etc.',
'target' => '#jform_params_trusted_ips',
'type' => 2,
'position' => 'top',
],
[
'title' => 'WAF Shields',
'description' => 'Enable protection against SQL injection, XSS, malicious agents, and more.',
'target' => '#attrib-waf',
'type' => 2,
'position' => 'bottom',
],
],
],
];
foreach ($tours as $tourDef)
{
// Check if tour already exists
$db->setQuery(
$db->getQuery(true)
->select('id')
->from($db->quoteName('#__guidedtours'))
->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid']))
);
// Check if tour already exists by uid
$query = $db->getQuery(true)
->select($db->quoteName('id'))
->from($db->quoteName('#__guidedtours'))
->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid']));
$db->setQuery($query);
$existingId = (int) $db->loadResult();
if ($db->loadResult())
if ($existingId)
{
continue;
// Update existing tour metadata
$update = $db->getQuery(true)
->update($db->quoteName('#__guidedtours'))
->set($db->quoteName('title') . ' = ' . $db->quote($tourDef['title']))
->set($db->quoteName('description') . ' = ' . $db->quote($tourDef['description']))
->set($db->quoteName('extensions') . ' = ' . $db->quote($tourDef['extensions']))
->set($db->quoteName('url') . ' = ' . $db->quote($tourDef['url']))
->set($db->quoteName('published') . ' = 1')
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
->where($db->quoteName('id') . ' = ' . $existingId);
$db->setQuery($update)->execute();
// Delete existing steps so they are re-inserted fresh
$delete = $db->getQuery(true)
->delete($db->quoteName('#__guidedtour_steps'))
->where($db->quoteName('tour_id') . ' = ' . $existingId);
$db->setQuery($delete)->execute();
$tourId = $existingId;
}
else
{
// Insert new tour
$tour = (object) [
'title' => $tourDef['title'],
'uid' => $tourDef['uid'],
'description' => $tourDef['description'],
'extensions' => $tourDef['extensions'],
'url' => $tourDef['url'],
'created' => $now,
'created_by' => 0,
'modified' => $now,
'modified_by' => 0,
'published' => 1,
'language' => '*',
'note' => 'MokoSuiteClient',
'access' => 1,
'ordering' => 0,
'autostart' => 0,
];
$db->insertObject('#__guidedtours', $tour, 'id');
$tourId = (int) $tour->id;
}
$tour = (object) [
'title' => $tourDef['title'],
'uid' => $tourDef['uid'],
'description' => $tourDef['desc'],
'extensions' => '',
'url' => $tourDef['url'],
'created' => date('Y-m-d H:i:s'),
'created_by' => 0,
'modified' => date('Y-m-d H:i:s'),
'modified_by' => 0,
'published' => 1,
'language' => '*',
'note' => 'MokoSuiteClient',
'access' => 3,
'ordering' => 0,
'autostart' => 0,
];
$db->insertObject('#__guidedtours', $tour, 'id');
$tourId = (int) $tour->id;
// Insert steps
foreach ($tourDef['steps'] as $i => $stepDef)
{
$step = (object) [
'tour_id' => $tourId,
'title' => $stepDef['title'],
'description' => $stepDef['desc'],
'description' => $stepDef['description'],
'target' => $stepDef['target'],
'type' => $stepDef['type'],
'interactive_type' => 1,
'url' => '',
'position' => 'bottom',
'position' => $stepDef['position'],
'ordering' => $i + 1,
'published' => 1,
'created' => date('Y-m-d H:i:s'),
'created' => $now,
'created_by' => 0,
'modified' => date('Y-m-d H:i:s'),
'modified' => $now,
'modified_by' => 0,
'language' => '*',
'note' => '',
@@ -1586,10 +1709,12 @@ class Pkg_MokosuiteclientInstallerScript
$db->insertObject('#__guidedtour_steps', $step, 'id');
}
}
Log::add('Registered ' . \count($tours) . ' MokoSuiteClient guided tours.', Log::INFO, 'mokosuiteclient');
}
catch (\Throwable $e)
{
Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
Log::add('Guided tour registration error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}