Compare commits

..

2 Commits

Author SHA1 Message Date
gitea-actions[bot] b3d9efc7d9 chore(version): pre-release bump to 02.51.00-rc [skip ci] 2026-06-28 18:30:30 +00:00
gitea-actions[bot] 902670d678 chore(release): build 02.49.00-rc [skip ci] 2026-06-28 18:29:19 +00:00
67 changed files with 294 additions and 1646 deletions
+1 -3
View File
@@ -7,7 +7,7 @@
# INGROUP: mokocli.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.01.00
# VERSION: 05.00.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +=======================================================================+
@@ -75,7 +75,6 @@ jobs:
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools
env:
@@ -174,7 +173,6 @@ jobs:
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes
run: |
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 02.52.15
# VERSION: 02.51.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+61 -7
View File
@@ -14,21 +14,75 @@
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
PATH: ./CHANGELOG.md
VERSION: 02.52.15
VERSION: 02.51.00
BRIEF: Version history using `Keep a Changelog`
-->
# Changelog
## [Unreleased]
## [02.52.15] --- 2026-06-29
## [02.49.00] --- 2026-06-28
## [02.52.15] --- 2026-06-29
### Added
- **Mirror Domains & Staging** — repeatable subform table in DevTools plugin for configuring domain aliases with per-alias offline bypass, robots directive, and labels
- **Daily Support PIN** — HMAC-SHA256 rotating PIN shown on cpanel module, component dashboard, and HQ site cards
- **Support PIN in status bar** — cache/temp module now shows PIN request button instead of domain; click to request, click again to copy
- **Frontend link in status bar** — cache/temp module now has 4 buttons: Site (frontend link), PIN, Cache, Temp
- **Help buttons** — all admin views link to Gitea wiki pages via toolbar help button
- **Support PIN in heartbeat** — core system plugin includes current PIN in heartbeat payload to HQ
- **HQ config sync** — client stores HQ-configured `support_pin_hours` from heartbeat response, PIN TTL now configurable from HQ
## [02.52.13] --- 2026-06-29
### Changed
- **Support PIN UI unified** — `SupportPinHelper::renderBadge()` and `renderScript()` replace 3 separate inline implementations (dashboard, cpanel module, cache module) with click-to-copy on all PIN badges
- Admin sidebar menu module now loads component-local language files (fixes untranslated keys for MokoSuiteCross and other components)
- Support PIN TTL is now configurable via HQ global options instead of hardcoded 72 hours
- Removed MokoSuiteHQ from extension catalog (internal app, not for client sites)
- **SupportPinHelper** — shared helper centralises PIN generation across dashboard, cpanel module, cache module, and AJAX controller
- **Current IP display** — firewall plugin settings show admin's IP with copy button
- **Heartbeat monitor** — consolidated into core plugin from retired monitor plugin, with diagnostic logging on all bail-out points
- **Backup bridge plugin** — discovers MokoSuiteBackup's BackupStatusHelper and sends status in heartbeat payloads
- **Activity log** — blockchain-style hash chain for tamper detection in MokoSuiteHQ
- **Dev domain in heartbeat** — client sends dev alias to HQ for display on dashboard
- **Login app badges** — recent logins table shows Admin/Site badge parsed from Joomla action log message JSON
## [02.52.13] --- 2026-06-29
### Changed
- **Plugin install** — self-healing: extracts plugin zips from package on every update, creates missing extension records with namespace
- **Menu naming** — MokoSuiteClient displays as "MokoSuite", MokoSuiteHQ as "MokoHQ", others stripped of prefix
- **Menu ordering** — HQ first, MokoSuite second, others alphabetical
- **Cpanel module** — always starts collapsed, access level 3 (Special), pretty plugin badge labels
- **Module namespaces** — fixed cpanel (MokoSuiteCpanel → MokoSuiteClientCpanel) and cache (MokoSuiteCache → MokoSuiteClientCache)
- **Health checks** — return status:error on exceptions instead of false status:ok; MokoSuiteBackup detection queries correct table
- **Heartbeat** — correct URL (suite.dev), correct API route (mokosuitehq), correct headers (X-MokoSuite-*), fresh RSA key pair
- **Date formats** — all templates use Joomla locale-aware DATE_FORMAT_LC2/LC4
- **Domains** — updated from waas.dev to suite.dev.mokoconsulting.tech throughout
- **Dashboard info bar** — reverted stacked layout; info items back to horizontal row
- **Extension version bar** — full-width auto-sized strip with equal-width cells and border separators
- **Recent logins** — exact match on LOGGED_IN key (excludes logout noise), limit increased to 10
## [02.52.04] --- 2026-06-29
### Removed
- **Helpdesk/tickets** — migrated to MokoSuiteCRM (issue #67)
- **Monitor plugin** — retired, config consolidated into core plugin
- **Backup bridge** — temporarily removed from package manifest (build pipeline issue)
- **Update server migration** — removed migrateUpdateServerUrls, cleanupStaleUpdateSites, fixUpdateRecords, enableUpdateServer calls
## [02.52.04] --- 2026-06-29
### Fixed
- **Regular Labs import** — destination tables missing from SQL update files; sites that upgraded never got the tables, causing "No data found" on import
- **Regular Labs import banner** — detection now requires both source AND destination tables before showing the import button
- **DB-IP auto-enrichment** — all IPs in `<code>` tags in admin backend now show country flag emoji and geo tooltip on hover
- **MokoSuiteBackup quick action** — dashboard now includes MokoSuiteBackup button when component is installed
- **PIN copy** — fixed duplicate click handlers (4 toast messages), "Copied!" not reverting, added "Click to copy" hover tooltip
- Health endpoint cron check SQL error — orphan `setQuery(getQuery(true), 0, 5)` produced bare `LIMIT 5`, returning 503 for all health polls
- License plugin missing `src/` and `language/` directories causing install failure
- PIN generation inconsistency — controller used `floor(now/TTL)` while display used `floor(requestedAt/TTL)`
- Plugin files installing to group root instead of element subdirectory (ALTER TABLE DEFAULT '' + empty element cleanup)
- Orphan extension rows with empty element or display-name-as-element
- Module not publishing (ensureAdminModule direct DB update bypasses checked_out)
- RSA key pair had Windows line endings causing signature verification failure
- Heartbeat connection failing due to wrong domain, route, and header names
## [02.44.00] --- 2026-06-20
## [02.42.00] --- 2026-06-20
## [02.42.00] --- 2026-06-20
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.52.15
VERSION: 02.51.00
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.52.15
VERSION: 02.51.00
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.52.15
VERSION: 02.51.00
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.52.15
VERSION: 02.51.00
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.52.15
VERSION: 02.51.00
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.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Build Guide (VERSION: 02.51.00)
## 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.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Configuration Guide (VERSION: 02.51.00)
## 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.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Installation Guide (VERSION: 02.51.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Operations Guide (VERSION: 02.51.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.51.00)
## Introduction
+2 -2
View File
@@ -7,13 +7,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Testing Guide (VERSION: 02.51.00)
## 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.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.51.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Guides
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.51.00)
## Introduction
+2 -2
View File
@@ -10,13 +10,13 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoSuiteClient.Documentation
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
VERSION: 02.52.15
VERSION: 02.51.00
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.52.15)
# MokoSuiteClient Documentation Index (VERSION: 02.51.00)
## 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.52.15
VERSION: 02.51.00
BRIEF: Baseline documentation for the MokoSuiteClient system plugin
NOTE: Foundational reference for internal and external stakeholders
-->
# MokoSuiteClient Plugin Overview (VERSION: 02.52.15)
# MokoSuiteClient Plugin Overview (VERSION: 02.51.00)
## 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.52.15
VERSION: 02.51.00
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
MokoSuite Extension Catalog
Each entry points to the extension's own updates.xml on the main branch.
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.
@@ -18,7 +18,7 @@
<icon>icon-shield-alt</icon>
<category>Platform</category>
<protected>true</protected>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteBackup</name>
@@ -27,7 +27,7 @@
<description>Full-site backup and restore for Joomla — database, files, and configuration.</description>
<icon>icon-archive</icon>
<category>Platform</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
@@ -40,7 +40,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCRM/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteERP</name>
@@ -49,7 +49,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteERP/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteShop</name>
@@ -58,7 +58,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteShop/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuitePOS</name>
@@ -67,7 +67,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuitePOS/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteMRP</name>
@@ -76,7 +76,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteMRP/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteHRM</name>
@@ -85,7 +85,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHRM/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteRestaurant</name>
@@ -94,7 +94,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteRestaurant/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteChild</name>
@@ -103,7 +103,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteChild/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteNPO</name>
@@ -112,7 +112,7 @@
<description>Nonprofit management: donors, donations, campaigns, grants, volunteers, events.</description>
<icon>icon-heart</icon>
<category>Industry</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteNPO/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteNPO/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteField</name>
@@ -121,7 +121,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCreate</name>
@@ -130,7 +130,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCreate/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
@@ -143,7 +143,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteForms/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCommunity</name>
@@ -152,7 +152,7 @@
<description>Community profiles, connections, and activity streams for Joomla.</description>
<icon>icon-users</icon>
<category>Content</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCommunity/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCommunity/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteCross</name>
@@ -161,7 +161,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCross/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteOpenGraph</name>
@@ -170,7 +170,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/raw/branch/main/updates.xml</updateserver>
</extension>
<extension>
<name>MokoSuiteStoreLocator</name>
@@ -179,7 +179,7 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
@@ -192,7 +192,7 @@
<description>Hero module — image slideshow, video backgrounds, solid color/gradient, parallax.</description>
<icon>icon-image</icon>
<category>Modules</category>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml</updateserver>
</extension>
<!-- ═══════════════════════════════════════════════════════════════════
@@ -205,6 +205,6 @@
<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/updates.xml</updateserver>
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml</updateserver>
</extension>
</catalog>
@@ -17,9 +17,3 @@ COM_MOKOSUITECLIENT_MENU_WAFLOG="WAF Log"
COM_MOKOSUITECLIENT_MENU_DATABASE="Database Tools"
COM_MOKOSUITECLIENT_MENU_CLEANUP="Cache Cleanup"
COM_MOKOSUITECLIENT_MENU_CACHE="Cache Management"
COM_MOKOSUITECLIENT_MENU_CONDITIONS="Conditions"
COM_MOKOSUITECLIENT_MENU_SNIPPETS="Snippets"
COM_MOKOSUITECLIENT_MENU_TEMPLATES="Content Templates"
COM_MOKOSUITECLIENT_MENU_REPLACEMENTS="Replacements"
COM_MOKOSUITECLIENT_MENU_AUTOMATION="Automation"
COM_MOKOSUITECLIENT_MENU_MODULES="Modules"
@@ -1,11 +0,0 @@
-- 02.52.00 — Create license cache table (missing from prior migrations)
-- This table was in install.mysql.sql but never had an update file,
-- so existing installs never received it.
CREATE TABLE IF NOT EXISTS `#__mokosuite_license_cache` (
`dlid_hash` CHAR(64) NOT NULL COMMENT 'SHA-256 of DLID (never store raw DLID)',
`response_data` TEXT NOT NULL COMMENT 'JSON validation response from MokoGitea',
`checked_at` DATETIME NOT NULL,
PRIMARY KEY (`dlid_hash`),
KEY `idx_checked` (`checked_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -36,7 +36,6 @@ class DisplayController extends BaseController
'templates' => 'mokosuiteclient.templates.manage',
'replacements' => 'mokosuiteclient.replacements.manage',
'conditions' => 'mokosuiteclient.conditions.manage',
'modules' => 'core.admin',
];
public function display($cachable = false, $urlparams = [])
@@ -86,12 +85,7 @@ class DisplayController extends BaseController
public function sendHeartbeat()
{
if (!Session::checkToken())
{
$this->jsonResponse(['success' => false, 'message' => 'Session expired — please reload the page.']);
return;
}
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
try
{
@@ -806,65 +800,6 @@ class DisplayController extends BaseController
$this->jsonResponse($this->getModel('Import')->importAdminTools());
}
// ==================================================================
// Toggle Published
// ==================================================================
public function togglePublished()
{
if (!Session::checkToken())
{
$this->jsonResponse(['success' => false, 'message' => Text::_('JINVALID_TOKEN')]);
return;
}
if (!$this->checkAcl('core.admin'))
{
$this->jsonForbidden();
return;
}
$app = Factory::getApplication();
$table = $app->getInput()->getString('table', '');
$id = $app->getInput()->getInt('id', 0);
$allowed = ['mokosuiteclient_conditions', 'mokosuiteclient_snippets',
'mokosuiteclient_replacements', 'mokosuiteclient_content_templates', 'modules'];
if (!in_array($table, $allowed, true) || $id <= 0)
{
$this->jsonResponse(['success' => false, 'message' => 'Invalid table or ID.']);
return;
}
try
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$dbTable = '#__' . $table;
$current = (int) $db->setQuery(
$db->getQuery(true)
->select($db->quoteName('published'))
->from($db->quoteName($dbTable))
->where($db->quoteName('id') . ' = ' . $id)
)->loadResult();
$newState = $current ? 0 : 1;
$db->setQuery(
$db->getQuery(true)
->update($db->quoteName($dbTable))
->set($db->quoteName('published') . ' = ' . $newState)
->where($db->quoteName('id') . ' = ' . $id)
)->execute();
$this->jsonResponse(['success' => true, 'published' => $newState]);
}
catch (\Throwable $e)
{
$this->jsonResponse(['success' => false, 'message' => $e->getMessage()]);
}
}
// ==================================================================
// Helpers
// ==================================================================
@@ -1,112 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ConditionsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('c.id'),
$db->quoteName('c.alias'),
$db->quoteName('c.name'),
$db->quoteName('c.description'),
$db->quoteName('c.category'),
$db->quoteName('c.color'),
$db->quoteName('c.match_all'),
$db->quoteName('c.published'),
'(SELECT COUNT(*) FROM ' . $db->quoteName('#__mokosuiteclient_conditions_groups')
. ' WHERE ' . $db->quoteName('condition_id') . ' = ' . $db->quoteName('c.id') . ') AS group_count',
'(SELECT COUNT(*) FROM ' . $db->quoteName('#__mokosuiteclient_conditions_rules', 'r')
. ' INNER JOIN ' . $db->quoteName('#__mokosuiteclient_conditions_groups', 'g')
. ' ON ' . $db->quoteName('g.id') . ' = ' . $db->quoteName('r.group_id')
. ' WHERE ' . $db->quoteName('g.condition_id') . ' = ' . $db->quoteName('c.id') . ') AS rule_count',
])
->from($db->quoteName('#__mokosuiteclient_conditions', 'c'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('c.name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('c.alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('c.published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('c.name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions', 'c'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('c.name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('c.alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('c.published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
public function getGroupCount(int $conditionId): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions_groups'))
->where($db->quoteName('condition_id') . ' = ' . $conditionId)
);
return (int) $db->loadResult();
}
public function getRuleCount(int $conditionId): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_conditions_rules', 'r'))
->join('INNER', $db->quoteName('#__mokosuiteclient_conditions_groups', 'g')
. ' ON ' . $db->quoteName('g.id') . ' = ' . $db->quoteName('r.group_id'))
->where($db->quoteName('g.condition_id') . ' = ' . $conditionId)
);
return (int) $db->loadResult();
}
}
@@ -1,93 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ModulesModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('m.id'),
$db->quoteName('m.title'),
$db->quoteName('m.module'),
$db->quoteName('m.position'),
$db->quoteName('m.published'),
$db->quoteName('m.ordering'),
$db->quoteName('m.client_id'),
$db->quoteName('m.access'),
$db->quoteName('m.language'),
])
->from($db->quoteName('#__modules', 'm'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('m.title') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.module') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.position') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('m.published') . ' = ' . (int) $filters['published']);
}
if ($filters['client_id'] !== '' && $filters['client_id'] !== null)
{
$query->where($db->quoteName('m.client_id') . ' = ' . (int) $filters['client_id']);
}
$query->order($db->quoteName('m.client_id') . ' ASC, '
. $db->quoteName('m.position') . ' ASC, '
. $db->quoteName('m.ordering') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__modules', 'm'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('m.title') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.module') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('m.position') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('m.published') . ' = ' . (int) $filters['published']);
}
if ($filters['client_id'] !== '' && $filters['client_id'] !== null)
{
$query->where($db->quoteName('m.client_id') . ' = ' . (int) $filters['client_id']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -1,69 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class ReplacementsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_replacements'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('search') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_replacements'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('search') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -1,69 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class SnippetsModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_snippets'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_snippets'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -1,69 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\Model;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Database\DatabaseInterface;
class TemplatesModel extends BaseDatabaseModel
{
public function getItems(array $filters = [], int $limit = 50, int $offset = 0): array
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('*')
->from($db->quoteName('#__mokosuiteclient_content_templates'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$query->order($db->quoteName('ordering') . ' ASC, ' . $db->quoteName('name') . ' ASC');
$db->setQuery($query, $offset, $limit);
return $db->loadObjectList() ?: [];
}
public function getTotal(array $filters = []): int
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiteclient_content_templates'));
if (!empty($filters['search']))
{
$search = $db->quote('%' . $db->escape($filters['search'], true) . '%');
$query->where('(' . $db->quoteName('name') . ' LIKE ' . $search
. ' OR ' . $db->quoteName('alias') . ' LIKE ' . $search . ')');
}
if ($filters['published'] !== '' && $filters['published'] !== null)
{
$query->where($db->quoteName('published') . ' = ' . (int) $filters['published']);
}
$db->setQuery($query);
return (int) $db->loadResult();
}
}
@@ -1,55 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Conditions;
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 $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ConditionsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Conditions', 'shuffle');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -1,56 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Modules;
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 $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ModulesModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
'client_id' => $input->get('filter_client', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Module Manager', 'cube');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -1,55 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Replacements;
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 $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\ReplacementsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Replacements', 'right-left');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -1,55 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Snippets;
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 $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\SnippetsModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Snippets', 'code');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -1,55 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Moko\Component\MokoSuiteClient\Administrator\View\Templates;
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 $items = [];
protected $total = 0;
protected $filters = [];
public function display($tpl = null)
{
$model = new \Moko\Component\MokoSuiteClient\Administrator\Model\TemplatesModel();
$input = Factory::getApplication()->getInput();
$this->filters = [
'search' => $input->getString('filter_search', ''),
'published' => $input->get('filter_published', ''),
];
$page = max(1, $input->getInt('page', 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$this->items = $model->getItems($this->filters, $limit, $offset);
$this->total = $model->getTotal($this->filters);
$this->addToolbar();
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
$wa->registerAndUseStyle('com_mokosuiteclient.dashboard', 'com_mokosuiteclient/dashboard.css');
parent::display($tpl);
}
protected function addToolbar(): void
{
ToolbarHelper::title('Content Templates', 'file-lines');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuiteclient');
}
}
@@ -1,142 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-conditions">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="conditions">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-shuffle"></span> Conditions</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Match</th>
<th style="width:8%">Groups</th>
<th style="width:8%">Rules</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="8" class="text-center text-muted py-4">No conditions found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><code><?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?></code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><span class="badge bg-<?php echo $item->match_all ? 'primary' : 'warning text-dark'; ?>"><?php echo $item->match_all ? 'ALL' : 'ANY'; ?></span></td>
<td><?php echo (int) $item->group_count; ?></td>
<td><?php echo (int) $item->rule_count; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_conditions" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=conditions&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -1,152 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
$publishedLabels = [1 => 'Published', 0 => 'Unpublished', -2 => 'Trashed'];
$publishedColors = [1 => 'success', 0 => 'danger', -2 => 'dark'];
?>
<div id="mokosuiteclient-modules">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="modules">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search title, type, or position..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_client" class="form-select form-select-sm">
<option value="">All Clients</option>
<option value="0"<?php echo $filters['client_id'] === '0' ? ' selected' : ''; ?>>Site</option>
<option value="1"<?php echo $filters['client_id'] === '1' ? ' selected' : ''; ?>>Administrator</option>
</select>
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
<option value="-2"<?php echo $filters['published'] === '-2' ? ' selected' : ''; ?>>Trashed</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-cube"></span> Module Manager</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Title</th>
<th>Position</th>
<th>Type</th>
<th style="width:8%">Client</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="7" class="text-center text-muted py-4">No modules found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<a href="<?php echo Route::_('index.php?option=com_modules&task=module.edit&id=' . (int) $item->id); ?>">
<?php echo htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); ?>
</a>
</td>
<td><code><?php echo htmlspecialchars($item->position ?: '(none)', ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><small><?php echo htmlspecialchars($item->module, ENT_QUOTES, 'UTF-8'); ?></small></td>
<td><span class="badge bg-<?php echo $item->client_id ? 'dark' : 'primary'; ?>"><?php echo $item->client_id ? 'Admin' : 'Site'; ?></span></td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<?php
$pub = (int) $item->published;
$label = $publishedLabels[$pub] ?? 'Unknown';
$color = $publishedColors[$pub] ?? 'secondary';
?>
<a href="#" class="mokosuite-toggle-module badge bg-<?php echo $color; ?>"
data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $label; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=modules&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')
. ($filters['client_id'] !== '' ? '&filter_client=' . $filters['client_id'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-module').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', 'modules');
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-module badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-module badge bg-warning text-dark';
});
});
});
});
</script>
@@ -1,142 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-replacements">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="replacements">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or pattern..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-right-left"></span> Replacements</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Search</th>
<th>Replace</th>
<th style="width:7%">Area</th>
<th style="width:5%">Regex</th>
<th>Category</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="8" class="text-center text-muted py-4">No replacement rules found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
</td>
<td><code style="font-size:0.8rem"><?php echo htmlspecialchars(mb_strimwidth($item->search, 0, 50, '...'), ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><code style="font-size:0.8rem"><?php echo htmlspecialchars(mb_strimwidth($item->replace_value, 0, 50, '...'), ENT_QUOTES, 'UTF-8'); ?></code></td>
<td><span class="badge bg-secondary"><?php echo htmlspecialchars($item->area, ENT_QUOTES, 'UTF-8'); ?></span></td>
<td><?php echo $item->regex ? '<span class="badge bg-warning text-dark">Yes</span>' : ''; ?></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_replacements" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=replacements&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -1,141 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-snippets">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="snippets">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-code"></span> Snippets</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4">No snippets found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($item->description): ?>
<br><small class="text-muted"><?php echo htmlspecialchars(mb_strimwidth($item->description, 0, 80, '...'), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</td>
<td><code>{snippet <?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?>}</code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_snippets" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=snippets&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -1,141 +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
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
$items = $this->items;
$total = $this->total;
$filters = $this->filters;
$token = Session::getFormToken();
$input = Factory::getApplication()->getInput();
$page = max(1, $input->getInt('page', 1));
$pages = max(1, ceil($total / 50));
?>
<div id="mokosuiteclient-templates">
<form method="get" action="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates'); ?>" class="mb-3">
<input type="hidden" name="option" value="com_mokosuiteclient">
<input type="hidden" name="view" value="templates">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<input type="text" name="filter_search" class="form-control form-control-sm" placeholder="Search by name or alias..." value="<?php echo htmlspecialchars($filters['search'], ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="col-md-2">
<select name="filter_published" class="form-select form-select-sm">
<option value="">All States</option>
<option value="1"<?php echo $filters['published'] === '1' ? ' selected' : ''; ?>>Published</option>
<option value="0"<?php echo $filters['published'] === '0' ? ' selected' : ''; ?>>Unpublished</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary"><span class="icon-search"></span> Filter</button>
<a href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates'); ?>" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><span class="icon-file-alt"></span> Content Templates</span>
<span class="badge bg-secondary"><?php echo number_format($total); ?> total</span>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th style="width:5%">ID</th>
<th>Name</th>
<th>Alias</th>
<th>Category</th>
<th style="width:8%">Order</th>
<th style="width:8%">Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4">No content templates found.</td></tr>
<?php else: ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?php echo (int) $item->id; ?></td>
<td>
<?php if ($item->color): ?>
<span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:<?php echo htmlspecialchars($item->color, ENT_QUOTES, 'UTF-8'); ?>;vertical-align:middle;margin-right:4px;"></span>
<?php endif; ?>
<?php echo htmlspecialchars($item->name, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($item->description): ?>
<br><small class="text-muted"><?php echo htmlspecialchars(mb_strimwidth($item->description, 0, 80, '...'), ENT_QUOTES, 'UTF-8'); ?></small>
<?php endif; ?>
</td>
<td><code><?php echo htmlspecialchars($item->alias, ENT_QUOTES, 'UTF-8'); ?></code></td>
<td>
<?php if ($item->category): ?>
<span class="badge bg-info"><?php echo htmlspecialchars($item->category, ENT_QUOTES, 'UTF-8'); ?></span>
<?php endif; ?>
</td>
<td><?php echo (int) $item->ordering; ?></td>
<td>
<a href="#" class="mokosuite-toggle-published badge bg-<?php echo $item->published ? 'success' : 'danger'; ?>"
data-table="mokosuiteclient_content_templates" data-id="<?php echo (int) $item->id; ?>"
data-token="<?php echo $token; ?>">
<?php echo $item->published ? 'Published' : 'Unpublished'; ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm justify-content-center">
<?php for ($p = 1; $p <= $pages; $p++): ?>
<li class="page-item<?php echo $p === $page ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo Route::_('index.php?option=com_mokosuiteclient&view=templates&page=' . $p
. ($filters['search'] ? '&filter_search=' . urlencode($filters['search']) : '')
. ($filters['published'] !== '' ? '&filter_published=' . $filters['published'] : '')); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.mokosuite-toggle-published').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
var table = this.dataset.table, id = this.dataset.id, token = this.dataset.token, badge = this;
var fd = new FormData();
fd.append('table', table);
fd.append('id', id);
fd.append(token, '1');
fetch('index.php?option=com_mokosuiteclient&task=display.togglePublished&format=json', {
method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'}, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) {
var pub = d.published;
badge.className = 'mokosuite-toggle-published badge bg-' + (pub ? 'success' : 'danger');
badge.textContent = pub ? 'Published' : 'Unpublished';
}
}).catch(function() {
badge.textContent = 'Error';
badge.className = 'mokosuite-toggle-published badge bg-warning text-dark';
});
});
});
});
</script>
@@ -120,37 +120,26 @@ document.addEventListener('DOMContentLoaded', function () {
var icon = btn.querySelector('span');
btn.disabled = true;
if (icon) { icon.className = 'icon-spinner icon-spin'; icon.style.color = ''; }
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.text().then(function (text) {
try { return JSON.parse(text); }
catch (e) { return {success: false, message: 'Server error: ' + text.substring(0, 200)}; }
});
})
.then(function (r) { return r.json(); })
.then(function (d) {
var msg = d.message || (d.success ? 'Heartbeat sent to HQ.' : 'Heartbeat failed.');
if (d.success) {
if (icon) { icon.className = 'icon-check'; icon.style.color = '#198754'; }
Joomla.renderMessages({message: [msg]});
Joomla.renderMessages({message: [d.message || 'Heartbeat sent to HQ.']});
} else {
if (icon) { icon.className = 'icon-times'; icon.style.color = '#dc3545'; }
Joomla.renderMessages({error: [msg]});
Joomla.renderMessages({error: [d.message || 'Heartbeat failed.']});
}
})
.catch(function (err) {
if (icon) { icon.className = 'icon-times'; icon.style.color = '#dc3545'; }
Joomla.renderMessages({error: ['Heartbeat failed: ' + (err.message || 'network error')]});
.catch(function () {
Joomla.renderMessages({error: ['Network error sending heartbeat.']});
})
.finally(function () {
btn.disabled = false;
setTimeout(function () {
if (icon) { icon.className = 'icon-upload'; icon.style.color = ''; }
}, 3000);
if (icon) icon.className = 'icon-upload';
});
});
}
@@ -20,7 +20,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.52.15</version>
<version>02.51.00</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>
@@ -47,12 +47,6 @@
<menu link="option=com_mokosuiteclient&amp;view=waflog" img="class:shield-alt">COM_MOKOSUITECLIENT_MENU_WAFLOG</menu>
<menu link="option=com_mokosuiteclient&amp;view=database" img="class:database">COM_MOKOSUITECLIENT_MENU_DATABASE</menu>
<menu link="option=com_mokosuiteclient&amp;view=cleanup" img="class:trash">COM_MOKOSUITECLIENT_MENU_CLEANUP</menu>
<menu link="option=com_mokosuiteclient&amp;view=conditions" img="class:shuffle">COM_MOKOSUITECLIENT_MENU_CONDITIONS</menu>
<menu link="option=com_mokosuiteclient&amp;view=snippets" img="class:code">COM_MOKOSUITECLIENT_MENU_SNIPPETS</menu>
<menu link="option=com_mokosuiteclient&amp;view=templates" img="class:file-alt">COM_MOKOSUITECLIENT_MENU_TEMPLATES</menu>
<menu link="option=com_mokosuiteclient&amp;view=replacements" img="class:exchange-alt">COM_MOKOSUITECLIENT_MENU_REPLACEMENTS</menu>
<menu link="option=com_mokosuiteclient&amp;view=automation" img="class:random">COM_MOKOSUITECLIENT_MENU_AUTOMATION</menu>
<menu link="option=com_mokosuiteclient&amp;view=modules" img="class:th-large">COM_MOKOSUITECLIENT_MENU_MODULES</menu>
<menu link="option=com_plugins&amp;filter[folder]=system&amp;filter[search]=mokosuiteclient" img="class:power-off">COM_MOKOSUITECLIENT_MENU_PLUGINS</menu>
<menu link="option=com_installer&amp;view=update" img="class:refresh">COM_MOKOSUITECLIENT_MENU_UPDATES</menu>
<menu link="option=com_checkin" img="class:check-square">COM_MOKOSUITECLIENT_MENU_CHECKIN</menu>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.52.15</version>
<version>02.51.00</version>
<description>MOD_MOKOSUITECLIENT_CACHE_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCache</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.52.15</version>
<version>02.51.00</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.52.15</version>
<version>02.51.00</version>
<description>MOD_MOKOSUITECLIENT_CPANEL_DESC</description>
<namespace path="src">Moko\Module\MokoSuiteClientCpanel</namespace>
@@ -7,7 +7,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.52.15</version>
<version>02.51.00</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>
@@ -28,7 +28,6 @@ $allViews = [
['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-cube', 'title' => 'Modules', 'link' => 'index.php?option=com_mokosuiteclient&view=modules', 'acl' => 'core.admin'],
['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'],
@@ -43,10 +42,9 @@ $iconOverrides = [
'com_mokosuiteclient' => 'icon-shield-alt',
'com_mokosuitehq' => 'icon-tachometer-alt',
'com_mokosuitebackup' => 'icon-archive',
'com_mokosuite_crm' => 'icon-address-book',
'com_mokosuite_erp' => 'icon-briefcase',
'com_mokosuitecrm' => 'icon-address-book',
'com_mokosuiteerp' => 'icon-briefcase',
'com_mokosuiteshop' => 'icon-shopping-cart',
'com_mokoshop' => 'icon-shopping-cart',
'com_mokosuitepos' => 'icon-calculator',
'com_mokosuitemrp' => 'icon-cogs',
'com_mokosuitehrm' => 'icon-id-badge',
@@ -58,24 +56,8 @@ $iconOverrides = [
'com_mokosuiteforms' => 'icon-list-alt',
'com_mokosuitecommunity' => 'icon-comments',
'com_mokosuitecross' => 'icon-share-alt',
'com_mokoog' => 'icon-globe',
'com_mokosuiteopengraph' => 'icon-globe',
'com_mokosuitestorelocator' => 'icon-map-marker-alt',
'com_mokosuiteanalytics' => 'icon-chart-line',
'com_mokosuitesecurity' => 'icon-lock',
'com_mokosuitenotify' => 'icon-bell',
'com_mokosuiteworkflow' => 'icon-random',
'com_mokosuiteai' => 'icon-magic',
'com_mokosuiteauto' => 'icon-car',
'com_mokosuitebeauty' => 'icon-spa',
'com_mokosuiteconstruction' => 'icon-hard-hat',
'com_mokosuiteeditor' => 'icon-edit',
'com_mokosuiteevent' => 'icon-calendar',
'com_mokosuiteinsight' => 'icon-lightbulb',
'com_mokosuitelibrary' => 'icon-book',
'com_mokosuiterealty' => 'icon-home',
'com_mokosuitesupport' => 'icon-life-ring',
'com_mokosuitetaxi' => 'icon-taxi',
];
$childIconMap = [
@@ -280,7 +262,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
$hasChildren = !empty($comp['children']);
?>
<?php if ($hasChildren): ?>
<li class="item parent item-level-1 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<li class="item parent item-level-2 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<a class="has-arrow<?php echo $compActive ? ' mm-active' : ''; ?>" href="#">
<span class="<?php echo htmlspecialchars($comp['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -300,7 +282,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
: ($currentView === $childView);
}
?>
<li class="item item-level-2 mokosuiteclient-ext-child<?php echo $childActive ? ' mm-active' : ''; ?>">
<li class="item mokosuiteclient-ext-child<?php echo $childActive ? ' mm-active' : ''; ?>">
<a class="no-dropdown<?php echo $childActive ? ' mm-active' : ''; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo htmlspecialchars($child['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($child['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -310,7 +292,7 @@ $iconStyle = 'display:inline-block!important;width:1.25em;text-align:center;marg
</ul>
</li>
<?php else: ?>
<li class="item item-level-1 mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<li class="item mokosuiteclient-ext-item<?php echo $compActive ? ' mm-active' : ''; ?>">
<a class="no-dropdown<?php echo $compActive ? ' mm-active' : ''; ?>" href="<?php echo Route::_($comp['link']); ?>"<?php echo $compActive ? ' aria-current="page"' : ''; ?>>
<span class="<?php echo htmlspecialchars($comp['icon'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true" style="<?php echo $iconStyle; ?>"></span>
<span class="sidebar-item-title"><?php echo htmlspecialchars($comp['title'], ENT_QUOTES, 'UTF-8'); ?></span>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.52.15
* VERSION: 02.51.00
* PATH: /src/Extension/MokoSuiteClient.php
* NOTE: Core system plugin for MokoSuiteClient admin tools suite
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.52.15
* VERSION: 02.51.00
* PATH: /src/Field/ArticlesField.php
* BRIEF: List field that populates with published Joomla articles
*/
@@ -8,7 +8,7 @@
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.52.15
* VERSION: 02.51.00
* 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.52.15</version>
<version>02.51.00</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>
@@ -22,7 +22,7 @@
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
* VERSION: 02.52.15
* VERSION: 02.51.00
* 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.52.15
* VERSION: 02.51.00
* 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.52.15</version>
<version>02.51.00</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.52.15</version>
<version>02.51.00</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DBIP_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDBIP</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.52.15</version>
<version>02.51.00</version>
<description>PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDevTools</namespace>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.52.15</version>
<version>02.51.00</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.52.15</version>
<version>02.51.00</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.52.15</version>
<version>02.51.00</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.52.15</version>
<version>02.51.00</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.52.15</version>
<version>02.51.00</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.52.15
* VERSION: 02.51.00
* 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.52.15</version>
<version>02.51.00</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.52.15
* VERSION: 02.51.00
* 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.52.15
* VERSION: 02.51.00
* 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.52.15</version>
<version>02.51.00</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>
+1 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteClient</name>
<packagename>mokosuiteclient</packagename>
<version>02.52.15</version>
<version>02.51.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+146
View File
@@ -818,6 +818,152 @@ class Pkg_MokosuiteclientInstallerScript
}
}
/**
* Rewrite all Moko Consulting update server URLs from the old
* raw/branch/main pattern to the new clean /updates.xml pattern.
*
* Old: https://git.mokoconsulting.tech/MokoConsulting/{repo}/raw/branch/main/updates.xml
* New: https://git.mokoconsulting.tech/MokoConsulting/{repo}/updates.xml
*/
private function migrateUpdateServerUrls(): void
{
try
{
$db = Factory::getDbo();
$db->setQuery(
"UPDATE " . $db->quoteName('#__update_sites')
. " SET " . $db->quoteName('location') . " = REPLACE("
. $db->quoteName('location') . ", '/raw/branch/main/updates.xml', '/updates.xml')"
. " WHERE " . $db->quoteName('location') . " LIKE " . $db->quote('%mokoconsulting.tech%/raw/branch/main/updates.xml')
);
$db->execute();
$count = $db->getAffectedRows();
if ($count > 0)
{
Factory::getApplication()->enqueueMessage(
sprintf('Migrated %d Moko update server URL(s) to new format.', $count),
'message'
);
}
}
catch (\Throwable $e)
{
Log::add('Update server URL migration error: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
}
}
/**
* Remove stale and duplicate MokoSuiteClient update site entries.
*
* Keeps only the package-level update site pointing to the dynamic
* MokoGitea endpoint. Removes plugin-level entries, old static URLs,
* and orphaned #__updates rows tied to deleted update sites.
*
* @return void
*
* @since 02.31.00
*/
private function fixUpdateRecords(): void
{
try
{
$db = Factory::getDbo();
// Link orphaned #__updates records to the installed extension
$db->setQuery(
"UPDATE " . $db->quoteName('#__updates') . " u"
. " JOIN " . $db->quoteName('#__extensions') . " e"
. " ON u.element = e.element AND u.type = e.type"
. " SET u.extension_id = e.extension_id"
. " WHERE u.extension_id = 0"
. " AND u.element LIKE " . $db->quote('%mokosuiteclient%')
);
$db->execute();
}
catch (\Throwable $e)
{
// Non-critical
}
}
private function cleanupStaleUpdateSites(): void
{
try
{
$db = Factory::getDbo();
$dynamicUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/updates.xml';
// Find MokoSuiteClient update sites (exclude MokoSuiteClientHQ and other Moko extensions)
$query = $db->getQuery(true)
->select($db->quoteName(['update_site_id', 'location']))
->from($db->quoteName('#__update_sites'))
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoSuiteClient%')
. ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoSuiteClient%') . ')')
->where($db->quoteName('name') . ' NOT LIKE ' . $db->quote('%MokoSuiteClientHQ%'))
->where($db->quoteName('location') . ' NOT LIKE ' . $db->quote('%MokoSuiteClientHQ%'));
$db->setQuery($query);
$sites = $db->loadObjectList();
$keepId = null;
$removeIds = [];
foreach ($sites as $site)
{
if ($site->location === $dynamicUrl && $keepId === null)
{
$keepId = (int) $site->update_site_id;
}
else
{
$removeIds[] = (int) $site->update_site_id;
}
}
if (empty($removeIds))
{
return;
}
$idList = implode(',', $removeIds);
// Remove orphaned #__updates rows
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__updates'))
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
)->execute();
// Remove link rows
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites_extensions'))
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
)->execute();
// Remove stale update sites
$db->setQuery(
$db->getQuery(true)
->delete($db->quoteName('#__update_sites'))
->where($db->quoteName('update_site_id') . ' IN (' . $idList . ')')
)->execute();
$count = count($removeIds);
if ($count > 0)
{
Factory::getApplication()->enqueueMessage(
sprintf('Cleaned up %d stale MokoSuiteClient update site(s).', $count),
'message'
);
}
}
catch (\Throwable $e)
{
Log::add('Error cleaning up stale update sites: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Backup all non-empty extra_query values from update sites.
-27
View File
@@ -1,27 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 02.52.15
-->
<updates>
<update>
<name>MokoSuiteClient</name>
<description>MokoSuiteClient stable build.</description>
<element>pkg_mokosuiteclient</element>
<type>package</type>
<client>site</client>
<version>02.52.15</version>
<creationDate>2026-06-29</creationDate>
<infourl title='MokoSuiteClient'>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/releases/tag/stable</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/releases/download/stable/pkg_mokosuiteclient-02.52.15.zip</downloadurl>
</downloads>
<sha256>ed3f4cec71bfb8a4cc199ae0d6defc88d6de116b0492d2275d1da2afb414e2af</sha256>
<tags><tag>stable</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update>
</updates>