Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5b93d5d9b |
+1
-1
@@ -121,7 +121,7 @@ releases/
|
||||
build/
|
||||
dist/
|
||||
out/
|
||||
/site/
|
||||
site/
|
||||
!source/packages/*/site/
|
||||
*.map
|
||||
*.css.map
|
||||
|
||||
+21
-21
@@ -1,4 +1,4 @@
|
||||
# MokoSuite
|
||||
# MokoWaaS
|
||||
|
||||
Joomla 5/6 admin tools suite — heartbeat health monitoring, extension management, security firewall, tenant restrictions, and site administration.
|
||||
|
||||
@@ -6,10 +6,10 @@ Joomla 5/6 admin tools suite — heartbeat health monitoring, extension manageme
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Package** | `pkg_mokosuite` |
|
||||
| **Package** | `pkg_mokowaas` |
|
||||
| **Language** | PHP 8.1+ |
|
||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||
| **Wiki** | [MokoSuite Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki) |
|
||||
| **Wiki** | [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) |
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -19,38 +19,38 @@ composer install # Install PHP dependencies
|
||||
|
||||
## Architecture
|
||||
|
||||
Joomla **package** (`pkg_mokosuite`) with 17 sub-extensions:
|
||||
Joomla **package** (`pkg_mokowaas`) with 17 sub-extensions:
|
||||
|
||||
### Core Plugin (`plg_system_mokosuite`)
|
||||
- Heartbeat health endpoint (`/?mokosuite=health`) with 16 diagnostic checks
|
||||
### Core Plugin (`plg_system_mokowaas`)
|
||||
- Heartbeat health endpoint (`/?mokowaas=health`) with 16 diagnostic checks
|
||||
- Grafana provisioning and heartbeat sender
|
||||
- Site alias / domain management
|
||||
- Extension cascade (enable/disable coordination)
|
||||
- Download key preservation across Joomla updates
|
||||
- Namespace: `Moko\Plugin\System\MokoSuite`
|
||||
- Namespace: `Moko\Plugin\System\MokoWaaS`
|
||||
|
||||
### Feature Plugins
|
||||
- `plg_system_mokosuite_firewall` — WAF, IP blocklist, security headers, password policy
|
||||
- `plg_system_mokosuite_tenant` — admin restrictions for non-master users
|
||||
- `plg_system_mokosuite_devtools` — dev mode, hit reset, version cleanup, download key reset
|
||||
- `plg_system_mokosuite_offline` — offline mode bypass for legal pages
|
||||
- `plg_system_mokosuite_monitor` — Grafana heartbeat registration
|
||||
- `plg_system_mokowaas_firewall` — WAF, IP blocklist, security headers, password policy
|
||||
- `plg_system_mokowaas_tenant` — admin restrictions for non-master users
|
||||
- `plg_system_mokowaas_devtools` — dev mode, hit reset, version cleanup, download key reset
|
||||
- `plg_system_mokowaas_offline` — offline mode bypass for legal pages
|
||||
- `plg_system_mokowaas_monitor` — Grafana heartbeat registration
|
||||
|
||||
### Component (`com_mokosuite`)
|
||||
### Component (`com_mokowaas`)
|
||||
- Admin dashboard with plugin management, WAF charts, extension catalog
|
||||
- Helpdesk ticketing system
|
||||
- REST API controllers
|
||||
|
||||
### Modules
|
||||
- `mod_mokosuite_cpanel` — admin dashboard widget
|
||||
- `mod_mokosuite_menu` — admin sidebar menu
|
||||
- `mod_mokosuite_cache` — status bar cache/temp cleaner
|
||||
- `mod_mokosuite_categories` — auto-category tree menu
|
||||
- `mod_mokowaas_cpanel` — admin dashboard widget
|
||||
- `mod_mokowaas_menu` — admin sidebar menu
|
||||
- `mod_mokowaas_cache` — status bar cache/temp cleaner
|
||||
- `mod_mokowaas_categories` — auto-category tree menu
|
||||
|
||||
### Task Plugins
|
||||
- `plg_task_mokosuitedemo` — scheduled demo site reset
|
||||
- `plg_task_mokosuitesync` — scheduled content sync
|
||||
- `plg_task_mokosuite_tickets` — ticket automation
|
||||
- `plg_task_mokowaasdemo` — scheduled demo site reset
|
||||
- `plg_task_mokowaassync` — scheduled content sync
|
||||
- `plg_task_mokowaas_tickets` — ticket automation
|
||||
|
||||
### Update Server
|
||||
|
||||
@@ -59,7 +59,7 @@ MokoGitea generates update feeds dynamically from releases — no static `update
|
||||
## Source Directory
|
||||
|
||||
Source lives in `source/` (not `src/`):
|
||||
- `source/pkg_mokosuite.xml` — package manifest
|
||||
- `source/pkg_mokowaas.xml` — package manifest
|
||||
- `source/script.php` — install script
|
||||
- `source/packages/` — all sub-extensions
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Suite Client Site Issue
|
||||
about: Report an issue with a Suite client site (branding, deployment, media sync)
|
||||
name: WaaS Client Site Issue
|
||||
about: Report an issue with a WaaS client site (branding, deployment, media sync)
|
||||
title: '[WAAS] '
|
||||
labels: 'waas, client-site'
|
||||
assignees: ''
|
||||
@@ -52,7 +52,7 @@ Attach screenshots showing the issue (desktop and mobile if relevant).
|
||||
## Template Details
|
||||
- **Joomla Version**: [e.g., 5.x]
|
||||
- **Template Name**: [e.g., clienttemplate]
|
||||
- **MokoSuite Plugin**: [Active / Inactive]
|
||||
- **MokoWaaS Plugin**: [Active / Inactive]
|
||||
- **MokoOnyx Admin**: [Active / Inactive]
|
||||
|
||||
## CSS Custom Properties
|
||||
@@ -11,9 +11,9 @@ INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards
|
||||
PATH: /templates/github/copilot-instructions.joomla.md.template
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoSuite governed repositories
|
||||
NOTE: Synced to .github/copilot-instructions.md in all Joomla/Suite repos via bulk sync.
|
||||
Tokens replaced at sync time: MokoSuite, https://github.com/mokoconsulting-tech/MokoSuite, {{EXTENSION_NAME}},
|
||||
BRIEF: GitHub Copilot custom instructions template for Joomla/MokoWaaS governed repositories
|
||||
NOTE: Synced to .github/copilot-instructions.md in all Joomla/WaaS repos via bulk sync.
|
||||
Tokens replaced at sync time: MokoWaaS, https://github.com/mokoconsulting-tech/MokoWaaS, {{EXTENSION_NAME}},
|
||||
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}
|
||||
-->
|
||||
|
||||
@@ -36,24 +36,24 @@ NOTE: Synced to .github/copilot-instructions.md in all Joomla/Suite repos via bu
|
||||
>
|
||||
> | Placeholder | Where to find the value |
|
||||
> |---|---|
|
||||
> | `MokoSuite` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `https://github.com/mokoconsulting-tech/MokoSuite` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
|
||||
> | `MokoWaaS` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `https://github.com/mokoconsulting-tech/MokoWaaS` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
|
||||
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
|
||||
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
|
||||
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
|
||||
>
|
||||
> ---
|
||||
|
||||
# MokoSuite — GitHub Copilot Custom Instructions
|
||||
# MokoWaaS — GitHub Copilot Custom Instructions
|
||||
|
||||
## What This Repo Is
|
||||
|
||||
This is a **Moko Consulting MokoSuite** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
||||
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
||||
|
||||
Repository URL: https://github.com/mokoconsulting-tech/MokoSuite
|
||||
Repository URL: https://github.com/mokoconsulting-tech/MokoWaaS
|
||||
Extension name: **{{EXTENSION_NAME}}**
|
||||
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
|
||||
Platform: **Joomla 4.x / MokoSuite**
|
||||
Platform: **Joomla 4.x / MokoWaaS**
|
||||
|
||||
---
|
||||
|
||||
@@ -77,9 +77,9 @@ Every new file needs a copyright header as its first content.
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: MokoSuite.{{EXTENSION_TYPE}}
|
||||
* INGROUP: MokoSuite
|
||||
* REPO: https://github.com/mokoconsulting-tech/MokoSuite
|
||||
* DEFGROUP: MokoWaaS.{{EXTENSION_TYPE}}
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://github.com/mokoconsulting-tech/MokoWaaS
|
||||
* PATH: /path/to/file.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of purpose
|
||||
@@ -98,9 +98,9 @@ This file is part of a Moko Consulting project.
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoSuite.Documentation
|
||||
INGROUP: MokoSuite
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoSuite
|
||||
DEFGROUP: MokoWaaS.Documentation
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
|
||||
PATH: /docs/file.md
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: One-line description
|
||||
@@ -138,7 +138,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
|
||||
<version>01.02.04</version>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://github.com/mokoconsulting-tech/MokoSuite/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
|
||||
https://github.com/mokoconsulting-tech/MokoWaaS/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="4\.[0-9]+" />
|
||||
@@ -152,7 +152,7 @@ The version in `README.md` **must always match** the `<version>` tag in `manifes
|
||||
## Joomla Extension Structure
|
||||
|
||||
```
|
||||
MokoSuite/
|
||||
MokoWaaS/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── (no updates.xml) # Update XML is generated dynamically by MokoGitea
|
||||
├── site/ # Frontend (site) code
|
||||
@@ -191,11 +191,11 @@ MokoSuite/
|
||||
https://git.mokoconsulting.tech/{Owner}/{Repo}/updates.xml
|
||||
```
|
||||
|
||||
The package manifest (`pkg_mokosuite.xml`) references it via:
|
||||
The package manifest (`pkg_mokowaas.xml`) references it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="MokoSuite Update Server">
|
||||
https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/updates.xml
|
||||
<server type="extension" priority="1" name="MokoWaaS Update Server">
|
||||
https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
@@ -257,7 +257,7 @@ This repository is governed by [MokoStandards](https://github.com/mokoconsulting
|
||||
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
|
||||
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoSuite Joomla extension development guide |
|
||||
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
-->
|
||||
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||
<identity>
|
||||
<name>MokoSuite</name>
|
||||
<display-name>Package - MokoSuite</display-name>
|
||||
<name>MokoWaaS</name>
|
||||
<display-name>Package - MokoWaaS</display-name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>White-label identity, security hardening, and tenant restriction layer for Suite-managed Joomla environments</description>
|
||||
<version>02.34.50</version>
|
||||
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
|
||||
<version>02.35.00</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -1,66 +1,63 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, type-prefixed packages |
|
||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
@@ -71,21 +71,16 @@ jobs:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
@@ -107,13 +102,14 @@ jobs:
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
@@ -140,7 +136,7 @@ jobs:
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found — aborting release"
|
||||
echo "::error::Merge conflict markers found - aborting release"
|
||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -155,44 +151,23 @@ jobs:
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: "Determine version bump level"
|
||||
id: bump
|
||||
run: |
|
||||
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
|
||||
# Feature/dev branches: bump minor for the new stable release
|
||||
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
|
||||
case "$HEAD_REF" in
|
||||
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
|
||||
*) BUMP="minor" ;;
|
||||
esac
|
||||
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
|
||||
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
BUMP_FLAG=""
|
||||
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
|
||||
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
|
||||
fi
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
@@ -269,7 +244,7 @@ jobs:
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
# Delete rc branch (ephemeral - created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
@@ -326,7 +301,7 @@ jobs:
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Already Released - ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Universal
|
||||
# INGROUP: MokoPlatform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Delete feature branches after PR merge
|
||||
|
||||
name: "Branch Cleanup"
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.gitea/workflows/ci-generic.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||
|
||||
name: "Generic: Project CI"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
- version/**
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
# ── Lint & Validate ───────────────────────────────────────────────────
|
||||
lint:
|
||||
name: Lint & Validate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect toolchain
|
||||
id: detect
|
||||
run: |
|
||||
HAS_PHP=false
|
||||
HAS_NODE=false
|
||||
[ -f "composer.json" ] && HAS_PHP=true
|
||||
[ -f "package.json" ] && HAS_NODE=true
|
||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||
fi
|
||||
php -v
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install PHP dependencies
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "composer.json" ]; then
|
||||
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "package.json" ]; then
|
||||
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: PHP syntax check
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
ERRORS=0
|
||||
while IFS= read -r -d '' file; do
|
||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||
echo "::error file=${file}::PHP syntax error"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
||||
|
||||
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: TypeScript/JavaScript lint
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "node_modules/.bin/eslint" ]; then
|
||||
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
||||
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
||||
echo "::warning::ESLint config found but eslint not installed"
|
||||
else
|
||||
echo "No ESLint configured — skipping"
|
||||
fi
|
||||
|
||||
- name: TypeScript compile check
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
||||
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
||||
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
||||
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: PHPStan static analysis
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
||||
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
||||
fi
|
||||
|
||||
# ── Tests ─────────────────────────────────────────────────────────────
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect toolchain
|
||||
id: detect
|
||||
run: |
|
||||
HAS_PHP=false
|
||||
HAS_NODE=false
|
||||
[ -f "composer.json" ] && HAS_PHP=true
|
||||
[ -f "package.json" ] && HAS_NODE=true
|
||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
||||
|
||||
- name: Run PHP tests
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "vendor/bin/phpunit" ]; then
|
||||
vendor/bin/phpunit --testdox 2>&1
|
||||
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||
echo "::warning::PHPUnit config found but phpunit not installed"
|
||||
else
|
||||
echo "No PHPUnit configured — skipping"
|
||||
fi
|
||||
|
||||
- name: Run Node.js tests
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
||||
npm test 2>&1
|
||||
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "No test script in package.json — skipping"
|
||||
fi
|
||||
|
||||
- name: Build check
|
||||
run: |
|
||||
if [ -f "Makefile" ]; then
|
||||
make build 2>&1 || echo "::warning::Build failed or not configured"
|
||||
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
||||
npm run build 2>&1 || echo "::warning::Build failed"
|
||||
fi
|
||||
@@ -1,500 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# This file is part of a Moko Consulting project.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow.Template
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
||||
# VERSION: 04.06.00
|
||||
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
|
||||
|
||||
name: "Joomla: Extension CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'dev/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
lint-and-validate:
|
||||
name: Lint & Validate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
php -v && composer --version
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
|
||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||
run: |
|
||||
if [ -d "/tmp/moko-platform" ] || [ -d "/opt/moko-platform" ]; then
|
||||
echo "moko-platform already available on runner — skipping clone"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform 2>/dev/null || echo "moko-platform clone skipped — continuing without it"
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
if [ -f "composer.json" ]; then
|
||||
composer install \
|
||||
--no-interaction \
|
||||
--prefer-dist \
|
||||
--optimize-autoloader
|
||||
else
|
||||
echo "No composer.json found — skipping dependency install"
|
||||
fi
|
||||
|
||||
- name: PHP syntax check
|
||||
run: |
|
||||
ERRORS=0
|
||||
for DIR in src/ htdocs/; do
|
||||
if [ -d "$DIR" ]; then
|
||||
FOUND=1
|
||||
while IFS= read -r -d '' FILE; do
|
||||
OUTPUT=$(php -l "$FILE" 2>&1)
|
||||
if echo "$OUTPUT" | grep -q "Parse error"; then
|
||||
echo "::error file=${FILE}::${OUTPUT}"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done < <(find "$DIR" -name "*.php" -print0)
|
||||
fi
|
||||
done
|
||||
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: XML manifest validation
|
||||
run: |
|
||||
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
# Find the extension manifest (XML with <extension tag)
|
||||
MANIFEST=""
|
||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||
MANIFEST="$XML_FILE"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Validate well-formed XML
|
||||
php -r "
|
||||
\$xml = @simplexml_load_file('$MANIFEST');
|
||||
if (\$xml === false) {
|
||||
echo 'INVALID';
|
||||
exit(1);
|
||||
}
|
||||
echo 'VALID';
|
||||
" > /tmp/xml_result 2>&1
|
||||
XML_RESULT=$(cat /tmp/xml_result)
|
||||
if [ "$XML_RESULT" != "VALID" ]; then
|
||||
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# Check required tags: name, version, author
|
||||
for TAG in name version author; do
|
||||
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
|
||||
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
done
|
||||
|
||||
# Namespace is required for components/plugins but not packages
|
||||
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||
if [ "$EXT_TYPE" != "package" ]; then
|
||||
if ! grep -q "<namespace" "$MANIFEST" 2>/dev/null; then
|
||||
echo "Missing required tag: \`<namespace>\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Found required tag: \`<namespace>\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
else
|
||||
echo "Package extension — \`<namespace>\` not required." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Check language files referenced in manifest
|
||||
run: |
|
||||
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
MANIFEST=""
|
||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||
MANIFEST="$XML_FILE"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$MANIFEST" ]; then
|
||||
# Extract language file references from manifest
|
||||
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||
if [ -z "$LANG_FILES" ]; then
|
||||
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
while IFS= read -r LANG_FILE; do
|
||||
LANG_FILE=$(echo "$LANG_FILE" | xargs)
|
||||
if [ -z "$LANG_FILE" ]; then
|
||||
continue
|
||||
fi
|
||||
# Check in common locations
|
||||
FOUND=0
|
||||
for BASE in "." "src" "htdocs"; do
|
||||
if [ -f "${BASE}/${LANG_FILE}" ]; then
|
||||
FOUND=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$FOUND" -eq 0 ]; then
|
||||
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
done <<< "$LANG_FILES"
|
||||
fi
|
||||
else
|
||||
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [ "${ERRORS}" -gt 0 ]; then
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: Check index.html files in directories
|
||||
run: |
|
||||
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
|
||||
MISSING=0
|
||||
CHECKED=0
|
||||
|
||||
for DIR in src/ htdocs/; do
|
||||
if [ -d "$DIR" ]; then
|
||||
while IFS= read -r -d '' SUBDIR; do
|
||||
CHECKED=$((CHECKED + 1))
|
||||
if [ ! -f "${SUBDIR}/index.html" ]; then
|
||||
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
done < <(find "$DIR" -type d -print0)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${CHECKED}" -eq 0 ]; then
|
||||
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${MISSING}" -gt 0 ]; then
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
release-readiness:
|
||||
name: Release Readiness Check
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate release readiness
|
||||
run: |
|
||||
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=0
|
||||
|
||||
# Extract version from README.md
|
||||
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
|
||||
if [ -z "$README_VERSION" ]; then
|
||||
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# Find the extension manifest
|
||||
MANIFEST=""
|
||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||
MANIFEST="$XML_FILE"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Check <version> matches README VERSION
|
||||
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
||||
if [ -z "$MANIFEST_VERSION" ]; then
|
||||
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
|
||||
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# Check extension type, element, client attributes
|
||||
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||
if [ -z "$EXT_TYPE" ]; then
|
||||
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
else
|
||||
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
# Element check (component/module/plugin name)
|
||||
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
|
||||
if [ "$HAS_ELEMENT" -eq 0 ]; then
|
||||
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Client attribute for site/admin modules and plugins
|
||||
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
|
||||
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
|
||||
if [ "$HAS_CLIENT" -eq 0 ]; then
|
||||
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check updates.xml exists
|
||||
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
|
||||
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Check CHANGELOG.md exists
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
if [ $ERRORS -gt 0 ]; then
|
||||
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
else
|
||||
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
test:
|
||||
name: Tests (PHP ${{ matrix.php }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-and-validate
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: ['8.2', '8.3']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP ${{ matrix.php }}
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
php -v && composer --version
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
if [ -f "composer.json" ]; then
|
||||
composer install \
|
||||
--no-interaction \
|
||||
--prefer-dist \
|
||||
--optimize-autoloader
|
||||
else
|
||||
echo "No composer.json found — skipping dependency install"
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
|
||||
EXIT=${PIPESTATUS[0]}
|
||||
if [ $EXIT -eq 0 ]; then
|
||||
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
exit $EXIT
|
||||
else
|
||||
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
static-analysis:
|
||||
name: PHPStan Analysis
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-and-validate
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
php -v && composer --version
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
if [ -f "composer.json" ]; then
|
||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||
fi
|
||||
|
||||
- name: Install PHPStan
|
||||
run: |
|
||||
if ! command -v vendor/bin/phpstan &> /dev/null; then
|
||||
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
|
||||
composer global require phpstan/phpstan --no-interaction
|
||||
fi
|
||||
|
||||
- name: Run PHPStan
|
||||
run: |
|
||||
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
|
||||
PHPSTAN="vendor/bin/phpstan"
|
||||
if [ ! -f "$PHPSTAN" ]; then
|
||||
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
|
||||
fi
|
||||
|
||||
# Determine source directory
|
||||
SRC_DIR=""
|
||||
for DIR in src/ htdocs/ lib/; do
|
||||
if [ -d "$DIR" ]; then
|
||||
SRC_DIR="$DIR"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$SRC_DIR" ]; then
|
||||
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Use repo phpstan.neon if present, otherwise use baseline config
|
||||
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
|
||||
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
|
||||
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
ARGS="$ARGS --level=3"
|
||||
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
|
||||
EXIT=${PIPESTATUS[0]}
|
||||
|
||||
if [ $EXIT -eq 0 ]; then
|
||||
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
|
||||
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
exit $EXIT
|
||||
|
||||
pre-release:
|
||||
name: Build RC Pre-Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-and-validate, test]
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Trigger pre-release build
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ github.head_ref }}
|
||||
run: |
|
||||
curl -s -X POST \
|
||||
"${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \
|
||||
-H "Authorization: token ${GA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -4,10 +4,10 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# INGROUP: moko-platform.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||
|
||||
name: "Universal: Repository Cleanup"
|
||||
@@ -33,17 +33,17 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GA_TOKEN }}
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
- name: Delete merged branches
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Merged Branch Cleanup ==="
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
|
||||
# List branches via API
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||
|
||||
DELETED=0
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
# Check if branch is merged into main
|
||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||
echo " Deleting merged branch: ${BRANCH}"
|
||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
fi
|
||||
@@ -66,20 +66,20 @@ jobs:
|
||||
|
||||
- name: Clean old workflow runs
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Workflow Run Cleanup ==="
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Get old completed runs
|
||||
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/actions/runs?status=completed&limit=50" | \
|
||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||
|
||||
DELETED=0
|
||||
for RUN_ID in $RUNS; do
|
||||
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
done
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Deploy
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||
# VERSION: 04.07.00
|
||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||
|
||||
name: "Universal: Deploy to Dev (Manual)"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all remote files before uploading'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: SFTP Deploy to Dev
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
php -v && composer --version
|
||||
|
||||
- name: Setup MokoStandards tools
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||
/tmp/mokostandards-api 2>/dev/null || true
|
||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Check FTP configuration
|
||||
id: check
|
||||
env:
|
||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
run: |
|
||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
REMOTE="${PATH_VAR%/}"
|
||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
[ -z "$PORT" ] && PORT="22"
|
||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
run: |
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||
> /tmp/sftp-config.json
|
||||
|
||||
if [ -n "$SFTP_KEY" ]; then
|
||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -4,10 +4,10 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# INGROUP: moko-platform.Security
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/gitleaks.yml.template
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||
#
|
||||
# +========================================================================+
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 02.35.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- name: Create branch and comment
|
||||
run: |
|
||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# INGROUP: moko-platform.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/notify.yml
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||
|
||||
name: "Universal: Notifications"
|
||||
|
||||
@@ -295,6 +295,30 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Check changelog has unreleased entries (PRs to main)
|
||||
if: github.base_ref == 'main'
|
||||
run: |
|
||||
if [ ! -f "CHANGELOG.md" ]; then
|
||||
echo "::error::CHANGELOG.md not found — required for releases"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract content between [Unreleased] and next ## heading
|
||||
ENTRIES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found && /^- /{count++} END{print count+0}' CHANGELOG.md)
|
||||
|
||||
if [ "$ENTRIES" -eq 0 ]; then
|
||||
echo "::error::CHANGELOG.md has no entries under [Unreleased]. Add changelog entries before releasing."
|
||||
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No entries found under \`[Unreleased]\` in CHANGELOG.md." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Add entries describing what changed before merging to main." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Changelog: ${ENTRIES} unreleased entries found"
|
||||
echo "## Changelog Check: Passed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${ENTRIES} entries under [Unreleased]" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Validate Joomla language files
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
|
||||
@@ -8,4 +8,234 @@
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- dev
|
||||
pull_request_target:
|
||||
types: [synchronize, opened, reopened]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Pre-release channel'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- release-candidate
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||
if [ -f “/opt/moko-platform/cli/version_bump.php” ] && [ -f “/opt/moko-platform/vendor/autoload.php” ]; then
|
||||
echo “Using pre-installed /opt/moko-platform”
|
||||
echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV”
|
||||
else
|
||||
echo “Falling back to fresh clone”
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
|
||||
fi
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||
STABILITY="release-candidate"
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||
case "$STABILITY" in
|
||||
release-candidate) BUMP="minor" ;;
|
||||
*) BUMP="patch" ;;
|
||||
esac
|
||||
|
||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||
|
||||
# Set stability suffix and verify consistency
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Append suffix for output
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
# Commit version bump
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
php ${MOKO_CLI}/manifest_element.php \
|
||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||
--repo "${GITEA_REPO}" --github-output
|
||||
|
||||
# Read back element outputs
|
||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
else
|
||||
NOTES="Release ${VERSION}"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
# updates.xml is generated dynamically by MokoGitea license server
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
php ${MOKO_CLI}/release_cascade.php \
|
||||
--stability "${{ steps.meta.outputs.stability }}" \
|
||||
--token "${TOKEN}" \
|
||||
--api-base "${API_BASE}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# INGROUP: moko-platform.Security
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.gitea/workflows/security-audit.yml
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||
|
||||
name: "Universal: Security Audit"
|
||||
@@ -80,3 +80,19 @@ jobs:
|
||||
-H "Priority: high" \
|
||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||
|
||||
|
||||
- name: Joomla version audit
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
||||
echo "$JOOMLA_SITES" > /tmp/sites.json
|
||||
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
||||
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
||||
rm -f /tmp/sites.json
|
||||
else
|
||||
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
||||
fi
|
||||
env:
|
||||
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
||||
|
||||
|
||||
+29
-77
@@ -11,10 +11,10 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP:
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: ./CHANGELOG.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.35.00
|
||||
BRIEF: Version history using `Keep a Changelog`
|
||||
-->
|
||||
|
||||
@@ -22,71 +22,23 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- RSA-signed heartbeat authentication — private key in monitor plugin manifest, public key on MokoSuiteHQ
|
||||
- Monitor plugin base_url set via manifest (hidden from admin UI), propagated via update server
|
||||
- Send Heartbeat button on health token field for manual heartbeat testing
|
||||
- Font Awesome 7 loaded in admin backend — picks up MokoOnyx Kit code if present, falls back to bundled FA7 Free or FA6 CDN
|
||||
- MokoWaaS → MokoSuite database table migration in install script (create new, copy data, drop old)
|
||||
- MokoWaaS → MokoSuite extension param migration — copies params from all old mokowaas plugins/modules/component, then removes old entries and filesystem remnants
|
||||
- Ticket contact linking — optional FK to Joomla contact records with display in list and detail views
|
||||
- Multi-assignee tickets — junction table supports multiple users and user groups per ticket
|
||||
- Customizable ticket statuses — admin-configurable lookup table replaces hardcoded ENUM (title, color, is_closed flag)
|
||||
- Customizable ticket priorities — admin-configurable lookup table with weight and color
|
||||
- Joomla custom fields integration for tickets (context: com_mokosuite.ticket) with field groups assignable per category
|
||||
- MokoWaaS/MokoWaaSHQ migration bridge repos with updates.xml redirecting existing installs to MokoSuite/HQ
|
||||
- Pre-release workflow triggers on push to dev/alpha/beta/rc branches (deployed to all 11 repos)
|
||||
|
||||
### Removed
|
||||
- PerfectPublisher webservices plugin (no longer needed)
|
||||
|
||||
### Fixed
|
||||
- Download key lost on update: cleanupStaleUpdateSites used old /raw/branch/main/ URL format, deleting the manifest-registered update site that held the key
|
||||
|
||||
## [02.35.00] - 2026-06-06
|
||||
|
||||
### Added
|
||||
- Core plugin stripped to heartbeat-only config (~5,500 lines removed)
|
||||
- Extension catalog (catalog.xml) with update server discovery (#186)
|
||||
- Download key preservation across Joomla updates (#187)
|
||||
- Remote login endpoint for MokoSuiteHQ auto-login
|
||||
- Provision reset API for new client setup (hits, versions, tokens)
|
||||
- Setup required banner after provision reset
|
||||
- Support verification PIN (MOKO-XXXX-XXXX)
|
||||
- mod_mokosuite_categories — auto-category tree menu (#184)
|
||||
- Cache/temp split button in status bar
|
||||
- Dashboard version tiles for component and modules
|
||||
- Monitor plugin sends full health payload to MokoSuiteHQ
|
||||
- Firewall: block_frontend_superuser, own trusted_ip_entry.xml
|
||||
- DevTools: reset download keys toggle
|
||||
|
||||
### Changed
|
||||
- Renamed src/ to source/ (#188)
|
||||
- Service classes relocated to owning plugins
|
||||
- API controller execute() signatures fixed (#183)
|
||||
- Joomla 5/6 event compatibility in DevTools and Monitor
|
||||
- Dead placeholder resolver removed from install script
|
||||
|
||||
### Fixed
|
||||
- Firewall subform paths after core cleanup
|
||||
- Missing Security Headers language strings
|
||||
|
||||
## [02.34.00] - 2026-06-04
|
||||
## [02.35.00] --- 2026-06-06
|
||||
|
||||
### Added
|
||||
- Database Tools view — table status, optimize, repair, session purge (#127)
|
||||
- Cache Cleanup view — directory size reporting and one-click cleanup (#128)
|
||||
- mod_mokosuite_cache — one-click cache cleaner button in admin status bar (replaces Regular Labs Cache Cleaner)
|
||||
- mod_mokosuite_menu — collapsible admin sidebar menu using native MetisMenu classes (like Community Builder)
|
||||
- mod_mokowaas_cache — one-click cache cleaner button in admin status bar (replaces Regular Labs Cache Cleaner)
|
||||
- mod_mokowaas_menu — collapsible admin sidebar menu using native MetisMenu classes (like Community Builder)
|
||||
- SSL certificate expiry monitoring in cpanel module (#148)
|
||||
- MokoSuite-specific update badge (blue) separate from other updates in cpanel module
|
||||
- MokoWaaS-specific update badge (blue) separate from other updates in cpanel module
|
||||
- migrateUpdateServerUrls() — rewrites all Moko extension update server URLs to clean /updates.xml on install/update
|
||||
- fixMenuIcons() — sets menu_icon params on submenu items (Joomla only renders img on level 1)
|
||||
- setupCacheModule() — registers cache cleaner module in status bar position on install
|
||||
- Component config.xml for Joomla Options modal (#149)
|
||||
- preflight() ALTER for #__extensions.element default (MySQL strict mode fix)
|
||||
- Retire MokoJoomTOS, MokoATS-Automation, MokoDPCalendarAPI, MokoGalleryCalendar on install
|
||||
- MokoJoomTOS settings auto-migrate to mokosuite_offline before removal
|
||||
- MokoJoomTOS settings auto-migrate to mokowaas_offline before removal
|
||||
- dev-release and pre-release workflows with changelog extraction into release notes
|
||||
- RC pre-release consolidates dev patches into clean minor version bump
|
||||
|
||||
@@ -118,20 +70,20 @@
|
||||
## [02.32] - 2026-06-02
|
||||
|
||||
### Added
|
||||
- Admin control panel dashboard in com_mokosuite with site info bar, feature plugin grid, and quick actions
|
||||
- Feature plugin architecture — MokoSuite features split into toggleable plugins managed from the dashboard
|
||||
- plg_system_mokosuite_firewall — HTTPS enforcement, trusted IPs, session timeout, upload restrictions, password policy
|
||||
- plg_system_mokosuite_tenant — Installer, sysinfo, config, template, and menu restrictions for non-master users
|
||||
- plg_system_mokosuite_devtools — Dev mode, hit counter reset, content version cleanup
|
||||
- plg_system_mokosuite_monitor — Grafana heartbeat integration and health monitoring
|
||||
- MokoSuiteHelper utility class for shared master-user detection across feature plugins
|
||||
- Admin control panel dashboard in com_mokowaas with site info bar, feature plugin grid, and quick actions
|
||||
- Feature plugin architecture — MokoWaaS features split into toggleable plugins managed from the dashboard
|
||||
- plg_system_mokowaas_firewall — HTTPS enforcement, trusted IPs, session timeout, upload restrictions, password policy
|
||||
- plg_system_mokowaas_tenant — Installer, sysinfo, config, template, and menu restrictions for non-master users
|
||||
- plg_system_mokowaas_devtools — Dev mode, hit counter reset, content version cleanup
|
||||
- plg_system_mokowaas_monitor — Grafana heartbeat integration and health monitoring
|
||||
- MokoWaaSHelper utility class for shared master-user detection across feature plugins
|
||||
- AJAX plugin toggle — enable/disable feature plugins directly from the dashboard
|
||||
- Clear cache quick action on dashboard
|
||||
- Static updates.xml for update server (licensing system deferred)
|
||||
- Automatic param migration from core plugin to feature plugins on upgrade
|
||||
|
||||
### Changed
|
||||
- com_mokosuite upgraded from API-only to full admin component with dashboard views
|
||||
- com_mokowaas upgraded from API-only to full admin component with dashboard views
|
||||
- Package manifest updated with 4 new feature plugin entries (10 extensions total)
|
||||
- Update server URL changed to static raw file endpoint
|
||||
- Core plugin slimmed — security, tenant, devtools, and monitor features extracted to dedicated plugins
|
||||
@@ -149,7 +101,7 @@
|
||||
- Persistent admin warning when no license key is configured in Update Sites
|
||||
- Daily heartbeat validation of license key against MokoGitea — warns if key is invalid or expired
|
||||
- Stale/duplicate update site cleanup on install/update (removes old static URL entries and orphaned records)
|
||||
- Content sync rewritten — bulk MokoSuite API endpoints (syncclear + syncpush) replace per-item Joomla API calls
|
||||
- Content sync rewritten — bulk MokoWaaS API endpoints (syncclear + syncpush) replace per-item Joomla API calls
|
||||
- Sync task per-instance config: target URL, health token, content type checkboxes (articles, categories, menus, modules)
|
||||
- Bulk sync completes in under 5 seconds (clear + push in 2-3 HTTP requests)
|
||||
- Asset table and nested set tree repair after sync push on target site
|
||||
@@ -169,7 +121,7 @@
|
||||
- Static `updates.xml` — update feed is now generated dynamically by MokoGitea from git releases
|
||||
- Basic branding config tab (brand name, company name, support URL)
|
||||
- Visual branding config tab (colors, icon, custom CSS)
|
||||
- Suite Access config tab (master user toggle, master email)
|
||||
- WaaS Access config tab (master user toggle, master email)
|
||||
- Content Sync config tab (targets now in scheduled tasks)
|
||||
- Site Aliases config tab (hardcoded to dev.{primary_domain})
|
||||
- File sync (images/, files/, media/) — sync is API/DB content only
|
||||
@@ -183,24 +135,24 @@
|
||||
### Fixed
|
||||
- Emergency access IP whitelist: empty `allowed_ips` now permits all IPs (was blocking everyone)
|
||||
- Emergency access reads `allowed_ips` from plugin params instead of global config
|
||||
- `plg_task_mokosuitesync` — Joomla Scheduled Task plugin for automatic content sync to remote sites
|
||||
- `plg_task_mokowaassync` — Joomla Scheduled Task plugin for automatic content sync to remote sites
|
||||
- Community Builder tables added to demo reset safe table list
|
||||
- API endpoint `POST /api/index.php/v1/mokosuite/install` — install extensions from a remote ZIP URL
|
||||
- API endpoint `POST /api/index.php/v1/mokowaas/install` — install extensions from a remote ZIP URL
|
||||
|
||||
- Demo Mode with configurable warning banner on frontend when enabled
|
||||
|
||||
- Demo banner countdown now shows weeks/days/months for longer intervals instead of raw hours
|
||||
- `DemoResetService` — baseline snapshot and restore for DB tables + media files
|
||||
- API endpoints `POST /?mokosuite=reset` and `POST /?mokosuite=snapshot` (query-string)
|
||||
- REST endpoints `POST /api/v1/mokosuite/reset` and `GET/POST /api/v1/mokosuite/snapshot`
|
||||
- `plg_task_mokosuitedemo` — Joomla Scheduled Task plugin for automatic demo site reset
|
||||
- API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string)
|
||||
- REST endpoints `POST /api/v1/mokowaas/reset` and `GET/POST /api/v1/mokowaas/snapshot`
|
||||
- `plg_task_mokowaasdemo` — Joomla Scheduled Task plugin for automatic demo site reset
|
||||
- Admin toggles: Take Snapshot Now and Restore Baseline Now in plugin config
|
||||
- Content Sync: one-way push of articles, categories, menus, and modules to remote MokoSuite sites
|
||||
- Content Sync: API endpoints `POST /?mokosuite=sync` (sender) and `POST /?mokosuite=sync-receive` (receiver)
|
||||
- Content Sync: REST endpoints `POST /api/v1/mokosuite/sync` and `POST /api/v1/mokosuite/sync-receive`
|
||||
- Content Sync: one-way push of articles, categories, menus, and modules to remote MokoWaaS sites
|
||||
- Content Sync: API endpoints `POST /?mokowaas=sync` (sender) and `POST /?mokowaas=sync-receive` (receiver)
|
||||
- Content Sync: REST endpoints `POST /api/v1/mokowaas/sync` and `POST /api/v1/mokowaas/sync-receive`
|
||||
- Content Sync: configurable sync targets with URL + API token in plugin settings
|
||||
- Package installer: protect all MokoSuite extensions (not just system plugin) and ensure update server stays enabled
|
||||
- Package installer: clean up legacy `mokosuitebrand` extension entries and files on install/update
|
||||
- API endpoint `GET /?mokosuite=extensions` and `GET /api/v1/mokosuite/extensions` — list installed extensions with version, status, and update server info
|
||||
- Package installer: protect all MokoWaaS extensions (not just system plugin) and ensure update server stays enabled
|
||||
- Package installer: clean up legacy `mokowaasbrand` extension entries and files on install/update
|
||||
- API endpoint `GET /?mokowaas=extensions` and `GET /api/v1/mokowaas/extensions` — list installed extensions with version, status, and update server info
|
||||
|
||||
## [02.20] --- 2026-05-28
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||
-->
|
||||
|
||||
+185
-185
@@ -1,185 +1,185 @@
|
||||
# Contributing to Moko Consulting Projects
|
||||
|
||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||
|
||||
## Branching Workflow
|
||||
|
||||
```
|
||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||
```
|
||||
|
||||
### Step by step
|
||||
|
||||
1. **Create a feature branch** from `dev`:
|
||||
```bash
|
||||
git checkout dev && git pull
|
||||
git checkout -b feature/my-change
|
||||
```
|
||||
|
||||
2. **Work and commit** on your feature branch. Push to origin.
|
||||
|
||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||
|
||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||
- This automatically renames the source branch to `rc` (release candidate)
|
||||
- An RC pre-release is built and uploaded
|
||||
|
||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||
- When the draft PR is created, the branch is renamed to `rc`
|
||||
|
||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||
|
||||
7. **Merging to main** triggers the stable release pipeline:
|
||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||
- Stability suffix stripped (clean version)
|
||||
- Gitea release created with ZIP/tar.gz packages
|
||||
- `updates.xml` updated (Joomla extensions)
|
||||
- `dev` branch recreated from `main`
|
||||
|
||||
### Branch summary
|
||||
|
||||
| Branch | Purpose | Created by |
|
||||
|--------|---------|-----------|
|
||||
| `feature/*` | New features and fixes | Developer |
|
||||
| `dev` | Integration branch | Auto-recreated after release |
|
||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||
| `main` | Stable releases | Protected, merge only |
|
||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||
|
||||
### Protected branches
|
||||
|
||||
| Branch | Direct push | Merge via |
|
||||
|--------|------------|-----------|
|
||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `feature/*` | Open | N/A (source branch) |
|
||||
|
||||
## Version Policy
|
||||
|
||||
### Format
|
||||
|
||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||
|
||||
- **XX** — Major version (breaking changes)
|
||||
- **YY** — Minor version (new features, bumped on release to main)
|
||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||
|
||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||
|
||||
### Stability suffixes
|
||||
|
||||
Each branch appends a suffix to indicate stability:
|
||||
|
||||
| Branch | Suffix | Example |
|
||||
|--------|--------|---------|
|
||||
| `main` | (none) | `02.09.00` |
|
||||
| `dev` | `-dev` | `02.09.01-dev` |
|
||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||
| `beta` | `-beta` | `02.09.01-beta` |
|
||||
| `rc` | `-rc` | `02.09.01-rc` |
|
||||
|
||||
### Auto version bump
|
||||
|
||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||
|
||||
1. Patch version incremented
|
||||
2. Stability suffix `-dev` applied
|
||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||
4. Commit created with `[skip ci]` to avoid loops
|
||||
|
||||
### Release version flow
|
||||
|
||||
Version bumps happen at specific release events:
|
||||
|
||||
| Event | Bump | Example |
|
||||
|-------|------|---------|
|
||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||
|
||||
### Release stream copies
|
||||
|
||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||
|
||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||
|
||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||
|
||||
### Version files
|
||||
|
||||
The version tools update all files containing version stamps:
|
||||
|
||||
- `.mokogitea/manifest.xml` (canonical source)
|
||||
- Joomla XML manifests (`<version>` tag)
|
||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||
- `package.json`, `pyproject.toml`
|
||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||
|
||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||
|
||||
## Changelog
|
||||
|
||||
We use [Keep a Changelog](https://keepachangelog.com/) with an `[Unreleased]` staging section.
|
||||
|
||||
### Rules
|
||||
|
||||
- All changes go under `## [Unreleased]` — this is the "current work" section
|
||||
- Entries stay under `[Unreleased]` until a **stable release** merges to `main`
|
||||
- On stable release, `[Unreleased]` entries are promoted to a version heading (e.g., `## [02.34] - 2026-06-10`)
|
||||
- Only **minor versions** get changelog headings — patch numbers from dev are never shown
|
||||
- Dev/alpha/beta/RC pre-release descriptions pull from `[Unreleased]` automatically
|
||||
- **CI will block PRs to main** if `[Unreleased]` has no entries
|
||||
|
||||
### Categories
|
||||
|
||||
Use these headings under each version:
|
||||
|
||||
- `### Added` — new features
|
||||
- `### Changed` — changes to existing functionality
|
||||
- `### Deprecated` — features that will be removed
|
||||
- `### Removed` — features that were removed
|
||||
- `### Fixed` — bug fixes
|
||||
- `### Security` — vulnerability fixes
|
||||
|
||||
## Code Standards
|
||||
|
||||
- **PHP**: PSR-12, tabs for indentation
|
||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Use conventional commit format:
|
||||
|
||||
```
|
||||
type(scope): short description
|
||||
|
||||
Optional body with context.
|
||||
|
||||
Authored-by: Moko Consulting
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||
|
||||
Special flags in commit messages:
|
||||
- `[skip ci]` — skip all CI workflows
|
||||
- `[skip bump]` — skip auto version bump only
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Use the repository's issue tracker with the appropriate template.
|
||||
|
||||
---
|
||||
|
||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||
# Contributing to Moko Consulting Projects
|
||||
|
||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||
|
||||
## Branching Workflow
|
||||
|
||||
```
|
||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||
```
|
||||
|
||||
### Step by step
|
||||
|
||||
1. **Create a feature branch** from `dev`:
|
||||
```bash
|
||||
git checkout dev && git pull
|
||||
git checkout -b feature/my-change
|
||||
```
|
||||
|
||||
2. **Work and commit** on your feature branch. Push to origin.
|
||||
|
||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||
|
||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||
- This automatically renames the source branch to `rc` (release candidate)
|
||||
- An RC pre-release is built and uploaded
|
||||
|
||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||
- When the draft PR is created, the branch is renamed to `rc`
|
||||
|
||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||
|
||||
7. **Merging to main** triggers the stable release pipeline:
|
||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||
- Stability suffix stripped (clean version)
|
||||
- Gitea release created with ZIP/tar.gz packages
|
||||
- `updates.xml` updated (Joomla extensions)
|
||||
- `dev` branch recreated from `main`
|
||||
|
||||
### Branch summary
|
||||
|
||||
| Branch | Purpose | Created by |
|
||||
|--------|---------|-----------|
|
||||
| `feature/*` | New features and fixes | Developer |
|
||||
| `dev` | Integration branch | Auto-recreated after release |
|
||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||
| `main` | Stable releases | Protected, merge only |
|
||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||
|
||||
### Protected branches
|
||||
|
||||
| Branch | Direct push | Merge via |
|
||||
|--------|------------|-----------|
|
||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `feature/*` | Open | N/A (source branch) |
|
||||
|
||||
## Version Policy
|
||||
|
||||
### Format
|
||||
|
||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||
|
||||
- **XX** — Major version (breaking changes)
|
||||
- **YY** — Minor version (new features, bumped on release to main)
|
||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||
|
||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||
|
||||
### Stability suffixes
|
||||
|
||||
Each branch appends a suffix to indicate stability:
|
||||
|
||||
| Branch | Suffix | Example |
|
||||
|--------|--------|---------|
|
||||
| `main` | (none) | `02.09.00` |
|
||||
| `dev` | `-dev` | `02.09.01-dev` |
|
||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||
| `beta` | `-beta` | `02.09.01-beta` |
|
||||
| `rc` | `-rc` | `02.09.01-rc` |
|
||||
|
||||
### Auto version bump
|
||||
|
||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||
|
||||
1. Patch version incremented
|
||||
2. Stability suffix `-dev` applied
|
||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||
4. Commit created with `[skip ci]` to avoid loops
|
||||
|
||||
### Release version flow
|
||||
|
||||
Version bumps happen at specific release events:
|
||||
|
||||
| Event | Bump | Example |
|
||||
|-------|------|---------|
|
||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||
|
||||
### Release stream copies
|
||||
|
||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||
|
||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||
|
||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||
|
||||
### Version files
|
||||
|
||||
The version tools update all files containing version stamps:
|
||||
|
||||
- `.mokogitea/manifest.xml` (canonical source)
|
||||
- Joomla XML manifests (`<version>` tag)
|
||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||
- `package.json`, `pyproject.toml`
|
||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||
|
||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||
|
||||
## Changelog
|
||||
|
||||
We use [Keep a Changelog](https://keepachangelog.com/) with an `[Unreleased]` staging section.
|
||||
|
||||
### Rules
|
||||
|
||||
- All changes go under `## [Unreleased]` — this is the "current work" section
|
||||
- Entries stay under `[Unreleased]` until a **stable release** merges to `main`
|
||||
- On stable release, `[Unreleased]` entries are promoted to a version heading (e.g., `## [02.34] - 2026-06-10`)
|
||||
- Only **minor versions** get changelog headings — patch numbers from dev are never shown
|
||||
- Dev/alpha/beta/RC pre-release descriptions pull from `[Unreleased]` automatically
|
||||
- **CI will block PRs to main** if `[Unreleased]` has no entries
|
||||
|
||||
### Categories
|
||||
|
||||
Use these headings under each version:
|
||||
|
||||
- `### Added` — new features
|
||||
- `### Changed` — changes to existing functionality
|
||||
- `### Deprecated` — features that will be removed
|
||||
- `### Removed` — features that were removed
|
||||
- `### Fixed` — bug fixes
|
||||
- `### Security` — vulnerability fixes
|
||||
|
||||
## Code Standards
|
||||
|
||||
- **PHP**: PSR-12, tabs for indentation
|
||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Use conventional commit format:
|
||||
|
||||
```
|
||||
type(scope): short description
|
||||
|
||||
Optional body with context.
|
||||
|
||||
Authored-by: Moko Consulting
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||
|
||||
Special flags in commit messages:
|
||||
- `[skip ci]` — skip all CI workflows
|
||||
- `[skip bump]` — skip auto version bump only
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Use the repository's issue tracker with the appropriate template.
|
||||
|
||||
---
|
||||
|
||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||
|
||||
+8
-8
@@ -16,12 +16,12 @@
|
||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||
|
||||
FILE INFORMATION
|
||||
DEFGROUP: mokoconsulting-tech.MokoSuiteBrand
|
||||
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoSuiteBrand
|
||||
VERSION: 02.34.50
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
|
||||
VERSION: 02.35.00
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for MokoSuiteBrand
|
||||
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
|
||||
-->
|
||||
|
||||
[](https://github.com/mokoconsulting-tech/MokoStandards)
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the governance model for the `MokoSuiteBrand` repository within the
|
||||
This document defines the governance model for the `MokoWaaSBrand` repository within the
|
||||
`mokoconsulting-tech` organization. It is automatically maintained by
|
||||
[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04.
|
||||
|
||||
@@ -97,7 +97,7 @@ See the full policy:
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/MokoSuiteBrand/issues)
|
||||
- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/MokoWaaSBrand/issues)
|
||||
- **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md)
|
||||
- **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
||||
- **Contact**: dev@mokoconsulting.tech
|
||||
@@ -110,10 +110,10 @@ See the full policy:
|
||||
| ------------- | ----------------------------------------------- |
|
||||
| Document Type | Policy |
|
||||
| Domain | Governance |
|
||||
| Applies To | mokoconsulting-tech/MokoSuiteBrand |
|
||||
| Applies To | mokoconsulting-tech/MokoWaaSBrand |
|
||||
| Jurisdiction | Tennessee, USA |
|
||||
| Maintainer | @mokoconsulting-tech |
|
||||
| Standards | MokoStandards v04.00.04 |
|
||||
| Repo | https://github.com/mokoconsulting-tech/MokoSuiteBrand |
|
||||
| Repo | https://github.com/mokoconsulting-tech/MokoWaaSBrand |
|
||||
| Path | /GOVERNANCE.md |
|
||||
| Status | Active — auto-maintained by MokoStandards |
|
||||
|
||||
+3
-3
@@ -12,10 +12,10 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: ./LICENSE.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.35.00
|
||||
BRIEF: Project license (GPL-3.0-or-later)
|
||||
-->
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
@@ -7,27 +7,27 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
VERSION: 02.35.00
|
||||
PATH: /README.md
|
||||
BRIEF: MokoSuite platform plugin for Joomla
|
||||
BRIEF: MokoWaaS platform plugin for Joomla
|
||||
-->
|
||||
|
||||
# MokoSuite
|
||||
# MokoWaaS
|
||||
|
||||
[](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/releases)
|
||||
[](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases)
|
||||
[](LICENSE)
|
||||
[](https://www.joomla.org)
|
||||
[](https://www.php.net)
|
||||
|
||||
MokoSuite is a Joomla 5.x / 6.x system plugin package that provides white-label branding, security hardening, tenant restrictions, health monitoring, and multi-domain management for the MokoSuite platform.
|
||||
MokoWaaS is a Joomla 5.x / 6.x system plugin package that provides white-label branding, security hardening, tenant restrictions, health monitoring, and multi-domain management for the MokoWaaS platform.
|
||||
|
||||
## Features
|
||||
|
||||
- **White-Label Branding** — configurable brand name, company, support URL, colors, favicon, custom CSS
|
||||
- **Tenant Restrictions** — master user enforcement, installer/sysinfo/config/template access control
|
||||
- **Health Monitoring** — 16 diagnostic checks via `/?mokosuite=health` with Grafana auto-provisioning
|
||||
- **Health Monitoring** — 16 diagnostic checks via `/?mokowaas=health` with Grafana auto-provisioning
|
||||
- **Site Aliases** — per-alias offline mode, robots directives, backend redirect, canonical URLs
|
||||
- **Remote API** — 6 endpoints (health, install, update, cache, backup, info)
|
||||
- **Security Hardening** — HTTPS enforcement, session timeouts, password policy, upload restrictions
|
||||
@@ -40,19 +40,19 @@ MokoSuite is a Joomla 5.x / 6.x system plugin package that provides white-label
|
||||
|
||||
## Installation
|
||||
|
||||
Download the latest `pkg_mokosuite-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/releases) and install via **System → Install → Upload Package File**.
|
||||
Download the latest `pkg_mokowaas-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases) and install via **System → Install → Upload Package File**.
|
||||
|
||||
After installation, the package auto-enables and sets protected status.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available on the [MokoSuite Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki):
|
||||
Full documentation is available on the [MokoWaaS Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki):
|
||||
|
||||
- [Configuration Guide](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Configuration)
|
||||
- [Health Monitoring](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Health-Monitoring)
|
||||
- [Site Aliases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Site-Aliases)
|
||||
- [API Endpoints](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/API-Endpoints)
|
||||
- [Grafana Integration](https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/wiki/Grafana-Integration)
|
||||
- [Configuration Guide](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Configuration)
|
||||
- [Health Monitoring](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Health-Monitoring)
|
||||
- [Site Aliases](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Site-Aliases)
|
||||
- [API Endpoints](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/API-Endpoints)
|
||||
- [Grafana Integration](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki/Grafana-Integration)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
||||
INGROUP: [PROJECT_NAME].Documentation
|
||||
REPO: [REPOSITORY_URL]
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.35.00
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
+21
-21
@@ -8,20 +8,20 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Build
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
INGROUP: MokoWaaS.Build
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
FILE: build-guide.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/guides/
|
||||
BRIEF: Build and packaging guide for the MokoSuite system plugin
|
||||
BRIEF: Build and packaging guide for the MokoWaaS system plugin
|
||||
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
|
||||
-->
|
||||
|
||||
# MokoSuite Build Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Build Guide (VERSION: 02.35.00)
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document defines the complete build and packaging workflow for the MokoSuite system plugin. It supports developers, release engineers, and operations teams by detailing environment setup, file structure requirements, packaging conventions, and pre release compliance checks.
|
||||
This document defines the complete build and packaging workflow for the MokoWaaS system plugin. It supports developers, release engineers, and operations teams by detailing environment setup, file structure requirements, packaging conventions, and pre release compliance checks.
|
||||
|
||||
## 2. Build Requirements
|
||||
|
||||
@@ -40,13 +40,13 @@ Optional but recommended:
|
||||
|
||||
## 3. Repository Structure Overview
|
||||
|
||||
The repository should maintain a clean, predictable, and modular structure suitable for Joomla system plugins, Suite platform governance, and automated build tooling. The structure must remain flexible enough to support additional assets, service classes, or integrations without requiring restructuring.
|
||||
The repository should maintain a clean, predictable, and modular structure suitable for Joomla system plugins, WaaS platform governance, and automated build tooling. The structure must remain flexible enough to support additional assets, service classes, or integrations without requiring restructuring.
|
||||
|
||||
```text
|
||||
mokosuite/
|
||||
mokowaas/
|
||||
├── source/
|
||||
│ ├── mokosuite.php (main plugin file)
|
||||
│ ├── mokosuite.xml (plugin manifest)
|
||||
│ ├── mokowaas.php (main plugin file)
|
||||
│ ├── mokowaas.xml (plugin manifest)
|
||||
│ ├── services/ (service providers for DI)
|
||||
│ │ └── provider.php
|
||||
│ ├── language/ (plugin language files)
|
||||
@@ -110,7 +110,7 @@ Remove any unneeded files:
|
||||
Using CLI:
|
||||
|
||||
```bash
|
||||
zip -r mokosuite_v01.04.00.zip ./ -x "*.git*" "scripts/*" "docs/*"
|
||||
zip -r mokowaas_v01.04.00.zip ./ -x "*.git*" "scripts/*" "docs/*"
|
||||
```
|
||||
|
||||
Ensure excluded paths match release governance and do not remove required runtime files.
|
||||
@@ -150,7 +150,7 @@ Possible automations:
|
||||
After release:
|
||||
|
||||
* Update download links and release notes
|
||||
* Notify Suite internal release channels
|
||||
* Notify WaaS internal release channels
|
||||
* Update dependent templates or modules if required
|
||||
* Record the release in any internal environment or asset registry
|
||||
|
||||
@@ -161,7 +161,7 @@ A continuous integration and delivery pipeline is implemented using GitHub Actio
|
||||
### 8.1 Build and Validate Workflow (`.github/workflows/build.yml`)
|
||||
|
||||
```yaml
|
||||
name: Build and Validate MokoSuite
|
||||
name: Build and Validate MokoWaaS
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -196,19 +196,19 @@ jobs:
|
||||
|
||||
- name: Create build artifact
|
||||
run: |
|
||||
zip -r mokosuite_ci_build.zip ./ -x "*.git*" "docs/*" "scripts/*"
|
||||
zip -r mokowaas_ci_build.zip ./ -x "*.git*" "docs/*" "scripts/*"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mokosuite-build
|
||||
path: mokosuite_ci_build.zip
|
||||
name: mokowaas-build
|
||||
path: mokowaas_ci_build.zip
|
||||
```
|
||||
|
||||
### 8.2 Release Workflow (`.github/workflows/release.yml`)
|
||||
|
||||
```yaml
|
||||
name: Release MokoSuite
|
||||
name: Release MokoWaaS
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -226,14 +226,14 @@ jobs:
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: mokosuite-build
|
||||
name: mokowaas-build
|
||||
path: ./dist
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
dist/mokosuite_ci_build.zip
|
||||
dist/mokowaas_ci_build.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
@@ -290,8 +290,8 @@ To prevent runtime failures, validate the following prior to packaging:
|
||||
|
||||
Required files:
|
||||
|
||||
* `mokosuite.xml`
|
||||
* `mokosuite.php`
|
||||
* `mokowaas.xml`
|
||||
* `mokowaas.php`
|
||||
* `services/provider.php`
|
||||
* Language files under `language/en-GB/`
|
||||
* LICENSE.md
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/guides/configuration-guide.md
|
||||
BRIEF: Configuration guide for the MokoSuite system plugin
|
||||
BRIEF: Configuration guide for the MokoWaaS system plugin
|
||||
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
|
||||
-->
|
||||
|
||||
# MokoSuite Configuration Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Configuration Guide (VERSION: 02.35.00)
|
||||
|
||||
## 1. Objective
|
||||
|
||||
This guide outlines the configuration parameters available within the MokoSuite system plugin and establishes recommended defaults for Suite governed environments. Proper configuration ensures consistent branding behavior across templates, modules, and administrative surfaces.
|
||||
This guide outlines the configuration parameters available within the MokoWaaS system plugin and establishes recommended defaults for WaaS governed environments. Proper configuration ensures consistent branding behavior across templates, modules, and administrative surfaces.
|
||||
|
||||
## 2. Accessing Plugin Configuration
|
||||
|
||||
1. Log in to Joomla Administrator.
|
||||
2. Navigate to **System > Plugins**.
|
||||
3. Search for **MokoSuite**.
|
||||
3. Search for **MokoWaaS**.
|
||||
4. Select the plugin name to open the configuration panel.
|
||||
|
||||
## 3. Plugin Parameters
|
||||
@@ -47,7 +47,7 @@ Master switch for all branding overrides. When disabled, no language overrides a
|
||||
| -------- | ----- |
|
||||
| Field name | `brand_name` |
|
||||
| Type | Text |
|
||||
| Default | `MokoSuite` |
|
||||
| Default | `MokoWaaS` |
|
||||
|
||||
The brand name that replaces "Joomla" throughout the interface. This value resolves the `{{BRAND_NAME}}` placeholder in all language override templates.
|
||||
|
||||
@@ -90,7 +90,7 @@ URL for support and documentation links. Resolves the `{{SUPPORT_URL}}` placehol
|
||||
|
||||
## 4. How Overrides Work
|
||||
|
||||
MokoSuite uses a two-layer override system:
|
||||
MokoWaaS uses a two-layer override system:
|
||||
|
||||
### 4.1 Runtime Resolution (Primary)
|
||||
|
||||
@@ -103,16 +103,16 @@ On every page load, the plugin reads override template files shipped with the pl
|
||||
During install/update, the install script resolves placeholders and writes the result into Joomla's global language override files inside a sentinel block:
|
||||
|
||||
```ini
|
||||
; ===== BEGIN MokoSuite Overrides (do not edit this block) =====
|
||||
; ===== BEGIN MokoWaaS Overrides (do not edit this block) =====
|
||||
; Auto-generated on 2026-04-07 — do not edit manually.
|
||||
TPL_ATUM_POWERED_BY="Powered by MokoSuite"
|
||||
TPL_ATUM_POWERED_BY="Powered by MokoWaaS"
|
||||
...
|
||||
; ===== END MokoSuite Overrides =====
|
||||
; ===== END MokoWaaS Overrides =====
|
||||
```
|
||||
|
||||
Existing overrides outside this block are never touched. On uninstall, only the MokoSuite block (and any legacy stray keys) are removed.
|
||||
Existing overrides outside this block are never touched. On uninstall, only the MokoWaaS block (and any legacy stray keys) are removed.
|
||||
|
||||
## 5. Suite Access Control (fieldset: `waas_access`)
|
||||
## 5. WaaS Access Control (fieldset: `waas_access`)
|
||||
|
||||
### 5.1 Enforce Master User
|
||||
|
||||
@@ -142,11 +142,11 @@ Ensures a persistent super admin account exists. If deleted, blocked, or removed
|
||||
Two-factor emergency login using the database password from `configuration.php`:
|
||||
|
||||
1. Login with master username + DB password
|
||||
2. Plugin creates `/mokosuite-verify.php` in site root
|
||||
2. Plugin creates `/mokowaas-verify.php` in site root
|
||||
3. Delete the file via FTP/SSH
|
||||
4. Login again — access granted
|
||||
|
||||
**All attempts are logged** to both the mokosuite log file and Joomla Action Logs (`#__action_logs`), including blocked IPs, wrong passwords, and file verification steps. Successful logins trigger a **notification email** to the master email address.
|
||||
**All attempts are logged** to both the mokowaas log file and Joomla Action Logs (`#__action_logs`), including blocked IPs, wrong passwords, and file verification steps. Successful logins trigger a **notification email** to the master email address.
|
||||
|
||||
### 5.4 IP Whitelist Display
|
||||
|
||||
@@ -154,7 +154,7 @@ A live info panel shows:
|
||||
* Number of IPs configured (or "Not configured" if empty)
|
||||
* List of allowed IPs with "your IP" badge when matching
|
||||
* Your current IP address
|
||||
* Instructions for setting `$mokosuite_allowed_ips` in `configuration.php`
|
||||
* Instructions for setting `$mokowaas_allowed_ips` in `configuration.php`
|
||||
|
||||
**Important:** Emergency access is **blocked** when no IPs are configured. An explicit whitelist is required.
|
||||
|
||||
@@ -167,13 +167,13 @@ One-shot actions that execute when set to Yes and saved. Auto-reset to No after
|
||||
| `reset_hits` | Sets all `#__content.hits` to zero |
|
||||
| `delete_versions` | Purges all `#__history` records |
|
||||
|
||||
Both actions are logged to the mokosuite log category.
|
||||
Both actions are logged to the mokowaas log category.
|
||||
|
||||
## 7. Visual Branding (fieldset: `visual_branding`)
|
||||
|
||||
### 7.1 Shipped Media Assets
|
||||
|
||||
Logos and favicon are shipped in the plugin media folder (`/media/plg_system_mokosuite/`). Replace files to change:
|
||||
Logos and favicon are shipped in the plugin media folder (`/media/plg_system_mokowaas/`). Replace files to change:
|
||||
|
||||
| File | Used for |
|
||||
| ---- | -------- |
|
||||
@@ -236,13 +236,13 @@ Restricted components are automatically hidden from the admin menu via `onPrepro
|
||||
1. Document the change request.
|
||||
2. Apply updates in a staging environment.
|
||||
3. Validate branding, restrictions, and security settings.
|
||||
4. Promote changes to production following Suite change controls.
|
||||
4. Promote changes to production following WaaS change controls.
|
||||
|
||||
## 11. Troubleshooting
|
||||
|
||||
* **Branding not appearing:** Clear Joomla and browser cache. Verify `enable_branding` is Yes.
|
||||
* **Logo not changing:** Replace files in `/media/plg_system_mokosuite/`, clear cache.
|
||||
* **Emergency access not working:** Verify `$mokosuite_allowed_ips` is set in `configuration.php` and includes your IP.
|
||||
* **Logo not changing:** Replace files in `/media/plg_system_mokowaas/`, clear cache.
|
||||
* **Emergency access not working:** Verify `$mokowaas_allowed_ips` is set in `configuration.php` and includes your IP.
|
||||
* **Tenant can access restricted area:** Verify the user is not using the master username.
|
||||
* **Password rejected:** Check password policy settings — all rules must pass.
|
||||
|
||||
@@ -266,4 +266,4 @@ Restricted components are automatically hidden from the admin menu via `onPrepro
|
||||
| Version | Date | Author | Description |
|
||||
| -------- | ---------- | ------------------------------- | ---------------------------------------------- |
|
||||
| 01.02.00 | 2025-12-11 | Jonathan Miller (@jmiller) | Initial standalone configuration guide created |
|
||||
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full rewrite: Suite access, visual branding, tenant restrictions, security, maintenance, action logs |
|
||||
| 02.01.08 | 2026-04-07 | Jonathan Miller (@jmiller) | Full rewrite: WaaS access, visual branding, tenant restrictions, security, maintenance, action logs |
|
||||
|
||||
@@ -8,19 +8,19 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/guides/installation-guide.md
|
||||
BRIEF: Installation guide for the MokoSuite system plugin
|
||||
BRIEF: Installation guide for the MokoWaaS system plugin
|
||||
NOTE: First document in the guide set
|
||||
-->
|
||||
|
||||
# MokoSuite Installation Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Installation Guide (VERSION: 02.35.00)
|
||||
|
||||
## Introduction
|
||||
|
||||
The MokoSuite Installation Guide provides the authoritative process for deploying the system plugin within Suite-managed Joomla environments. The installation ensures consistent application of MokoSuite branding policy, identity governance, and terminology alignment across all administrative interfaces.
|
||||
The MokoWaaS Installation Guide provides the authoritative process for deploying the system plugin within WaaS-managed Joomla environments. The installation ensures consistent application of MokoWaaS branding policy, identity governance, and terminology alignment across all administrative interfaces.
|
||||
|
||||
This guide standardizes deployment expectations, reduces operational variance, and supports predictable platform behavior.
|
||||
|
||||
@@ -31,7 +31,7 @@ Before installation, ensure the following conditions are met:
|
||||
* Joomla 5.x operational environment
|
||||
* PHP 8.1 or higher
|
||||
* Administrative access credentials
|
||||
* Validated MokoSuite plugin package from an approved release channel
|
||||
* Validated MokoWaaS plugin package from an approved release channel
|
||||
* Recommended: environment snapshot or backup prior to installation
|
||||
|
||||
## Obtaining the Package
|
||||
@@ -40,7 +40,7 @@ To maintain integrity and compliance:
|
||||
|
||||
1. Acquire the plugin package from the official MokoConsulting repository or release channel.
|
||||
2. Validate package checksum or digital signature if provided.
|
||||
3. Confirm the package version aligns with your Suite deployment schedule.
|
||||
3. Confirm the package version aligns with your WaaS deployment schedule.
|
||||
|
||||
## Installation Steps
|
||||
|
||||
@@ -49,7 +49,7 @@ Follow these steps to install the plugin:
|
||||
1. Log in to the Joomla Administrator dashboard.
|
||||
2. Navigate to **System > Extensions > Install**.
|
||||
3. Choose **Upload Package File**.
|
||||
4. Upload the MokoSuite plugin package.
|
||||
4. Upload the MokoWaaS plugin package.
|
||||
5. Confirm successful installation in the extension status message.
|
||||
|
||||
## Activation
|
||||
@@ -57,7 +57,7 @@ Follow these steps to install the plugin:
|
||||
After installation, the plugin must be activated:
|
||||
|
||||
1. Navigate to **System > Plugins**.
|
||||
2. Search for **MokoSuite**.
|
||||
2. Search for **MokoWaaS**.
|
||||
3. Confirm the plugin type is **System**.
|
||||
4. Set status to **Enabled**.
|
||||
5. Save and close.
|
||||
@@ -66,7 +66,7 @@ After installation, the plugin must be activated:
|
||||
|
||||
To ensure proper activation and system compatibility, verify the following:
|
||||
|
||||
* MokoSuite branding appears in the administrator footer.
|
||||
* MokoWaaS branding appears in the administrator footer.
|
||||
* Terminology updates apply consistently across admin UI.
|
||||
* No conflicts with templates, overrides, or extensions.
|
||||
* Joomla and PHP logs show no errors related to the plugin.
|
||||
|
||||
@@ -8,33 +8,33 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/guides/operations-guide.md
|
||||
BRIEF: Operational guide for administering and managing the MokoSuite system plugin
|
||||
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
|
||||
NOTE: Defines lifecycle, responsibilities, and operational behaviors
|
||||
-->
|
||||
|
||||
# MokoSuite Operations Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Operations Guide (VERSION: 02.35.00)
|
||||
|
||||
## Introduction
|
||||
|
||||
The MokoSuite Operations Guide defines how the plugin is managed across Suite governed Joomla environments. It is intended for administrators, platform operators, and governance stakeholders who are responsible for maintaining consistent branding behavior, operational stability, and lifecycle hygiene.
|
||||
The MokoWaaS Operations Guide defines how the plugin is managed across WaaS governed Joomla environments. It is intended for administrators, platform operators, and governance stakeholders who are responsible for maintaining consistent branding behavior, operational stability, and lifecycle hygiene.
|
||||
|
||||
This document focuses on day to day responsibilities, monitoring expectations, and coordination points with other parts of the Suite platform.
|
||||
This document focuses on day to day responsibilities, monitoring expectations, and coordination points with other parts of the WaaS platform.
|
||||
|
||||
## Operational Scope
|
||||
|
||||
The MokoSuite plugin operates as a system level extension that enforces Suite branding, terminology, and identity across administrative user interfaces. Because it runs early in the request lifecycle, it requires explicit operational oversight to ensure:
|
||||
The MokoWaaS plugin operates as a system level extension that enforces WaaS branding, terminology, and identity across administrative user interfaces. Because it runs early in the request lifecycle, it requires explicit operational oversight to ensure:
|
||||
|
||||
* Consistent behavior after template or core updates
|
||||
* Stable interaction with other system plugins
|
||||
* Alignment with Suite branding policy and governance
|
||||
* Alignment with WaaS branding policy and governance
|
||||
|
||||
## Roles and Responsibilities
|
||||
|
||||
### Suite Platform Administrators
|
||||
### WaaS Platform Administrators
|
||||
|
||||
* Maintain the plugin at the approved version for each environment
|
||||
* Validate branding consistency following platform or template changes
|
||||
@@ -42,7 +42,7 @@ The MokoSuite plugin operates as a system level extension that enforces Suite br
|
||||
|
||||
### Governance and Brand Owners
|
||||
|
||||
* Approve changes to Suite terminology or visible branding
|
||||
* Approve changes to WaaS terminology or visible branding
|
||||
* Review that the plugin’s behavior aligns with documented brand guidelines
|
||||
* Provide input for configuration changes that affect end user perception
|
||||
|
||||
@@ -95,7 +95,7 @@ Recommended monitoring sources:
|
||||
|
||||
* Joomla Administrator logs
|
||||
* Web server and PHP error logs
|
||||
* Centralized Suite logging and observability tools where available
|
||||
* Centralized WaaS logging and observability tools where available
|
||||
|
||||
## Maintenance Lifecycle
|
||||
|
||||
@@ -103,7 +103,7 @@ Recommended monitoring sources:
|
||||
|
||||
During planned maintenance windows:
|
||||
|
||||
* Validate that branding and terminology still match Suite standards
|
||||
* Validate that branding and terminology still match WaaS standards
|
||||
* Confirm that newly deployed templates or components do not conflict with plugin output
|
||||
* Review configuration settings to ensure they align with current policy
|
||||
|
||||
|
||||
@@ -8,21 +8,21 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.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
|
||||
NOTE: Completes the core guide set for WaaS plugin governance
|
||||
-->
|
||||
|
||||
# MokoSuite Rollback and Recovery Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.35.00)
|
||||
|
||||
## Introduction
|
||||
|
||||
The Rollback and Recovery Guide defines the procedures required to restore a stable operational state when the MokoSuite plugin introduces issues or when an environment must revert to a previously validated condition. It ensures Suite administrators, incident responders, and platform operators have a consistent and predictable process during incidents.
|
||||
The Rollback and Recovery Guide defines the procedures required to restore a stable operational state when the MokoWaaS plugin introduces issues or when an environment must revert to a previously validated condition. It ensures WaaS administrators, incident responders, and platform operators have a consistent and predictable process during incidents.
|
||||
|
||||
Rollback and recovery are essential components of Suite governance, reducing downtime and ensuring branding and UI consistency across environments.
|
||||
Rollback and recovery are essential components of WaaS governance, reducing downtime and ensuring branding and UI consistency across environments.
|
||||
|
||||
## When to Initiate Rollback
|
||||
|
||||
@@ -40,7 +40,7 @@ These symptoms indicate that immediate containment and structured recovery are n
|
||||
|
||||
To prevent further disruption:
|
||||
|
||||
1. Disable the MokoSuite plugin via **System > Plugins**.
|
||||
1. Disable the MokoWaaS plugin via **System > Plugins**.
|
||||
2. Clear Joomla cache.
|
||||
3. Retest impacted areas to confirm whether disabling stabilizes behavior.
|
||||
4. Review Joomla and PHP logs for indicators of root cause.
|
||||
@@ -72,7 +72,7 @@ Snapshots provide a guaranteed restoration point for complex failures.
|
||||
|
||||
Once recovery steps are complete:
|
||||
|
||||
* Ensure branding matches Suite identity guidelines.
|
||||
* Ensure branding matches WaaS identity guidelines.
|
||||
* Confirm no plugin initialization or load order errors.
|
||||
* Validate terminology strings across admin surfaces.
|
||||
* Verify stable rendering of the administrator dashboard.
|
||||
@@ -97,11 +97,11 @@ To reduce the likelihood of rollback events:
|
||||
|
||||
* Test all plugin and template updates in staging before production rollout
|
||||
* Maintain version synchronization across branding related assets
|
||||
* Acquire plugin builds only from approved Suite release channels
|
||||
* Acquire plugin builds only from approved WaaS release channels
|
||||
* Enforce strict change control and governance for branding updates
|
||||
* Audit template overrides regularly to avoid conflicts
|
||||
|
||||
These strategies improve long term Suite platform stability.
|
||||
These strategies improve long term WaaS platform stability.
|
||||
|
||||
## Revision History
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/guides/testing-guide.md
|
||||
BRIEF: Testing guide for MokoSuite v02.01.08
|
||||
BRIEF: Testing guide for MokoWaaS v02.01.08
|
||||
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
|
||||
-->
|
||||
|
||||
# MokoSuite Testing Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Testing Guide (VERSION: 02.35.00)
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
@@ -36,22 +36,22 @@
|
||||
| # | Step | Expected Result | Pass |
|
||||
|---|------|-----------------|------|
|
||||
| 1 | Install plugin via Extensions > Install | "Installed frontend language overrides for en-GB" and "Installed administrator language overrides for en-GB" messages | [ ] |
|
||||
| 2 | Navigate to Extensions > Plugins | Plugin appears as "System - MokoSuite" (not raw key `PLG_SYSTEM_MOKOSUITE`) | [ ] |
|
||||
| 3 | Open plugin config | Three fields visible: Brand Name (default "MokoSuite"), Company Name (default "Moko Consulting"), Support URL (default "https://mokoconsulting.tech") | [ ] |
|
||||
| 4 | Check admin dashboard | "Welcome to MokoSuite!" appears in control panel | [ ] |
|
||||
| 5 | Check admin footer | "Powered by MokoSuite" appears | [ ] |
|
||||
| 6 | Check admin login page | "MokoSuite Administrator Login" title, support links show "Moko Consulting" | [ ] |
|
||||
| 7 | Check frontend footer | "Powered by MokoSuite" in MokoOnyx template | [ ] |
|
||||
| 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoSuite Overrides` sentinel block | [ ] |
|
||||
| 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoSuite Overrides` sentinel block | [ ] |
|
||||
| 2 | Navigate to Extensions > Plugins | Plugin appears as "System - MokoWaaS" (not raw key `PLG_SYSTEM_MOKOWAAS`) | [ ] |
|
||||
| 3 | Open plugin config | Three fields visible: Brand Name (default "MokoWaaS"), Company Name (default "Moko Consulting"), Support URL (default "https://mokoconsulting.tech") | [ ] |
|
||||
| 4 | Check admin dashboard | "Welcome to MokoWaaS!" appears in control panel | [ ] |
|
||||
| 5 | Check admin footer | "Powered by MokoWaaS" appears | [ ] |
|
||||
| 6 | Check admin login page | "MokoWaaS Administrator Login" title, support links show "Moko Consulting" | [ ] |
|
||||
| 7 | Check frontend footer | "Powered by MokoWaaS" in MokoOnyx template | [ ] |
|
||||
| 8 | Check Joomla override files at `administrator/language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
|
||||
| 9 | Check Joomla override files at `language/overrides/en-GB.override.ini` | Contains `BEGIN MokoWaaS Overrides` sentinel block | [ ] |
|
||||
|
||||
### 2.2 Override Preservation (Install on Site with Existing Overrides)
|
||||
|
||||
| # | Step | Expected Result | Pass |
|
||||
|---|------|-----------------|------|
|
||||
| 1 | Before install: add a custom override `MY_CUSTOM_KEY="My Value"` to `administrator/language/overrides/en-GB.override.ini` | Override file contains custom key | [ ] |
|
||||
| 2 | Install MokoSuite plugin | Success messages shown | [ ] |
|
||||
| 3 | Open `administrator/language/overrides/en-GB.override.ini` | `MY_CUSTOM_KEY="My Value"` still present AND MokoSuite sentinel block appended at end | [ ] |
|
||||
| 2 | Install MokoWaaS plugin | Success messages shown | [ ] |
|
||||
| 3 | Open `administrator/language/overrides/en-GB.override.ini` | `MY_CUSTOM_KEY="My Value"` still present AND MokoWaaS sentinel block appended at end | [ ] |
|
||||
| 4 | In Joomla admin: System > Language Overrides | Custom override still visible and functional | [ ] |
|
||||
|
||||
### 2.3 Brand Name Configuration
|
||||
@@ -60,7 +60,7 @@
|
||||
|---|------|-----------------|------|
|
||||
| 1 | Open plugin config, change Brand Name to "TestBrand" | Field accepts the value | [ ] |
|
||||
| 2 | Save and close plugin config | Save succeeds | [ ] |
|
||||
| 3 | Reload admin dashboard | "Welcome to TestBrand!" appears (not "MokoSuite") | [ ] |
|
||||
| 3 | Reload admin dashboard | "Welcome to TestBrand!" appears (not "MokoWaaS") | [ ] |
|
||||
| 4 | Check admin footer | "Powered by TestBrand" | [ ] |
|
||||
| 5 | Check frontend page | "Powered by TestBrand" in footer | [ ] |
|
||||
| 6 | Check Quick Icons area | "TestBrand is up to date." | [ ] |
|
||||
@@ -94,18 +94,18 @@
|
||||
|
||||
| # | Step | Expected Result | Pass |
|
||||
|---|------|-----------------|------|
|
||||
| 1 | Install v01.x of MokoSuite first | Old version installed | [ ] |
|
||||
| 1 | Install v01.x of MokoWaaS first | Old version installed | [ ] |
|
||||
| 2 | Install v02.01.08 over it | Upgrade succeeds with "Installed" messages | [ ] |
|
||||
| 3 | Check override files | MokoSuite sentinel block present, no duplicate keys | [ ] |
|
||||
| 4 | Verify old inline overrides (from v01.x) are cleaned up | No stray MokoSuite keys outside the sentinel block | [ ] |
|
||||
| 3 | Check override files | MokoWaaS sentinel block present, no duplicate keys | [ ] |
|
||||
| 4 | Verify old inline overrides (from v01.x) are cleaned up | No stray MokoWaaS keys outside the sentinel block | [ ] |
|
||||
|
||||
### 2.8 Uninstall
|
||||
|
||||
| # | Step | Expected Result | Pass |
|
||||
|---|------|-----------------|------|
|
||||
| 1 | Uninstall MokoSuite via Extensions > Manage | "Removed frontend language overrides" and "Removed administrator language overrides" messages | [ ] |
|
||||
| 2 | Check `administrator/language/overrides/en-GB.override.ini` | MokoSuite sentinel block removed; any custom overrides (e.g., `MY_CUSTOM_KEY`) still present | [ ] |
|
||||
| 3 | Check `language/overrides/en-GB.override.ini` | MokoSuite block removed; file deleted if no other overrides remain | [ ] |
|
||||
| 1 | Uninstall MokoWaaS via Extensions > Manage | "Removed frontend language overrides" and "Removed administrator language overrides" messages | [ ] |
|
||||
| 2 | Check `administrator/language/overrides/en-GB.override.ini` | MokoWaaS sentinel block removed; any custom overrides (e.g., `MY_CUSTOM_KEY`) still present | [ ] |
|
||||
| 3 | Check `language/overrides/en-GB.override.ini` | MokoWaaS block removed; file deleted if no other overrides remain | [ ] |
|
||||
| 4 | Reload admin dashboard | Default Joomla strings restored | [ ] |
|
||||
|
||||
### 2.9 Admin Override Key Coverage
|
||||
@@ -143,7 +143,7 @@ Verify the following admin areas no longer show "Joomla":
|
||||
| 3 | 404 error page | "Page Not Found" (no Joomla reference) | [ ] |
|
||||
| 4 | Frontend login support | "{company} Support" / "{brand} Documentation" | [ ] |
|
||||
|
||||
### 2.11 Suite Master User Enforcement
|
||||
### 2.11 WaaS Master User Enforcement
|
||||
|
||||
| # | Step | Expected Result | Pass |
|
||||
|---|------|-----------------|------|
|
||||
@@ -153,37 +153,37 @@ Verify the following admin areas no longer show "Joomla":
|
||||
| 4 | Remove from Super Users group, reload admin | Re-added to group | [ ] |
|
||||
| 5 | Change master_username to "customadmin" in config | Enforces new username | [ ] |
|
||||
| 6 | Set enforce_master_user to No, delete user | User NOT recreated | [ ] |
|
||||
| 7 | Check mokosuite log | Enforcement events logged | [ ] |
|
||||
| 7 | Check mokowaas log | Enforcement events logged | [ ] |
|
||||
|
||||
### 2.12 Emergency Access Two-Factor Flow
|
||||
|
||||
| # | Step | Expected Result | Pass |
|
||||
|---|------|-----------------|------|
|
||||
| 1 | Login as mokoconsulting with DB password | mokosuite-verify.php created in site root | [ ] |
|
||||
| 2 | Check error message | "delete /mokosuite-verify.php..." displayed | [ ] |
|
||||
| 3 | Delete mokosuite-verify.php via FTP/SSH | File removed from server | [ ] |
|
||||
| 1 | Login as mokoconsulting with DB password | mokowaas-verify.php created in site root | [ ] |
|
||||
| 2 | Check error message | "delete /mokowaas-verify.php..." displayed | [ ] |
|
||||
| 3 | Delete mokowaas-verify.php via FTP/SSH | File removed from server | [ ] |
|
||||
| 4 | Login again with same credentials | Access granted, logged in as master user | [ ] |
|
||||
| 5 | Check mokosuite-verify.flag | Cleaned up after successful login | [ ] |
|
||||
| 5 | Check mokowaas-verify.flag | Cleaned up after successful login | [ ] |
|
||||
| 6 | Check System > Action Logs | "Emergency access LOGIN" entry with IP | [ ] |
|
||||
| 7 | Check master email inbox | Notification email received with site, user, IP, time | [ ] |
|
||||
| 8 | Set `$mokosuite_allowed_ips = '1.2.3.4';` (not your IP) | Emergency login blocked | [ ] |
|
||||
| 8 | Set `$mokowaas_allowed_ips = '1.2.3.4';` (not your IP) | Emergency login blocked | [ ] |
|
||||
| 9 | Check Action Logs | "Emergency access BLOCKED (unauthorized IP)" entry | [ ] |
|
||||
| 10 | Add your IP to allowed list | Emergency login works | [ ] |
|
||||
| 11 | Remove `$mokosuite_allowed_ips` entirely | Emergency access BLOCKED (empty = denied) | [ ] |
|
||||
| 11 | Remove `$mokowaas_allowed_ips` entirely | Emergency access BLOCKED (empty = denied) | [ ] |
|
||||
| 12 | Use wrong DB password | Normal auth failure | [ ] |
|
||||
| 13 | Check Action Logs | "Emergency access FAILED (wrong password)" entry | [ ] |
|
||||
| 14 | Set emergency_access to No in plugin config | DB password login disabled | [ ] |
|
||||
| 15 | Plugin config > Suite Access tab | IP whitelist panel shows current IPs, your IP, status | [ ] |
|
||||
| 15 | Plugin config > WaaS Access tab | IP whitelist panel shows current IPs, your IP, status | [ ] |
|
||||
|
||||
### 2.13 Override Install Respects User Overrides
|
||||
|
||||
| # | Step | Expected Result | Pass |
|
||||
|---|------|-----------------|------|
|
||||
| 1 | Before install: set `TPL_ATUM_POWERED_BY="Powered by ClientCo"` | User override in file | [ ] |
|
||||
| 2 | Install MokoSuite plugin | Success messages shown | [ ] |
|
||||
| 2 | Install MokoWaaS plugin | Success messages shown | [ ] |
|
||||
| 3 | Check override file | `TPL_ATUM_POWERED_BY` still says "Powered by ClientCo" | [ ] |
|
||||
| 4 | Check MokoSuite sentinel block | `TPL_ATUM_POWERED_BY` NOT in the block (skipped) | [ ] |
|
||||
| 5 | Check all other MokoSuite keys | Present in the block | [ ] |
|
||||
| 4 | Check MokoWaaS sentinel block | `TPL_ATUM_POWERED_BY` NOT in the block (skipped) | [ ] |
|
||||
| 5 | Check all other MokoWaaS keys | Present in the block | [ ] |
|
||||
| 6 | Reinstall/update plugin | User key still preserved | [ ] |
|
||||
| 7 | Uninstall plugin | Only block keys removed, user key stays | [ ] |
|
||||
|
||||
@@ -197,7 +197,7 @@ Verify the following admin areas no longer show "Joomla":
|
||||
| 2 | Plugin config > Maintenance > Reset All Hits = Yes, save | "Reset hit counters on X articles." | [ ] |
|
||||
| 3 | Check #__content.hits | All values are 0 | [ ] |
|
||||
| 4 | Check Reset All Hits toggle | Auto-reset to No | [ ] |
|
||||
| 5 | Check mokosuite log | "All article hits reset" logged | [ ] |
|
||||
| 5 | Check mokowaas log | "All article hits reset" logged | [ ] |
|
||||
|
||||
#### 2.14b Delete All Versions
|
||||
|
||||
@@ -208,7 +208,7 @@ Verify the following admin areas no longer show "Joomla":
|
||||
| 3 | Check #__history table | Empty | [ ] |
|
||||
| 4 | Open article > Versions button | No versions shown | [ ] |
|
||||
| 5 | Check toggle | Auto-reset to No | [ ] |
|
||||
| 6 | Check mokosuite log | "All content versions purged" logged | [ ] |
|
||||
| 6 | Check mokowaas log | "All content versions purged" logged | [ ] |
|
||||
| 7 | Both toggles Yes at same time, save | Both actions execute | [ ] |
|
||||
|
||||
### 2.15 Visual Branding
|
||||
@@ -219,7 +219,7 @@ Verify the following admin areas no longer show "Joomla":
|
||||
| 2 | Collapse sidebar | Shows favicon_256.png | [ ] |
|
||||
| 3 | Log out | Login page shows logo.png | [ ] |
|
||||
| 4 | Check browser tab | favicon.svg displayed (modern) or favicon.ico (legacy) | [ ] |
|
||||
| 5 | Check /media/plg_system_mokosuite/ | All 4 image files present | [ ] |
|
||||
| 5 | Check /media/plg_system_mokowaas/ | All 4 image files present | [ ] |
|
||||
| 6 | Manually change Atum logo in template styles | Reload admin → enforced back to plugin logo | [ ] |
|
||||
| 7 | Check Atum style params in DB | logoBrandLarge, logoBrandSmall, loginLogo set, alt text empty | [ ] |
|
||||
| 8 | Set Primary Color | Admin accent color changes | [ ] |
|
||||
@@ -265,7 +265,7 @@ Verify the following admin areas no longer show "Joomla":
|
||||
|
||||
| # | Scenario | Expected Behavior |
|
||||
|---|----------|-------------------|
|
||||
| 1 | Brand Name field left empty | Falls back to default "MokoSuite" |
|
||||
| 1 | Brand Name field left empty | Falls back to default "MokoWaaS" |
|
||||
| 2 | Brand Name with special characters (`<script>`, `"`, `&`) | Characters appear escaped/safe, no XSS |
|
||||
| 3 | Very long brand name (100+ chars) | Renders without breaking layout |
|
||||
| 4 | Plugin disabled but override files exist | Sentinel block in Joomla override files still provides static branding |
|
||||
@@ -279,10 +279,10 @@ Run from the project root:
|
||||
```bash
|
||||
# Lint all PHP files
|
||||
php -l source/script.php
|
||||
php -l source/Extension/MokoSuite.php
|
||||
php -l source/Extension/MokoWaaS.php
|
||||
|
||||
# Verify all override files have placeholders (no hardcoded "MokoSuite" in values)
|
||||
grep -r '"MokoSuite' source/language/overrides/ source/administrator/language/overrides/
|
||||
# Verify all override files have placeholders (no hardcoded "MokoWaaS" in values)
|
||||
grep -r '"MokoWaaS' source/language/overrides/ source/administrator/language/overrides/
|
||||
# Expected: no output (all values should use {{BRAND_NAME}})
|
||||
|
||||
# Verify sentinel constants match
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/guides/troubleshooting-guide.md
|
||||
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuite plugin
|
||||
NOTE: Designed for administrators and Suite operations teams
|
||||
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
|
||||
NOTE: Designed for administrators and WaaS operations teams
|
||||
-->
|
||||
|
||||
# MokoSuite Troubleshooting Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Troubleshooting Guide (VERSION: 02.35.00)
|
||||
|
||||
## Introduction
|
||||
|
||||
The MokoSuite Troubleshooting Guide provides a structured, repeatable approach for diagnosing and resolving issues related to branding enforcement across Suite managed Joomla environments. It assists administrators, support engineers, and operations staff in identifying symptoms, validating root causes, and restoring consistent platform behavior.
|
||||
The MokoWaaS Troubleshooting Guide provides a structured, repeatable approach for diagnosing and resolving issues related to branding enforcement across WaaS managed Joomla environments. It assists administrators, support engineers, and operations staff in identifying symptoms, validating root causes, and restoring consistent platform behavior.
|
||||
|
||||
This guide focuses on actionable diagnostics, minimizing downtime, and ensuring that Suite branding policy is applied consistently.
|
||||
This guide focuses on actionable diagnostics, minimizing downtime, and ensuring that WaaS branding policy is applied consistently.
|
||||
|
||||
## Understanding the Plugin’s Operational Behavior
|
||||
|
||||
As a system level extension, the MokoSuite plugin:
|
||||
As a system level extension, the MokoWaaS plugin:
|
||||
|
||||
* Loads early in the Joomla lifecycle
|
||||
* Influences visible terminology and branding markers
|
||||
@@ -60,7 +60,7 @@ Branding appears unchanged or reverts to Joomla defaults.
|
||||
|
||||
### Missing or Incorrect Terminology
|
||||
|
||||
Labels or UI strings do not match expected Suite terminology.
|
||||
Labels or UI strings do not match expected WaaS terminology.
|
||||
|
||||
**Likely Causes:**
|
||||
|
||||
@@ -72,7 +72,7 @@ Labels or UI strings do not match expected Suite terminology.
|
||||
|
||||
1. Validate the integrity of all language files.
|
||||
2. Check extension overrides.
|
||||
3. Reapply updated MokoSuite language packs.
|
||||
3. Reapply updated MokoWaaS language packs.
|
||||
4. Review recent Joomla updates for changes in language constants.
|
||||
|
||||
---
|
||||
@@ -130,11 +130,11 @@ If your troubleshooting steps do not resolve the issue:
|
||||
|
||||
1. Document observed symptoms and any steps already taken.
|
||||
2. Capture relevant logs, console messages, and screenshots.
|
||||
3. Escalate to Suite operations or development teams.
|
||||
3. Escalate to WaaS operations or development teams.
|
||||
4. Include environmental details such as:
|
||||
|
||||
* Joomla version
|
||||
* MokoSuite plugin version
|
||||
* MokoWaaS plugin version
|
||||
* Template version
|
||||
* Installed third party extensions
|
||||
|
||||
|
||||
@@ -8,23 +8,23 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/guides/upgrade-and-versioning-guide.md
|
||||
BRIEF: Guide for updating, versioning, and maintaining the MokoSuite plugin
|
||||
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
|
||||
NOTE: Defines release flow, version rules, and upgrade validation
|
||||
-->
|
||||
|
||||
# MokoSuite Upgrade and Versioning Guide (VERSION: 02.34.50)
|
||||
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.35.00)
|
||||
|
||||
## Introduction
|
||||
|
||||
The MokoSuite Upgrade and Versioning Guide establishes a consistent lifecycle management process for the plugin across Suite governed environments. By defining clear versioning rules, upgrade requirements, and governance commitments, this guide ensures stability and predictable branding behavior throughout the platform.
|
||||
The MokoWaaS Upgrade and Versioning Guide establishes a consistent lifecycle management process for the plugin across WaaS governed environments. By defining clear versioning rules, upgrade requirements, and governance commitments, this guide ensures stability and predictable branding behavior throughout the platform.
|
||||
|
||||
## Versioning Standards
|
||||
|
||||
The plugin uses a semantic versioning model aligned with Suite operational governance. Each segment communicates functional impact and expected deployment considerations.
|
||||
The plugin uses a semantic versioning model aligned with WaaS operational governance. Each segment communicates functional impact and expected deployment considerations.
|
||||
|
||||
### Version Structure
|
||||
|
||||
@@ -47,7 +47,7 @@ Before applying a new release:
|
||||
1. Validate compatibility with:
|
||||
|
||||
* Joomla core version
|
||||
* Suite template version
|
||||
* WaaS template version
|
||||
* Language pack versions
|
||||
2. Review release notes and change logs.
|
||||
3. Capture an environment snapshot or backup.
|
||||
@@ -79,7 +79,7 @@ Versioning and rollout require alignment across multiple teams.
|
||||
* Tag releases using semantic rules.
|
||||
* Provide documentation, changelogs, and upgrade notes.
|
||||
|
||||
### Suite Platform Operations
|
||||
### WaaS Platform Operations
|
||||
|
||||
* Validate releases in staging.
|
||||
* Approve and coordinate production rollout.
|
||||
|
||||
+8
-8
@@ -8,25 +8,25 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
VERSION: 02.34.50
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.35.00
|
||||
PATH: /docs/index.md
|
||||
BRIEF: Master index of all documentation for the MokoSuite plugin
|
||||
BRIEF: Master index of all documentation for the MokoWaaS plugin
|
||||
NOTE: Automatically maintained index for all guide canvases
|
||||
-->
|
||||
|
||||
# MokoSuite Documentation Index (VERSION: 02.34.50)
|
||||
# MokoWaaS Documentation Index (VERSION: 02.35.00)
|
||||
|
||||
## Introduction
|
||||
|
||||
The MokoSuite Documentation Index provides the authoritative map of all documentation assets associated with the MokoSuite system plugin. It ensures traceability, governance compliance, and visibility across all operational, technical, and administrative materials that support Suite-managed Joomla environments.
|
||||
The MokoWaaS Documentation Index provides the authoritative map of all documentation assets associated with the MokoWaaS system plugin. It ensures traceability, governance compliance, and visibility across all operational, technical, and administrative materials that support WaaS-managed Joomla environments.
|
||||
|
||||
This index serves as the entry point for contributors, administrators, and governance teams who require a single source of truth for locating and validating documentation files.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
Documentation is organized into two primary categories: core documentation and operational guides. Each file is individually versioned, governed, and maintained as part of the Suite documentation ecosystem.
|
||||
Documentation is organized into two primary categories: core documentation and operational guides. Each file is individually versioned, governed, and maintained as part of the WaaS documentation ecosystem.
|
||||
|
||||
### Core Documentation
|
||||
|
||||
@@ -55,7 +55,7 @@ Documentation is organized into two primary categories: core documentation and o
|
||||
|
||||
## Maintenance and Governance
|
||||
|
||||
To preserve documentation integrity across the Suite platform, the following standards apply:
|
||||
To preserve documentation integrity across the WaaS platform, the following standards apply:
|
||||
|
||||
* All files must include the standard Moko Consulting metadata header.
|
||||
* Version changes must be reflected both in the header and revision history.
|
||||
|
||||
+16
-16
@@ -8,27 +8,27 @@
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoSuite
|
||||
REPO: https://github.com/mokoconsulting-tech/mokosuite
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: /docs/plugin-basic.md
|
||||
VERSION: 02.34.50
|
||||
BRIEF: Baseline documentation for the MokoSuite system plugin
|
||||
VERSION: 02.35.00
|
||||
BRIEF: Baseline documentation for the MokoWaaS system plugin
|
||||
NOTE: Foundational reference for internal and external stakeholders
|
||||
-->
|
||||
|
||||
# MokoSuite Plugin Overview (VERSION: 02.34.50)
|
||||
# MokoWaaS Plugin Overview (VERSION: 02.35.00)
|
||||
|
||||
## Introduction
|
||||
|
||||
The MokoSuite plugin is a foundational system component used across Suite-managed Joomla environments. It ensures consistent application of platform identity, terminology, and user experience standards. By centralizing key branding functions, the plugin supports multi‑tenant Suite operations and reduces administrative fragmentation.
|
||||
The MokoWaaS plugin is a foundational system component used across WaaS-managed Joomla environments. It ensures consistent application of platform identity, terminology, and user experience standards. By centralizing key branding functions, the plugin supports multi‑tenant WaaS operations and reduces administrative fragmentation.
|
||||
|
||||
## Role in the Suite Platform
|
||||
## Role in the WaaS Platform
|
||||
|
||||
The plugin establishes a unified naming and branding layer across administrator and user interfaces. As the primary enforcement point for Suite branding policy, it integrates with templates, modules, and language packs to maintain consistent terminology and presentation.
|
||||
The plugin establishes a unified naming and branding layer across administrator and user interfaces. As the primary enforcement point for WaaS branding policy, it integrates with templates, modules, and language packs to maintain consistent terminology and presentation.
|
||||
|
||||
Key functions include:
|
||||
|
||||
* Replacing Joomla-native labels with Suite-approved terminology.
|
||||
* Replacing Joomla-native labels with WaaS-approved terminology.
|
||||
* Ensuring consistent visual identifiers in administrative interfaces.
|
||||
* Providing a stable branding baseline consumed by other system extensions.
|
||||
|
||||
@@ -38,7 +38,7 @@ To ensure correct operation, the plugin requires:
|
||||
|
||||
* Joomla 5.x or higher
|
||||
* PHP 8.1 or higher
|
||||
* A compatible Suite template aligned with Moko platform standards
|
||||
* A compatible WaaS template aligned with Moko platform standards
|
||||
* System-level plugin execution priority before template rendering
|
||||
|
||||
## Installation Overview
|
||||
@@ -58,12 +58,12 @@ The plugin provides configurable controls under the Joomla Plugin Manager.
|
||||
|
||||
Primary configuration categories include:
|
||||
|
||||
* **Terminology Controls:** Apply standardized Suite vocabulary.
|
||||
* **Terminology Controls:** Apply standardized WaaS vocabulary.
|
||||
* **UI Adjustments:** Modify display elements such as headers or default labels.
|
||||
* **Visibility Controls:** Suppress or replace Joomla identifiers as needed.
|
||||
* **Branding Elements:** Manage powered‑by references and footer behavior.
|
||||
|
||||
Configuration ensures a consistent and predictable Suite identity across all managed sites.
|
||||
Configuration ensures a consistent and predictable WaaS identity across all managed sites.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
@@ -71,8 +71,8 @@ The plugin is implemented as a Joomla 5.x system plugin with the following archi
|
||||
|
||||
### Core Components
|
||||
|
||||
* **mokosuite.php** - Main plugin class (`PlgSystemMokoSuite`) that extends `CMSPlugin`
|
||||
* **mokosuite.xml** - Plugin manifest defining metadata, file structure, and configuration parameters
|
||||
* **mokowaas.php** - Main plugin class (`PlgSystemMokoWaaS`) that extends `CMSPlugin`
|
||||
* **mokowaas.xml** - Plugin manifest defining metadata, file structure, and configuration parameters
|
||||
* **services/provider.php** - Dependency injection service provider for Joomla 5.x container registration
|
||||
|
||||
### Event Handlers
|
||||
@@ -99,7 +99,7 @@ The plugin exposes the following configuration parameters:
|
||||
|
||||
### Namespace and Autoloading
|
||||
|
||||
Uses Joomla 5.x namespace: `Moko\Plugin\System\MokoSuite` with PSR-4 autoloading through the service provider.
|
||||
Uses Joomla 5.x namespace: `Moko\Plugin\System\MokoWaaS` with PSR-4 autoloading through the service provider.
|
||||
|
||||
## Operational Expectations
|
||||
|
||||
@@ -107,7 +107,7 @@ Platform operators should maintain the plugin in an enabled state at all times.
|
||||
|
||||
* Version alignment across branding components
|
||||
* Review of template overrides for conflict prevention
|
||||
* Coordination with Suite governance for terminology changes
|
||||
* Coordination with WaaS governance for terminology changes
|
||||
|
||||
## Constraints and Considerations
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# MokoSuite Plugin Overview
|
||||
# MokoWaaS Plugin Overview
|
||||
|
||||
## Executive Summary
|
||||
The MokoSuite plugin operates as a core enablement layer within the Suite delivery stack, aligning platform branding, terminology, and visual identity across administrative and user-facing touchpoints. It standardizes language, reinforces Suite positioning, and reduces fragmentation risk across templates and extensions.
|
||||
The MokoWaaS plugin operates as a core enablement layer within the WaaS delivery stack, aligning platform branding, terminology, and visual identity across administrative and user-facing touchpoints. It standardizes language, reinforces WaaS positioning, and reduces fragmentation risk across templates and extensions.
|
||||
|
||||
## Purpose
|
||||
- Replace default Joomla terminology with Suite aligned naming.
|
||||
- Replace default Joomla terminology with WaaS aligned naming.
|
||||
- Provide a consistent brand experience in the administrator interface.
|
||||
- Establish a baseline layer for future identity and UX governance.
|
||||
|
||||
@@ -17,16 +17,16 @@ The MokoSuite plugin operates as a core enablement layer within the Suite delive
|
||||
## System Requirements
|
||||
- Joomla 5.x
|
||||
- PHP 8.1 or higher
|
||||
- Compatible Suite template and language stack
|
||||
- Compatible WaaS template and language stack
|
||||
- Ability to run as a system plugin before template rendering
|
||||
|
||||
## High Level Lifecycle
|
||||
1. Install the plugin via the Joomla Extension Manager.
|
||||
2. Enable the plugin in the System Plugin list.
|
||||
3. Clear cache to propagate new language strings.
|
||||
4. Validate administrator and frontend views for correct Suite branding.
|
||||
4. Validate administrator and frontend views for correct WaaS branding.
|
||||
|
||||
## Operational Notes
|
||||
- The plugin should remain enabled on all Suite managed instances.
|
||||
- The plugin should remain enabled on all WaaS managed instances.
|
||||
- Changes to terminology may impact documentation and training materials and should be coordinated with internal teams.
|
||||
- Third party extensions may require additional overrides for full branding alignment.
|
||||
|
||||
@@ -6,11 +6,11 @@ This file is part of a Moko Consulting project.
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoSuite.Documentation
|
||||
DEFGROUP: MokoWaaS.Documentation
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoSuite
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
|
||||
PATH: /docs/update-server.md
|
||||
VERSION: 02.34.50
|
||||
VERSION: 02.35.00
|
||||
BRIEF: How this extension's Joomla update server file (update.xml) is managed
|
||||
-->
|
||||
|
||||
@@ -107,7 +107,7 @@ Your XML manifest must include an `<updateservers>` tag pointing to the `update.
|
||||
<!-- ... -->
|
||||
<updateservers>
|
||||
<server type="extension" name="My Extension Updates">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/MokoSuite/main/update.xml
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/MokoWaaS/main/update.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
</extension>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<access component="com_mokosuite">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
|
||||
<action name="mokosuite.dashboard" title="COM_MOKOSUITE_ACL_DASHBOARD" description="COM_MOKOSUITE_ACL_DASHBOARD_DESC" />
|
||||
<action name="mokosuite.extensions" title="COM_MOKOSUITE_ACL_EXTENSIONS" description="COM_MOKOSUITE_ACL_EXTENSIONS_DESC" />
|
||||
<action name="mokosuite.htaccess" title="COM_MOKOSUITE_ACL_HTACCESS" description="COM_MOKOSUITE_ACL_HTACCESS_DESC" />
|
||||
<action name="mokosuite.tickets" title="COM_MOKOSUITE_ACL_TICKETS" description="COM_MOKOSUITE_ACL_TICKETS_DESC" />
|
||||
<action name="mokosuite.tickets.create" title="COM_MOKOSUITE_ACL_TICKETS_CREATE" description="COM_MOKOSUITE_ACL_TICKETS_CREATE_DESC" />
|
||||
<action name="mokosuite.tickets.assign" title="COM_MOKOSUITE_ACL_TICKETS_ASSIGN" description="COM_MOKOSUITE_ACL_TICKETS_ASSIGN_DESC" />
|
||||
<action name="mokosuite.plugins.toggle" title="COM_MOKOSUITE_ACL_PLUGINS_TOGGLE" description="COM_MOKOSUITE_ACL_PLUGINS_TOGGLE_DESC" />
|
||||
<action name="mokosuite.cache" title="COM_MOKOSUITE_ACL_CACHE" description="COM_MOKOSUITE_ACL_CACHE_DESC" />
|
||||
</section>
|
||||
</access>
|
||||
@@ -1,41 +0,0 @@
|
||||
; MokoSuite Admin Dashboard - Language Strings
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
COM_MOKOSUITE_DASHBOARD_TITLE="MokoSuite Control Panel"
|
||||
COM_MOKOSUITE_SITE="Site"
|
||||
COM_MOKOSUITE_DATABASE="Database"
|
||||
COM_MOKOSUITE_DEBUG_ON="Debug ON"
|
||||
COM_MOKOSUITE_OFFLINE="Offline"
|
||||
COM_MOKOSUITE_CLEAR_CACHE="Clear Cache"
|
||||
COM_MOKOSUITE_CHECK_UPDATES="Check Updates"
|
||||
COM_MOKOSUITE_ENABLED="Enabled"
|
||||
COM_MOKOSUITE_DISABLED="Disabled"
|
||||
COM_MOKOSUITE_PROTECTED="Protected"
|
||||
COM_MOKOSUITE_CONFIGURE="Configure"
|
||||
COM_MOKOSUITE_TOGGLE_SUCCESS="Plugin state updated."
|
||||
COM_MOKOSUITE_TOGGLE_FAIL="Failed to update plugin state."
|
||||
COM_MOKOSUITE_CACHE_CLEARED="Cache cleared successfully."
|
||||
COM_MOKOSUITE_EXTENSIONS_TITLE="Moko Extensions"
|
||||
COM_MOKOSUITE_EXTENSIONS_INFO="Install Moko Consulting Joomla packages from the official release server. Updates are handled through Joomla's native System > Update mechanism — each package registers its own update server."
|
||||
COM_MOKOSUITE_EXTENSIONS_LINK="Moko Extensions"
|
||||
COM_MOKOSUITE_HTACCESS_TITLE=".htaccess Maker"
|
||||
COM_MOKOSUITE_TICKETS_TITLE="Helpdesk"
|
||||
|
||||
; ACL
|
||||
COM_MOKOSUITE_ACL_DASHBOARD="View Dashboard"
|
||||
COM_MOKOSUITE_ACL_DASHBOARD_DESC="Allow viewing the MokoSuite control panel dashboard."
|
||||
COM_MOKOSUITE_ACL_EXTENSIONS="Manage Extensions"
|
||||
COM_MOKOSUITE_ACL_EXTENSIONS_DESC="Allow installing and uninstalling Moko extensions."
|
||||
COM_MOKOSUITE_ACL_HTACCESS="Manage .htaccess"
|
||||
COM_MOKOSUITE_ACL_HTACCESS_DESC="Allow editing and saving the .htaccess configuration."
|
||||
COM_MOKOSUITE_ACL_TICKETS="View Tickets"
|
||||
COM_MOKOSUITE_ACL_TICKETS_DESC="Allow viewing helpdesk tickets."
|
||||
COM_MOKOSUITE_ACL_TICKETS_CREATE="Create Tickets"
|
||||
COM_MOKOSUITE_ACL_TICKETS_CREATE_DESC="Allow creating new helpdesk tickets."
|
||||
COM_MOKOSUITE_ACL_TICKETS_ASSIGN="Assign Tickets"
|
||||
COM_MOKOSUITE_ACL_TICKETS_ASSIGN_DESC="Allow assigning tickets to other users."
|
||||
COM_MOKOSUITE_ACL_PLUGINS_TOGGLE="Toggle Plugins"
|
||||
COM_MOKOSUITE_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoSuite feature plugins."
|
||||
COM_MOKOSUITE_ACL_CACHE="Clear Cache"
|
||||
COM_MOKOSUITE_ACL_CACHE_DESC="Allow clearing the Joomla cache from the dashboard."
|
||||
@@ -1,19 +0,0 @@
|
||||
; MokoSuite Admin Dashboard - System Language Strings
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
COM_MOKOSUITE="MokoSuite"
|
||||
COM_MOKOSUITE_DESCRIPTION="MokoSuite admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management."
|
||||
COM_MOKOSUITE_DASHBOARD_TITLE="MokoSuite Control Panel"
|
||||
COM_MOKOSUITE_MENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOSUITE_MENU_EXTENSIONS="Moko Extensions"
|
||||
COM_MOKOSUITE_MENU_PLUGINS="Feature Plugins"
|
||||
COM_MOKOSUITE_MENU_UPDATES="Joomla Updates"
|
||||
COM_MOKOSUITE_MENU_CHECKIN="Global Check-in"
|
||||
COM_MOKOSUITE_MENU_TICKETS="Helpdesk"
|
||||
COM_MOKOSUITE_MENU_HTACCESS=".htaccess Maker"
|
||||
COM_MOKOSUITE_MENU_PRIVACY="Privacy Guard"
|
||||
COM_MOKOSUITE_MENU_WAFLOG="WAF Log"
|
||||
COM_MOKOSUITE_MENU_DATABASE="Database Tools"
|
||||
COM_MOKOSUITE_MENU_CLEANUP="Cache Cleanup"
|
||||
COM_MOKOSUITE_MENU_CACHE="Cache Management"
|
||||
@@ -1,13 +0,0 @@
|
||||
--
|
||||
-- MokoSuite component uninstall — drop all tables
|
||||
--
|
||||
DROP TABLE IF EXISTS `#__mokosuite_download_keys`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_retention_policies`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_data_requests`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_consent_log`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_waf_log`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_ticket_automation`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_ticket_canned`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_ticket_replies`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_tickets`;
|
||||
DROP TABLE IF EXISTS `#__mokosuite_ticket_categories`;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Remove download_keys table (feature reverted — preflight handles key preservation)
|
||||
DROP TABLE IF EXISTS `#__mokosuite_download_keys`;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- RSA signing replaces key ring — drop table if it was created
|
||||
DROP TABLE IF EXISTS `#__mokosuite_api_keys`;
|
||||
@@ -1,85 +0,0 @@
|
||||
-- Add contact link to tickets (optional FK to #__contact_details)
|
||||
ALTER TABLE `#__mokosuite_tickets`
|
||||
ADD COLUMN `contact_id` INT UNSIGNED DEFAULT NULL AFTER `category_id`,
|
||||
ADD KEY `idx_contact` (`contact_id`);
|
||||
|
||||
-- Multi-assignee junction table (replaces single assigned_to column)
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_assignees` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ticket_id` INT UNSIGNED NOT NULL,
|
||||
`assignee_type` ENUM('user','group') NOT NULL DEFAULT 'user',
|
||||
`assignee_id` INT NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_unique_assignment` (`ticket_id`, `assignee_type`, `assignee_id`),
|
||||
KEY `idx_ticket` (`ticket_id`),
|
||||
KEY `idx_assignee` (`assignee_type`, `assignee_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Migrate existing single-assignee data to junction table
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_assignees` (`ticket_id`, `assignee_type`, `assignee_id`)
|
||||
SELECT `id`, 'user', `assigned_to` FROM `#__mokosuite_tickets` WHERE `assigned_to` IS NOT NULL AND `assigned_to` > 0;
|
||||
|
||||
-- Customizable ticket statuses (replaces ENUM)
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_statuses` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(100) NOT NULL,
|
||||
`alias` VARCHAR(100) NOT NULL,
|
||||
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0,
|
||||
`is_closed` TINYINT NOT NULL DEFAULT 0,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_alias` (`alias`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_statuses` (`id`, `title`, `alias`, `color`, `is_default`, `is_closed`, `ordering`) VALUES
|
||||
(1, 'Open', 'open', 'bg-primary', 1, 0, 1),
|
||||
(2, 'In Progress', 'in_progress', 'bg-info', 0, 0, 2),
|
||||
(3, 'Waiting', 'waiting', 'bg-warning text-dark', 0, 0, 3),
|
||||
(4, 'Resolved', 'resolved', 'bg-success', 0, 0, 4),
|
||||
(5, 'Closed', 'closed', 'bg-secondary', 0, 1, 5);
|
||||
|
||||
-- Customizable ticket priorities (replaces ENUM)
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_priorities` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(100) NOT NULL,
|
||||
`alias` VARCHAR(100) NOT NULL,
|
||||
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0,
|
||||
`weight` INT NOT NULL DEFAULT 0,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_alias` (`alias`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_priorities` (`id`, `title`, `alias`, `color`, `is_default`, `weight`, `ordering`) VALUES
|
||||
(1, 'Low', 'low', 'bg-secondary', 0, 10, 1),
|
||||
(2, 'Normal', 'normal', 'bg-primary', 1, 20, 2),
|
||||
(3, 'High', 'high', 'bg-warning text-dark', 0, 30, 3),
|
||||
(4, 'Urgent', 'urgent', 'bg-danger', 0, 40, 4);
|
||||
|
||||
-- Add INT FK columns for status/priority (coexist with ENUM during migration)
|
||||
ALTER TABLE `#__mokosuite_tickets`
|
||||
ADD COLUMN `status_id` INT UNSIGNED DEFAULT NULL AFTER `status`,
|
||||
ADD COLUMN `priority_id` INT UNSIGNED DEFAULT NULL AFTER `priority`,
|
||||
ADD KEY `idx_status_id` (`status_id`),
|
||||
ADD KEY `idx_priority_id` (`priority_id`);
|
||||
|
||||
-- Populate new columns from existing ENUM values
|
||||
UPDATE `#__mokosuite_tickets` t
|
||||
JOIN `#__mokosuite_ticket_statuses` s ON s.alias = t.status
|
||||
SET t.status_id = s.id
|
||||
WHERE t.status_id IS NULL;
|
||||
|
||||
UPDATE `#__mokosuite_tickets` t
|
||||
JOIN `#__mokosuite_ticket_priorities` p ON p.alias = t.priority
|
||||
SET t.priority_id = p.id
|
||||
WHERE t.priority_id IS NULL;
|
||||
|
||||
-- Junction: which Joomla field groups apply to which ticket categories
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_category_field_groups` (
|
||||
`category_id` INT UNSIGNED NOT NULL,
|
||||
`field_group_id` INT NOT NULL,
|
||||
PRIMARY KEY (`category_id`, `field_group_id`),
|
||||
KEY `idx_field_group` (`field_group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
|
||||
class ErpReportsModel extends BaseDatabaseModel
|
||||
{
|
||||
public function getSalesReport(string $from, string $to, string $groupBy = 'month'): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$fmt = match ($groupBy) { 'day' => '%Y-%m-%d', 'week' => '%Y-W%v', 'year' => '%Y', default => '%Y-%m' };
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('DATE_FORMAT(inv.created, ' . $db->quote($fmt) . ') AS period')
|
||||
->select('COUNT(*) AS invoice_count, COALESCE(SUM(inv.total), 0) AS revenue, COALESCE(SUM(inv.amount_paid), 0) AS collected')
|
||||
->from($db->quoteName('#__mokosuite_erp_invoices', 'inv'))
|
||||
->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from))
|
||||
->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59'))
|
||||
->where($db->quoteName('inv.type') . ' = ' . $db->quote('standard'))
|
||||
->group('period')->order('period ASC'));
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
public function getTopCustomers(string $from, string $to, int $limit = 10): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('cd.name AS contact_name, COALESCE(SUM(inv.total), 0) AS total_revenue, COUNT(*) AS invoice_count')
|
||||
->from($db->quoteName('#__mokosuite_erp_invoices', 'inv'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = inv.contact_id')
|
||||
->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from))
|
||||
->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59'))
|
||||
->group('inv.contact_id')->order('total_revenue DESC'), 0, $limit);
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
public function getTopProducts(string $from, string $to, int $limit = 10): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('p.sku, c.title AS product_name, COALESCE(SUM(ii.quantity), 0) AS qty_sold, COALESCE(SUM(ii.line_total), 0) AS revenue')
|
||||
->from($db->quoteName('#__mokosuite_erp_invoice_items', 'ii'))
|
||||
->join('INNER', $db->quoteName('#__mokosuite_erp_invoices', 'inv') . ' ON inv.id = ii.invoice_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuite_erp_products', 'p') . ' ON p.id = ii.product_id')
|
||||
->join('LEFT', $db->quoteName('#__content', 'c') . ' ON c.id = p.article_id')
|
||||
->where($db->quoteName('inv.created') . ' >= ' . $db->quote($from))
|
||||
->where($db->quoteName('inv.created') . ' <= ' . $db->quote($to . ' 23:59:59'))
|
||||
->where($db->quoteName('ii.product_id') . ' IS NOT NULL')
|
||||
->group('ii.product_id')->order('revenue DESC'), 0, $limit);
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
public function getPipelineReport(string $from, string $to): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('status, COUNT(*) AS cnt, COALESCE(SUM(value), 0) AS total_value')
|
||||
->from($db->quoteName('#__mokosuite_erp_deals'))
|
||||
->where($db->quoteName('created') . ' >= ' . $db->quote($from))
|
||||
->where($db->quoteName('created') . ' <= ' . $db->quote($to . ' 23:59:59'))
|
||||
->group('status'));
|
||||
return $db->loadObjectList('status') ?: [];
|
||||
}
|
||||
|
||||
public function getAgingReceivables(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('inv.id, inv.ref, inv.total, inv.amount_paid, inv.due_date, (inv.total - inv.amount_paid) AS balance, DATEDIFF(CURDATE(), inv.due_date) AS days_overdue')
|
||||
->select('cd.name AS contact_name')
|
||||
->from($db->quoteName('#__mokosuite_erp_invoices', 'inv'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = inv.contact_id')
|
||||
->where($db->quoteName('inv.status') . ' IN (' . $db->quote('sent') . ',' . $db->quote('partial') . ',' . $db->quote('overdue') . ')')
|
||||
->where('(inv.total - inv.amount_paid) > 0')
|
||||
->order('days_overdue DESC'));
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\ErpReports;
|
||||
|
||||
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 $activeTab = 'sales';
|
||||
protected $salesData = [];
|
||||
protected $topCustomers = [];
|
||||
protected $topProducts = [];
|
||||
protected $pipelineData = [];
|
||||
protected $agingData = [];
|
||||
protected $dateFrom;
|
||||
protected $dateTo;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$this->activeTab = $input->get('tab', 'sales', 'CMD');
|
||||
$this->dateFrom = $input->get('from', date('Y-01-01'), 'STRING');
|
||||
$this->dateTo = $input->get('to', date('Y-m-d'), 'STRING');
|
||||
$this->salesData = $model->getSalesReport($this->dateFrom, $this->dateTo);
|
||||
$this->topCustomers = $model->getTopCustomers($this->dateFrom, $this->dateTo);
|
||||
$this->topProducts = $model->getTopProducts($this->dateFrom, $this->dateTo);
|
||||
$this->pipelineData = $model->getPipelineReport($this->dateFrom, $this->dateTo);
|
||||
$this->agingData = $model->getAgingReceivables();
|
||||
ToolbarHelper::title('ERP Reports', 'icon-chart-bar');
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.erp', 'com_mokosuite/erp.css');
|
||||
$wa->registerAndUseScript('com_mokosuite.erp-dashboard', 'com_mokosuite/erp-dashboard.js', [], ['defer' => true]);
|
||||
Factory::getApplication()->getDocument()->addScriptOptions('mokosuite.erp', ['revenueChart' => $this->salesData]);
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
$tab = $this->activeTab;
|
||||
$totalRevenue = array_sum(array_map(fn($r) => (float) $r->revenue, $this->salesData));
|
||||
$totalCollected = array_sum(array_map(fn($r) => (float) $r->collected, $this->salesData));
|
||||
$wonDeals = $this->pipelineData['won'] ?? (object) ['cnt' => 0, 'total_value' => 0];
|
||||
$lostDeals = $this->pipelineData['lost'] ?? (object) ['cnt' => 0, 'total_value' => 0];
|
||||
$openDeals = $this->pipelineData['open'] ?? (object) ['cnt' => 0, 'total_value' => 0];
|
||||
$closedTotal = ((int) ($wonDeals->cnt ?? 0)) + ((int) ($lostDeals->cnt ?? 0));
|
||||
$winRate = $closedTotal > 0 ? round((int) ($wonDeals->cnt ?? 0) / $closedTotal * 100, 1) : 0;
|
||||
?>
|
||||
<div class="mokosuite-erp-reports">
|
||||
<div class="card shadow-sm mb-3"><div class="card-body py-2">
|
||||
<form method="get" class="d-flex gap-2 align-items-center flex-wrap">
|
||||
<input type="hidden" name="option" value="com_mokosuite"><input type="hidden" name="view" value="erpreports"><input type="hidden" name="tab" value="<?php echo $this->escape($tab); ?>">
|
||||
<label class="form-label mb-0 small">From:</label><input type="date" name="from" class="form-control form-control-sm" style="max-width:160px" value="<?php echo $this->escape($this->dateFrom); ?>">
|
||||
<label class="form-label mb-0 small">To:</label><input type="date" name="to" class="form-control form-control-sm" style="max-width:160px" value="<?php echo $this->escape($this->dateTo); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
|
||||
</form>
|
||||
</div></div>
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item"><a class="nav-link <?php echo $tab === 'sales' ? 'active' : ''; ?>" href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpreports&tab=sales&from=' . $this->dateFrom . '&to=' . $this->dateTo); ?>">Sales</a></li>
|
||||
<li class="nav-item"><a class="nav-link <?php echo $tab === 'pipeline' ? 'active' : ''; ?>" href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpreports&tab=pipeline&from=' . $this->dateFrom . '&to=' . $this->dateTo); ?>">Pipeline</a></li>
|
||||
<li class="nav-item"><a class="nav-link <?php echo $tab === 'aging' ? 'active' : ''; ?>" href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpreports&tab=aging'); ?>">Aging Receivables</a></li>
|
||||
</ul>
|
||||
<?php if ($tab === 'sales') : ?>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Revenue</div><div class="fs-3 fw-bold">$<?php echo number_format($totalRevenue, 0); ?></div></div></div></div>
|
||||
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Collected</div><div class="fs-3 fw-bold text-success">$<?php echo number_format($totalCollected, 0); ?></div></div></div></div>
|
||||
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Outstanding</div><div class="fs-3 fw-bold text-danger">$<?php echo number_format($totalRevenue - $totalCollected, 0); ?></div></div></div></div>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-3"><div class="card-header"><h5 class="mb-0">Revenue by Period</h5></div><div class="card-body"><canvas id="erp-revenue-chart" height="300"></canvas></div></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><div class="card shadow-sm"><div class="card-header"><h5 class="mb-0">Top Customers</h5></div><div class="card-body p-0"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>Customer</th><th class="text-end">Revenue</th><th class="text-end">Invoices</th></tr></thead><tbody>
|
||||
<?php foreach ($this->topCustomers as $c) : ?><tr><td><?php echo $this->escape($c->contact_name ?? '—'); ?></td><td class="text-end fw-bold">$<?php echo number_format((float) $c->total_revenue, 0); ?></td><td class="text-end"><?php echo (int) $c->invoice_count; ?></td></tr><?php endforeach; ?>
|
||||
</tbody></table></div></div></div>
|
||||
<div class="col-md-6"><div class="card shadow-sm"><div class="card-header"><h5 class="mb-0">Top Products</h5></div><div class="card-body p-0"><table class="table table-sm mb-0"><thead class="table-light"><tr><th>Product</th><th class="text-end">Qty</th><th class="text-end">Revenue</th></tr></thead><tbody>
|
||||
<?php foreach ($this->topProducts as $p) : ?><tr><td><?php echo $this->escape($p->product_name ?? $p->sku); ?></td><td class="text-end"><?php echo number_format((float) $p->qty_sold, 0); ?></td><td class="text-end fw-bold">$<?php echo number_format((float) $p->revenue, 0); ?></td></tr><?php endforeach; ?>
|
||||
</tbody></table></div></div></div>
|
||||
</div>
|
||||
<?php elseif ($tab === 'pipeline') : ?>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Open</div><div class="fs-3 fw-bold"><?php echo (int) ($openDeals->cnt ?? 0); ?></div><div class="small">$<?php echo number_format((float) ($openDeals->total_value ?? 0), 0); ?></div></div></div></div>
|
||||
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Won</div><div class="fs-3 fw-bold text-success"><?php echo (int) ($wonDeals->cnt ?? 0); ?></div></div></div></div>
|
||||
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Lost</div><div class="fs-3 fw-bold text-danger"><?php echo (int) ($lostDeals->cnt ?? 0); ?></div></div></div></div>
|
||||
<div class="col-md-3"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Win Rate</div><div class="fs-3 fw-bold"><?php echo $winRate; ?>%</div></div></div></div>
|
||||
</div>
|
||||
<?php elseif ($tab === 'aging') : ?>
|
||||
<?php $buckets = ['current' => 0, '1_30' => 0, '31_60' => 0, '61_90' => 0, 'over_90' => 0];
|
||||
foreach ($this->agingData as $r) { $d = (int) $r->days_overdue; if ($d <= 0) $buckets['current'] += (float) $r->balance; elseif ($d <= 30) $buckets['1_30'] += (float) $r->balance; elseif ($d <= 60) $buckets['31_60'] += (float) $r->balance; elseif ($d <= 90) $buckets['61_90'] += (float) $r->balance; else $buckets['over_90'] += (float) $r->balance; }
|
||||
$totalAging = array_sum($buckets); ?>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">Current</div><div class="fw-bold">$<?php echo number_format($buckets['current'], 0); ?></div></div></div></div>
|
||||
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">1-30 Days</div><div class="fw-bold text-warning">$<?php echo number_format($buckets['1_30'], 0); ?></div></div></div></div>
|
||||
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">31-60</div><div class="fw-bold text-warning">$<?php echo number_format($buckets['31_60'], 0); ?></div></div></div></div>
|
||||
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">61-90</div><div class="fw-bold text-danger">$<?php echo number_format($buckets['61_90'], 0); ?></div></div></div></div>
|
||||
<div class="col"><div class="card shadow-sm"><div class="card-body text-center"><div class="text-muted small">90+</div><div class="fw-bold text-danger">$<?php echo number_format($buckets['over_90'], 0); ?></div></div></div></div>
|
||||
</div>
|
||||
<div class="card shadow-sm"><div class="card-body p-0"><table class="table table-sm table-hover mb-0"><thead class="table-light"><tr><th>Ref</th><th>Contact</th><th class="text-end">Total</th><th class="text-end">Paid</th><th class="text-end">Balance</th><th>Due</th><th>Days</th></tr></thead><tbody>
|
||||
<?php foreach ($this->agingData as $r) : ?>
|
||||
<tr class="<?php echo (int) $r->days_overdue > 60 ? 'table-danger' : ((int) $r->days_overdue > 30 ? 'table-warning' : ''); ?>">
|
||||
<td><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=erpinvoice&id=' . (int) $r->id); ?>" class="font-monospace"><?php echo $this->escape($r->ref); ?></a></td>
|
||||
<td><?php echo $this->escape($r->contact_name ?? '—'); ?></td><td class="text-end">$<?php echo number_format((float) $r->total, 2); ?></td>
|
||||
<td class="text-end">$<?php echo number_format((float) $r->amount_paid, 2); ?></td><td class="text-end fw-bold text-danger">$<?php echo number_format((float) $r->balance, 2); ?></td>
|
||||
<td class="small"><?php echo $this->escape($r->due_date); ?></td><td class="fw-bold"><?php echo (int) $r->days_overdue; ?></td>
|
||||
</tr><?php endforeach; ?>
|
||||
</tbody><tfoot class="table-light"><tr><td colspan="4" class="text-end fw-bold">Total</td><td class="text-end fw-bold text-danger">$<?php echo number_format($totalAging, 2); ?></td><td colspan="2"></td></tr></tfoot></table></div></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* MokoSuite+ERP Customer Portal styles
|
||||
* @since 02.34.16
|
||||
*/
|
||||
|
||||
.mokosuite-portal h2,
|
||||
.mokosuite-portal-orders h2,
|
||||
.mokosuite-portal-invoices h2,
|
||||
.mokosuite-portal-license h2 {
|
||||
color: #1a2744;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Signing page */
|
||||
.mokosuite-sign-page {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#signature-canvas {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Verification page */
|
||||
.mokosuite-verify-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Portal cards */
|
||||
.mokosuite-portal .card {
|
||||
transition: box-shadow 0.15s;
|
||||
}
|
||||
.mokosuite-portal .card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* MokoSuite+ERP Signature Pad — HTML5 Canvas drawing for e-signature capture.
|
||||
* Touch-friendly, works on mobile/tablet/desktop.
|
||||
* @since 02.34.16
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
'use strict';
|
||||
|
||||
var canvas = document.getElementById('signature-canvas');
|
||||
if (!canvas) { return; }
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
var drawing = false;
|
||||
var hasSigned = false;
|
||||
|
||||
// High-DPI support
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
var rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = '#000';
|
||||
|
||||
function getPos(e) {
|
||||
var r = canvas.getBoundingClientRect();
|
||||
var touch = e.touches ? e.touches[0] : e;
|
||||
return { x: touch.clientX - r.left, y: touch.clientY - r.top };
|
||||
}
|
||||
|
||||
canvas.addEventListener('mousedown', function (e) { drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); });
|
||||
canvas.addEventListener('mousemove', function (e) { if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); hasSigned = true; });
|
||||
canvas.addEventListener('mouseup', function () { drawing = false; });
|
||||
canvas.addEventListener('mouseleave', function () { drawing = false; });
|
||||
|
||||
canvas.addEventListener('touchstart', function (e) { e.preventDefault(); drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); }, { passive: false });
|
||||
canvas.addEventListener('touchmove', function (e) { e.preventDefault(); if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); hasSigned = true; }, { passive: false });
|
||||
canvas.addEventListener('touchend', function () { drawing = false; });
|
||||
|
||||
// Clear
|
||||
var clearBtn = document.getElementById('clear-signature');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', function () {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
hasSigned = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Submit
|
||||
var form = document.getElementById('signing-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!hasSigned) {
|
||||
alert('Please draw your signature before submitting.');
|
||||
return;
|
||||
}
|
||||
|
||||
var consentBox = document.getElementById('consent-checkbox');
|
||||
if (consentBox && !consentBox.checked) {
|
||||
alert('You must accept the e-signature consent agreement.');
|
||||
return;
|
||||
}
|
||||
|
||||
var token = form.dataset.token;
|
||||
var signatureData = canvas.toDataURL('image/png');
|
||||
var basePath = (Joomla.getOptions('system.paths') || {}).baseFull || '';
|
||||
|
||||
var body = {
|
||||
token: token,
|
||||
signature: signatureData,
|
||||
signature_type: 'draw'
|
||||
};
|
||||
|
||||
// Geolocation
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
body.geo_lat = pos.coords.latitude;
|
||||
body.geo_lon = pos.coords.longitude;
|
||||
submitSignature(basePath, body);
|
||||
}, function () {
|
||||
submitSignature(basePath, body);
|
||||
}, { timeout: 5000 });
|
||||
} else {
|
||||
submitSignature(basePath, body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitSignature(basePath, body) {
|
||||
var btn = document.getElementById('btn-sign');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Submitting...';
|
||||
|
||||
// If consent needed, send consent first
|
||||
var consentBox = document.getElementById('consent-checkbox');
|
||||
var consentPromise = Promise.resolve();
|
||||
|
||||
if (consentBox) {
|
||||
consentPromise = fetch(basePath + 'api/index.php/v1/mokosuite/erp/esign/public', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: body.token, accepted: true, action: 'consent' })
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
consentPromise.then(function () {
|
||||
return fetch(basePath + 'api/index.php/v1/mokosuite/erp/esign/public', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (result) {
|
||||
if (result.ok) {
|
||||
document.querySelector('.mokosuite-sign-page').textContent = '';
|
||||
var success = document.createElement('div');
|
||||
success.className = 'alert alert-success fs-5 text-center py-5';
|
||||
success.textContent = 'Document signed successfully. Thank you!';
|
||||
document.querySelector('.mokosuite-sign-page').appendChild(success);
|
||||
} else {
|
||||
alert(result.error || 'Signing failed. Please try again.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Sign Document';
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
alert('Network error: ' + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Sign Document';
|
||||
});
|
||||
}
|
||||
|
||||
// Decline
|
||||
var declineBtn = document.getElementById('btn-decline');
|
||||
if (declineBtn) {
|
||||
declineBtn.addEventListener('click', function () {
|
||||
var reason = prompt('Reason for declining (optional):');
|
||||
if (reason === null) { return; }
|
||||
|
||||
var token = form.dataset.token;
|
||||
var basePath = (Joomla.getOptions('system.paths') || {}).baseFull || '';
|
||||
|
||||
fetch(basePath + 'api/index.php/v1/mokosuite/erp/esign/public', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: token, reason: reason, action: 'decline' })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (result) {
|
||||
document.querySelector('.mokosuite-sign-page').textContent = '';
|
||||
var msg = document.createElement('div');
|
||||
msg.className = 'alert alert-warning fs-5 text-center py-5';
|
||||
msg.textContent = 'Document declined.';
|
||||
document.querySelector('.mokosuite-sign-page').appendChild(msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,92 +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
|
||||
|
||||
FILE INFORMATION
|
||||
DEFGROUP: Joomla.Component
|
||||
INGROUP: MokoSuite
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuite
|
||||
VERSION: 02.34.16
|
||||
PATH: /mokosuite.xml
|
||||
BRIEF: Component manifest for MokoSuite admin dashboard and REST API
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>MokoSuite</name>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.34.50-dev</version>
|
||||
<description>MokoSuite admin dashboard and REST API. Provides a control panel for managing MokoSuite feature plugins, site health monitoring, and remote management endpoints.</description>
|
||||
|
||||
<namespace path="src">Moko\Component\MokoSuite</namespace>
|
||||
|
||||
<install>
|
||||
<sql><file driver="mysql" charset="utf8">sql/install.mysql.sql</file></sql>
|
||||
</install>
|
||||
<uninstall>
|
||||
<sql><file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file></sql>
|
||||
</uninstall>
|
||||
<update>
|
||||
<schemas>
|
||||
<schemapath type="mysql">sql/updates/mysql</schemapath>
|
||||
</schemas>
|
||||
</update>
|
||||
|
||||
<administration>
|
||||
<menu img="class:cogs">MokoSuite</menu>
|
||||
<submenu>
|
||||
<menu link="option=com_mokosuite" img="class:cogs">COM_MOKOSUITE_MENU_DASHBOARD</menu>
|
||||
<menu link="option=com_mokosuite&view=extensions" img="class:puzzle-piece">COM_MOKOSUITE_MENU_EXTENSIONS</menu>
|
||||
<menu link="option=com_mokosuite&view=tickets" img="class:headphones">COM_MOKOSUITE_MENU_TICKETS</menu>
|
||||
<menu link="option=com_mokosuite&view=htaccess" img="class:file-code">COM_MOKOSUITE_MENU_HTACCESS</menu>
|
||||
<menu link="option=com_mokosuite&view=privacy" img="class:lock">COM_MOKOSUITE_MENU_PRIVACY</menu>
|
||||
<menu link="option=com_mokosuite&view=waflog" img="class:shield-alt">COM_MOKOSUITE_MENU_WAFLOG</menu>
|
||||
<menu link="option=com_mokosuite&view=database" img="class:database">COM_MOKOSUITE_MENU_DATABASE</menu>
|
||||
<menu link="option=com_mokosuite&view=cleanup" img="class:trash">COM_MOKOSUITE_MENU_CLEANUP</menu>
|
||||
<menu link="option=com_plugins&filter[folder]=system&filter[search]=mokosuite" img="class:power-off">COM_MOKOSUITE_MENU_PLUGINS</menu>
|
||||
<menu link="option=com_installer&view=update" img="class:refresh">COM_MOKOSUITE_MENU_UPDATES</menu>
|
||||
<menu link="option=com_checkin" img="class:check-square">COM_MOKOSUITE_MENU_CHECKIN</menu>
|
||||
<menu link="option=com_cache" img="class:bolt">COM_MOKOSUITE_MENU_CACHE</menu>
|
||||
</submenu>
|
||||
<files folder="admin">
|
||||
<filename>access.xml</filename>
|
||||
<filename>catalog.xml</filename>
|
||||
<filename>config.xml</filename>
|
||||
<folder>language</folder>
|
||||
<folder>services</folder>
|
||||
<folder>sql</folder>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
</files>
|
||||
<languages folder="admin/language">
|
||||
<language tag="en-GB">en-GB/com_mokosuite.sys.ini</language>
|
||||
</languages>
|
||||
</administration>
|
||||
|
||||
<files folder="site">
|
||||
<folder>language</folder>
|
||||
<folder>services</folder>
|
||||
<folder>src</folder>
|
||||
<folder>tmpl</folder>
|
||||
</files>
|
||||
|
||||
<install>
|
||||
<sql><file driver="mysql" charset="utf8">admin/sql/install.mysql.sql</file></sql>
|
||||
</install>
|
||||
|
||||
<api>
|
||||
<files folder="api">
|
||||
<folder>src</folder>
|
||||
</files>
|
||||
</api>
|
||||
|
||||
<media destination="com_mokosuite" folder="media">
|
||||
<folder>css</folder>
|
||||
<folder>js</folder>
|
||||
</media>
|
||||
</extension>
|
||||
@@ -1,11 +0,0 @@
|
||||
; MokoSuite Customer Portal - Language Strings
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
COM_MOKOSUITE_PORTAL_TITLE="Support Portal"
|
||||
COM_MOKOSUITE_PORTAL_MY_TICKETS="My Support Tickets"
|
||||
COM_MOKOSUITE_PORTAL_NEW_TICKET="New Ticket"
|
||||
COM_MOKOSUITE_PORTAL_SUBMIT="Submit Ticket"
|
||||
COM_MOKOSUITE_PORTAL_REPLY="Send Reply"
|
||||
COM_MOKOSUITE_PORTAL_NO_TICKETS="You haven't submitted any support tickets yet."
|
||||
COM_MOKOSUITE_PORTAL_LOGIN_REQUIRED="Please log in to access the support portal."
|
||||
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
|
||||
/**
|
||||
* Portal Model — resolves logged-in user to ERP contact and loads their data.
|
||||
*/
|
||||
class PortalModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* Get the ERP contact ID for the current logged-in user (matched by email).
|
||||
*/
|
||||
public function getContactId(): int
|
||||
{
|
||||
$user = Factory::getUser();
|
||||
if ($user->guest) { return 0; }
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__contact_details'))
|
||||
->where($db->quoteName('email_to') . ' = ' . $db->quote($user->email))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->setLimit(1)
|
||||
);
|
||||
|
||||
return (int) $db->loadResult();
|
||||
}
|
||||
|
||||
public function getDashboard(int $contactId): object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$dash = new \stdClass();
|
||||
|
||||
// Open orders
|
||||
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__mokosuite_erp_orders'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->where($db->quoteName('status') . ' NOT IN (' . $db->quote('delivered') . ',' . $db->quote('cancelled') . ')'));
|
||||
$dash->open_orders = (int) $db->loadResult();
|
||||
|
||||
// Unpaid invoices
|
||||
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->select('COALESCE(SUM(total - amount_paid), 0) AS total_due')->from($db->quoteName('#__mokosuite_erp_invoices'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->where($db->quoteName('status') . ' IN (' . $db->quote('sent') . ',' . $db->quote('partial') . ',' . $db->quote('overdue') . ')'));
|
||||
$inv = $db->loadObject();
|
||||
$dash->unpaid_invoices = (int) $inv->{'COUNT(*)'};
|
||||
$dash->total_due = (float) $inv->total_due;
|
||||
|
||||
// Open tickets
|
||||
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__mokosuite_tickets'))->where($db->quoteName('created_by') . ' = ' . (int) Factory::getUser()->id)->where($db->quoteName('status') . ' NOT IN (' . $db->quote('closed') . ',' . $db->quote('resolved') . ')'));
|
||||
$dash->open_tickets = (int) $db->loadResult();
|
||||
|
||||
// Pending signatures
|
||||
$db->setQuery($db->getQuery(true)->select('COUNT(*)')->from($db->quoteName('#__mokosuite_erp_esign_signers'))->where($db->quoteName('email') . ' = ' . $db->quote(Factory::getUser()->email))->where($db->quoteName('status') . ' IN (' . $db->quote('pending') . ',' . $db->quote('viewed') . ')'));
|
||||
$dash->pending_signatures = (int) $db->loadResult();
|
||||
|
||||
// Recent orders
|
||||
$db->setQuery($db->getQuery(true)->select('id, ref, status, total, created')->from($db->quoteName('#__mokosuite_erp_orders'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->order('created DESC'), 0, 5);
|
||||
$dash->recent_orders = $db->loadObjectList() ?: [];
|
||||
|
||||
return $dash;
|
||||
}
|
||||
|
||||
public function getOrders(int $contactId, int $limit = 25): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)->select('*')->from($db->quoteName('#__mokosuite_erp_orders'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->order('created DESC'), 0, $limit);
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
public function getOrder(int $contactId, int $id): ?object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)->select('*')->from($db->quoteName('#__mokosuite_erp_orders'))->where($db->quoteName('id') . ' = ' . $id)->where($db->quoteName('contact_id') . ' = ' . $contactId));
|
||||
$order = $db->loadObject();
|
||||
if (!$order) { return null; }
|
||||
|
||||
$db->setQuery($db->getQuery(true)->select('oi.*, p.sku')->from($db->quoteName('#__mokosuite_erp_order_items', 'oi'))->join('LEFT', $db->quoteName('#__mokosuite_erp_products', 'p') . ' ON p.id = oi.product_id')->where($db->quoteName('oi.order_id') . ' = ' . $id)->order('oi.position ASC'));
|
||||
$order->items = $db->loadObjectList() ?: [];
|
||||
return $order;
|
||||
}
|
||||
|
||||
public function getInvoices(int $contactId, int $limit = 25): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)->select('*, (total - amount_paid) AS balance_due')->from($db->quoteName('#__mokosuite_erp_invoices'))->where($db->quoteName('contact_id') . ' = ' . $contactId)->order('created DESC'), 0, $limit);
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
public function getInvoice(int $contactId, int $id): ?object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery($db->getQuery(true)->select('*, (total - amount_paid) AS balance_due')->from($db->quoteName('#__mokosuite_erp_invoices'))->where($db->quoteName('id') . ' = ' . $id)->where($db->quoteName('contact_id') . ' = ' . $contactId));
|
||||
$inv = $db->loadObject();
|
||||
if (!$inv) { return null; }
|
||||
|
||||
$db->setQuery($db->getQuery(true)->select('ii.*, p.sku')->from($db->quoteName('#__mokosuite_erp_invoice_items', 'ii'))->join('LEFT', $db->quoteName('#__mokosuite_erp_products', 'p') . ' ON p.id = ii.product_id')->where($db->quoteName('ii.invoice_id') . ' = ' . $id)->order('ii.position ASC'));
|
||||
$inv->items = $db->loadObjectList() ?: [];
|
||||
return $inv;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\Invoice;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $invoice;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$user = Factory::getUser();
|
||||
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
|
||||
|
||||
$model = $this->getModel('Portal');
|
||||
$contactId = $model->getContactId();
|
||||
$this->invoice = $contactId ? $model->getInvoice($contactId, Factory::getApplication()->getInput()->getInt('id', 0)) : null;
|
||||
|
||||
if (!$this->invoice) { throw new \Exception('Invoice not found', 404); }
|
||||
|
||||
Factory::getApplication()->getDocument()->getWebAssetManager()
|
||||
->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\Invoices;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $items = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$user = Factory::getUser();
|
||||
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
|
||||
|
||||
$model = $this->getModel('Portal');
|
||||
$contactId = $model->getContactId();
|
||||
$this->items = $contactId ? $model->getInvoices($contactId) : [];
|
||||
|
||||
Factory::getApplication()->getDocument()->getWebAssetManager()
|
||||
->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\License;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $licenseData;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$user = Factory::getUser();
|
||||
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
|
||||
|
||||
// License data would come from plg_system_mokosuite_license cache
|
||||
// For now, placeholder structure
|
||||
$this->licenseData = (object) [
|
||||
'valid' => true,
|
||||
'package' => 'MokoSuite+ERP',
|
||||
'services' => ['base', 'erp'],
|
||||
'expiry' => null,
|
||||
'dlid' => '',
|
||||
];
|
||||
|
||||
Factory::getApplication()->getDocument()->getWebAssetManager()
|
||||
->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\Order;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $order;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$user = Factory::getUser();
|
||||
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
|
||||
|
||||
$model = $this->getModel('Portal');
|
||||
$contactId = $model->getContactId();
|
||||
$this->order = $contactId ? $model->getOrder($contactId, Factory::getApplication()->getInput()->getInt('id', 0)) : null;
|
||||
|
||||
if (!$this->order) { throw new \Exception('Order not found', 404); }
|
||||
|
||||
Factory::getApplication()->getDocument()->getWebAssetManager()
|
||||
->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\Orders;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $items = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$user = Factory::getUser();
|
||||
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
|
||||
|
||||
$model = $this->getModel('Portal');
|
||||
$contactId = $model->getContactId();
|
||||
$this->items = $contactId ? $model->getOrders($contactId) : [];
|
||||
|
||||
Factory::getApplication()->getDocument()->getWebAssetManager()
|
||||
->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\Portal;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $dashboard;
|
||||
protected $contactId = 0;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$user = Factory::getUser();
|
||||
if ($user->guest) { Factory::getApplication()->redirect('index.php?option=com_users&view=login'); return; }
|
||||
|
||||
$model = $this->getModel();
|
||||
$this->contactId = $model->getContactId();
|
||||
$this->dashboard = $this->contactId ? $model->getDashboard($this->contactId) : null;
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\Sign;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
/**
|
||||
* Public signing page — token-based, no login required.
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $signer;
|
||||
protected $request;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$token = Factory::getApplication()->getInput()->get('token', '', 'ALNUM');
|
||||
|
||||
if ($token && \strlen($token) === 128)
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('s.*, r.title AS request_title, r.description AS request_description, r.status AS request_status, r.require_selfie, r.require_id, r.require_consent')
|
||||
->from($db->quoteName('#__mokosuite_erp_esign_signers', 's'))
|
||||
->join('INNER', $db->quoteName('#__mokosuite_erp_esign_requests', 'r') . ' ON r.id = s.request_id')
|
||||
->where($db->quoteName('s.token') . ' = ' . $db->quote($token))
|
||||
);
|
||||
$this->signer = $db->loadObject();
|
||||
$this->request = $this->signer ? (object) ['title' => $this->signer->request_title, 'description' => $this->signer->request_description, 'status' => $this->signer->request_status] : null;
|
||||
}
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
$wa->registerAndUseScript('com_mokosuite.signature-pad', 'com_mokosuite/signature-pad.js', [], ['defer' => true]);
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Site\View\SignVerify;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $request;
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$hash = Factory::getApplication()->getInput()->get('hash', '', 'ALNUM');
|
||||
|
||||
if ($hash) {
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->select('id, ref, title, status, date_creation, date_signature')->from($db->quoteName('#__mokosuite_erp_esign_requests'))->where($db->quoteName('verification_hash') . ' = ' . $db->quote($hash)));
|
||||
$this->request = $db->loadObject();
|
||||
|
||||
if ($this->request) {
|
||||
$db->setQuery($db->getQuery(true)->select('role, email, firstname, lastname, status, date_signed, ip_address, geo_country, geo_city')->from($db->quoteName('#__mokosuite_erp_esign_signers'))->where($db->quoteName('request_id') . ' = ' . (int) $this->request->id)->order('position ASC'));
|
||||
$this->request->signers = $db->loadObjectList() ?: [];
|
||||
|
||||
$db->setQuery($db->getQuery(true)->select('code, label, ip, created')->from($db->quoteName('#__mokosuite_erp_esign_events'))->where($db->quoteName('request_id') . ' = ' . (int) $this->request->id)->order('created ASC'));
|
||||
$this->request->events = $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
Factory::getApplication()->getDocument()->getWebAssetManager()
|
||||
->registerAndUseStyle('com_mokosuite.portal', 'com_mokosuite/portal.css');
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Router\Route;
|
||||
$inv = $this->invoice;
|
||||
?>
|
||||
<div class="mokosuite-portal-invoice">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=invoices'); ?>" class="btn btn-sm btn-outline-secondary mb-3"><span class="icon-arrow-left"></span> My Invoices</a>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<h3 class="mb-0 font-monospace"><?php echo $this->escape($inv->ref); ?></h3>
|
||||
<span class="badge bg-<?php echo $inv->status === 'paid' ? 'success' : 'primary'; ?> fs-6"><?php echo ucfirst($inv->status); ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4"><div class="text-muted small">Due Date</div><div class="fw-bold"><?php echo $this->escape($inv->due_date ?? 'On receipt'); ?></div></div>
|
||||
<div class="col-md-4"><div class="text-muted small">Balance Due</div><div class="fs-4 fw-bold <?php echo (float) $inv->balance_due > 0 ? 'text-danger' : 'text-success'; ?>">$<?php echo number_format((float) $inv->balance_due, 2); ?></div></div>
|
||||
<div class="col-md-4"><div class="text-muted small">Created</div><div><?php echo $this->escape($inv->created); ?></div></div>
|
||||
</div>
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light"><tr><th>Description</th><th class="text-end">Qty</th><th class="text-end">Price</th><th class="text-end">Total</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($inv->items as $item) : ?>
|
||||
<tr>
|
||||
<td><?php echo $this->escape($item->description); ?></td>
|
||||
<td class="text-end"><?php echo number_format((float) $item->quantity, 2); ?></td>
|
||||
<td class="text-end">$<?php echo number_format((float) $item->unit_price, 2); ?></td>
|
||||
<td class="text-end fw-bold">$<?php echo number_format((float) $item->line_total, 2); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr><td colspan="3" class="text-end">Subtotal</td><td class="text-end">$<?php echo number_format((float) $inv->subtotal, 2); ?></td></tr>
|
||||
<tr><td colspan="3" class="text-end">Tax</td><td class="text-end">$<?php echo number_format((float) $inv->tax_total, 2); ?></td></tr>
|
||||
<tr><td colspan="3" class="text-end fw-bold fs-5">Total</td><td class="text-end fw-bold fs-5">$<?php echo number_format((float) $inv->total, 2); ?></td></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Router\Route;
|
||||
?>
|
||||
<div class="mokosuite-portal-invoices">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h2>My Invoices</h2>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=portal'); ?>" class="btn btn-sm btn-outline-secondary"><span class="icon-arrow-left"></span> Portal</a>
|
||||
</div>
|
||||
<div class="card shadow-sm"><div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light"><tr><th>Ref</th><th>Status</th><th class="text-end">Total</th><th class="text-end">Paid</th><th class="text-end">Balance</th><th>Due</th><th>Date</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($this->items as $inv) :
|
||||
$isOverdue = $inv->due_date && $inv->due_date < date('Y-m-d') && \in_array($inv->status, ['sent', 'partial', 'overdue']);
|
||||
?>
|
||||
<tr class="<?php echo $isOverdue ? 'table-warning' : ''; ?>">
|
||||
<td><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=invoice&id=' . (int) $inv->id); ?>" class="font-monospace fw-bold"><?php echo $this->escape($inv->ref); ?></a></td>
|
||||
<td><span class="badge bg-<?php echo $inv->status === 'paid' ? 'success' : ($isOverdue ? 'danger' : 'primary'); ?>"><?php echo ucfirst($inv->status); ?></span></td>
|
||||
<td class="text-end">$<?php echo number_format((float) $inv->total, 2); ?></td>
|
||||
<td class="text-end text-success">$<?php echo number_format((float) $inv->amount_paid, 2); ?></td>
|
||||
<td class="text-end <?php echo (float) $inv->balance_due > 0 ? 'text-danger fw-bold' : ''; ?>">$<?php echo number_format((float) $inv->balance_due, 2); ?></td>
|
||||
<td class="small <?php echo $isOverdue ? 'text-danger fw-bold' : 'text-muted'; ?>"><?php echo $this->escape($inv->due_date ?? '—'); ?></td>
|
||||
<td class="small text-muted"><?php echo $this->escape($inv->created); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($this->items)) : ?><tr><td colspan="7" class="text-center text-muted py-4">No invoices found</td></tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></div>
|
||||
</div>
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
$lic = $this->licenseData;
|
||||
?>
|
||||
<div class="mokosuite-portal-license">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h2>License & Subscription</h2>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=portal'); ?>" class="btn btn-sm btn-outline-secondary"><span class="icon-arrow-left"></span> Portal</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header"><h5 class="mb-0">Current License</h5></div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="text-muted small">Package</div>
|
||||
<div class="fs-5 fw-bold"><?php echo $this->escape($lic->package); ?></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-muted small">Status</div>
|
||||
<div><span class="badge bg-<?php echo $lic->valid ? 'success' : 'danger'; ?> fs-6"><?php echo $lic->valid ? 'Active' : 'Invalid'; ?></span></div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-muted small">Expires</div>
|
||||
<div class="fw-bold"><?php echo $lic->expiry ? $this->escape($lic->expiry) : 'No expiry'; ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($lic->services)) : ?>
|
||||
<hr>
|
||||
<div class="text-muted small mb-2">Active Services</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<?php foreach ($lic->services as $svc) : ?>
|
||||
<span class="badge bg-primary"><?php echo strtoupper($this->escape($svc)); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><h5 class="mb-0">Update License Key</h5></div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="<?php echo Route::_('index.php?option=com_mokosuite&task=saveLicense'); ?>">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Download Key (DLID)</label>
|
||||
<input type="text" name="dlid" class="form-control font-monospace" placeholder="Enter your license key" value="<?php echo $this->escape($lic->dlid); ?>">
|
||||
<div class="form-text">Enter or update your license key to activate features.</div>
|
||||
</div>
|
||||
<input type="hidden" name="<?php echo Session::getFormToken(); ?>" value="1">
|
||||
<button type="submit" class="btn btn-primary"><span class="icon-key"></span> Save License Key</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Router\Route;
|
||||
$o = $this->order;
|
||||
?>
|
||||
<div class="mokosuite-portal-order">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=orders'); ?>" class="btn btn-sm btn-outline-secondary mb-3"><span class="icon-arrow-left"></span> My Orders</a>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<h3 class="mb-0 font-monospace"><?php echo $this->escape($o->ref); ?></h3>
|
||||
<span class="badge bg-primary fs-6"><?php echo ucfirst($o->status); ?></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light"><tr><th>SKU</th><th>Description</th><th class="text-end">Qty</th><th class="text-end">Price</th><th class="text-end">Total</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($o->items as $item) : ?>
|
||||
<tr>
|
||||
<td class="font-monospace small"><?php echo $this->escape($item->sku ?? ''); ?></td>
|
||||
<td><?php echo $this->escape($item->description); ?></td>
|
||||
<td class="text-end"><?php echo number_format((float) $item->quantity, 2); ?></td>
|
||||
<td class="text-end">$<?php echo number_format((float) $item->unit_price, 2); ?></td>
|
||||
<td class="text-end fw-bold">$<?php echo number_format((float) $item->line_total, 2); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr><td colspan="4" class="text-end">Subtotal</td><td class="text-end">$<?php echo number_format((float) $o->subtotal, 2); ?></td></tr>
|
||||
<tr><td colspan="4" class="text-end">Tax</td><td class="text-end">$<?php echo number_format((float) $o->tax_total, 2); ?></td></tr>
|
||||
<tr><td colspan="4" class="text-end fw-bold fs-5">Total</td><td class="text-end fw-bold fs-5">$<?php echo number_format((float) $o->total, 2); ?></td></tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Router\Route;
|
||||
?>
|
||||
<div class="mokosuite-portal-orders">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h2>My Orders</h2>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=portal'); ?>" class="btn btn-sm btn-outline-secondary"><span class="icon-arrow-left"></span> Portal</a>
|
||||
</div>
|
||||
<div class="card shadow-sm"><div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light"><tr><th>Ref</th><th>Status</th><th>Payment</th><th class="text-end">Total</th><th>Date</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($this->items as $order) : ?>
|
||||
<tr>
|
||||
<td><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=order&id=' . (int) $order->id); ?>" class="font-monospace fw-bold"><?php echo $this->escape($order->ref); ?></a></td>
|
||||
<td><span class="badge bg-primary"><?php echo ucfirst($order->status); ?></span></td>
|
||||
<td><span class="badge bg-<?php echo $order->payment_status === 'paid' ? 'success' : 'warning'; ?>"><?php echo ucfirst($order->payment_status); ?></span></td>
|
||||
<td class="text-end fw-bold">$<?php echo number_format((float) $order->total, 2); ?></td>
|
||||
<td class="small text-muted"><?php echo $this->escape($order->created); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($this->items)) : ?><tr><td colspan="5" class="text-center text-muted py-4">No orders found</td></tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></div>
|
||||
</div>
|
||||
@@ -1,96 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
$dash = $this->dashboard;
|
||||
$user = \Joomla\CMS\Factory::getUser();
|
||||
?>
|
||||
|
||||
<div class="mokosuite-portal">
|
||||
<h2 class="mb-4">Welcome, <?php echo $this->escape($user->name); ?></h2>
|
||||
|
||||
<?php if (!$this->contactId) : ?>
|
||||
<div class="alert alert-warning">Your account is not linked to an ERP contact. Please contact support.</div>
|
||||
<?php else : ?>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fs-3 fw-bold"><?php echo (int) $dash->open_orders; ?></div>
|
||||
<div class="small text-muted">Open Orders</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=orders'); ?>" class="btn btn-sm btn-outline-primary w-100">View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm <?php echo (int) $dash->unpaid_invoices > 0 ? 'border-warning' : ''; ?>">
|
||||
<div class="card-body">
|
||||
<div class="fs-3 fw-bold <?php echo (float) $dash->total_due > 0 ? 'text-warning' : ''; ?>"><?php echo (int) $dash->unpaid_invoices; ?></div>
|
||||
<div class="small text-muted">Unpaid Invoices</div>
|
||||
<?php if ((float) $dash->total_due > 0) : ?><div class="small fw-bold text-warning">$<?php echo number_format($dash->total_due, 2); ?> due</div><?php endif; ?>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=invoices'); ?>" class="btn btn-sm btn-outline-warning w-100">View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fs-3 fw-bold"><?php echo (int) $dash->open_tickets; ?></div>
|
||||
<div class="small text-muted">Open Tickets</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=tickets'); ?>" class="btn btn-sm btn-outline-primary w-100">View</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center border-0 shadow-sm <?php echo (int) $dash->pending_signatures > 0 ? 'border-info' : ''; ?>">
|
||||
<div class="card-body">
|
||||
<div class="fs-3 fw-bold"><?php echo (int) $dash->pending_signatures; ?></div>
|
||||
<div class="small text-muted">Pending Signatures</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=sign'); ?>" class="btn btn-sm btn-outline-info w-100">Sign</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Orders -->
|
||||
<?php if (!empty($dash->recent_orders)) : ?>
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<h5 class="mb-0">Recent Orders</h5>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuite&view=orders'); ?>" class="btn btn-sm btn-outline-primary">View All</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light"><tr><th>Ref</th><th>Status</th><th class="text-end">Total</th><th>Date</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($dash->recent_orders as $order) : ?>
|
||||
<tr>
|
||||
<td><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=order&id=' . (int) $order->id); ?>" class="font-monospace"><?php echo $this->escape($order->ref); ?></a></td>
|
||||
<td><span class="badge bg-primary"><?php echo ucfirst($order->status); ?></span></td>
|
||||
<td class="text-end fw-bold">$<?php echo number_format((float) $order->total, 2); ?></td>
|
||||
<td class="small text-muted"><?php echo $this->escape($order->created); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=license'); ?>" class="btn btn-outline-secondary w-100"><span class="icon-key"></span> License & Subscription</a></div>
|
||||
<div class="col-md-4"><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=tickets&layout=submit'); ?>" class="btn btn-outline-secondary w-100"><span class="icon-life-ring"></span> Submit Ticket</a></div>
|
||||
<div class="col-md-4"><a href="<?php echo Route::_('index.php?option=com_mokosuite&view=invoices'); ?>" class="btn btn-outline-secondary w-100"><span class="icon-file-invoice"></span> My Invoices</a></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
$signer = $this->signer;
|
||||
$request = $this->request;
|
||||
$token = \Joomla\CMS\Factory::getApplication()->getInput()->get('token', '', 'ALNUM');
|
||||
?>
|
||||
|
||||
<div class="mokosuite-sign-page">
|
||||
<?php if (!$signer) : ?>
|
||||
<div class="alert alert-danger">Invalid or expired signing link.</div>
|
||||
<?php elseif ($signer->status === 'signed') : ?>
|
||||
<div class="alert alert-success"><strong>Already signed.</strong> You have already signed this document on <?php echo $this->escape($signer->date_signed); ?>.</div>
|
||||
<?php elseif (!\in_array($request->status, ['pending', 'inprogress'])) : ?>
|
||||
<div class="alert alert-warning">This signing request is no longer active (status: <?php echo $this->escape($request->status); ?>).</div>
|
||||
<?php else : ?>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header"><h3 class="mb-0"><?php echo $this->escape($request->title); ?></h3></div>
|
||||
<div class="card-body">
|
||||
<?php if ($request->description) : ?>
|
||||
<div class="border rounded p-3 mb-3 bg-light" style="max-height:400px;overflow-y:auto;">
|
||||
<?php echo nl2br($this->escape($request->description)); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form id="signing-form" data-token="<?php echo $this->escape($token); ?>">
|
||||
<!-- Consent -->
|
||||
<?php if ($signer->require_consent && !$signer->consent_accepted) : ?>
|
||||
<div class="alert alert-info">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="consent-checkbox" class="form-check-input" required>
|
||||
<label for="consent-checkbox" class="form-check-label">
|
||||
I agree to use electronic signatures and understand this is legally binding.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Signature Pad -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Your Signature</label>
|
||||
<div class="border rounded p-2 bg-white">
|
||||
<canvas id="signature-canvas" width="600" height="200" style="width:100%;height:200px;cursor:crosshair;touch-action:none;"></canvas>
|
||||
</div>
|
||||
<button type="button" id="clear-signature" class="btn btn-sm btn-outline-secondary mt-1">Clear</button>
|
||||
</div>
|
||||
|
||||
<!-- Optional Selfie -->
|
||||
<?php if ($signer->require_selfie) : ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Selfie Verification</label>
|
||||
<div><button type="button" id="btn-selfie" class="btn btn-outline-info btn-sm"><span class="icon-camera"></span> Take Selfie</button></div>
|
||||
<canvas id="selfie-preview" class="mt-2 d-none border rounded" width="320" height="240"></canvas>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Optional ID Photo -->
|
||||
<?php if ($signer->require_id) : ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">ID Verification</label>
|
||||
<div><button type="button" id="btn-id-photo" class="btn btn-outline-info btn-sm"><span class="icon-id-card"></span> Take ID Photo</button></div>
|
||||
<canvas id="id-preview" class="mt-2 d-none border rounded" width="320" height="240"></canvas>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" id="btn-sign" class="btn btn-success btn-lg flex-grow-1"><span class="icon-pen-nib"></span> Sign Document</button>
|
||||
<button type="button" id="btn-decline" class="btn btn-outline-danger">Decline</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
defined('_JEXEC') or die;
|
||||
$req = $this->request;
|
||||
?>
|
||||
<div class="mokosuite-verify-page">
|
||||
<?php if (!$req) : ?>
|
||||
<div class="alert alert-danger">Verification not found. Check the verification link.</div>
|
||||
<?php else : ?>
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header text-center">
|
||||
<h3 class="mb-0">Certificate of Verification</h3>
|
||||
<div class="small text-muted">Electronic Signature Verification</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-4">
|
||||
<?php if ($req->status === 'completed') : ?>
|
||||
<div class="alert alert-success fs-5"><span class="icon-check-circle"></span> This document has been fully signed and is legally valid.</div>
|
||||
<?php else : ?>
|
||||
<div class="alert alert-warning">Status: <?php echo ucfirst($req->status); ?> — this document has not been fully signed.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4"><div class="text-muted small">Reference</div><div class="font-monospace fw-bold"><?php echo $this->escape($req->ref); ?></div></div>
|
||||
<div class="col-md-4"><div class="text-muted small">Title</div><div><?php echo $this->escape($req->title); ?></div></div>
|
||||
<div class="col-md-4"><div class="text-muted small">Completed</div><div><?php echo $this->escape($req->date_signature ?? 'Pending'); ?></div></div>
|
||||
</div>
|
||||
|
||||
<h5>Signers</h5>
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light"><tr><th>Name</th><th>Email</th><th>Status</th><th>Signed</th><th>Location</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($req->signers as $s) :
|
||||
$name = trim(($s->firstname ?? '') . ' ' . ($s->lastname ?? '')) ?: '—';
|
||||
$loc = implode(', ', array_filter([$s->geo_city ?? '', $s->geo_country ?? ''])) ?: '—';
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo $this->escape($name); ?></td>
|
||||
<td class="small"><?php echo $this->escape($s->email); ?></td>
|
||||
<td><span class="badge bg-<?php echo $s->status === 'signed' ? 'success' : ($s->status === 'declined' ? 'danger' : 'warning'); ?>"><?php echo ucfirst($s->status); ?></span></td>
|
||||
<td class="small"><?php echo $this->escape($s->date_signed ?? '—'); ?></td>
|
||||
<td class="small"><?php echo $this->escape($loc); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5 class="mt-4">Audit Trail</h5>
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light"><tr><th>Time</th><th>Event</th><th>Description</th><th>IP</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($req->events as $ev) : ?>
|
||||
<tr>
|
||||
<td class="small text-muted"><?php echo $this->escape($ev->created); ?></td>
|
||||
<td><span class="badge bg-info"><?php echo $this->escape($ev->code); ?></span></td>
|
||||
<td class="small"><?php echo $this->escape($ev->label); ?></td>
|
||||
<td class="font-monospace small"><?php echo $this->escape($ev->ip ?? '—'); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<access component="com_mokowaas">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" description="JACTION_ADMIN_COMPONENT_DESC" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" description="JACTION_MANAGE_COMPONENT_DESC" />
|
||||
<action name="mokowaas.dashboard" title="COM_MOKOWAAS_ACL_DASHBOARD" description="COM_MOKOWAAS_ACL_DASHBOARD_DESC" />
|
||||
<action name="mokowaas.extensions" title="COM_MOKOWAAS_ACL_EXTENSIONS" description="COM_MOKOWAAS_ACL_EXTENSIONS_DESC" />
|
||||
<action name="mokowaas.htaccess" title="COM_MOKOWAAS_ACL_HTACCESS" description="COM_MOKOWAAS_ACL_HTACCESS_DESC" />
|
||||
<action name="mokowaas.tickets" title="COM_MOKOWAAS_ACL_TICKETS" description="COM_MOKOWAAS_ACL_TICKETS_DESC" />
|
||||
<action name="mokowaas.tickets.create" title="COM_MOKOWAAS_ACL_TICKETS_CREATE" description="COM_MOKOWAAS_ACL_TICKETS_CREATE_DESC" />
|
||||
<action name="mokowaas.tickets.assign" title="COM_MOKOWAAS_ACL_TICKETS_ASSIGN" description="COM_MOKOWAAS_ACL_TICKETS_ASSIGN_DESC" />
|
||||
<action name="mokowaas.plugins.toggle" title="COM_MOKOWAAS_ACL_PLUGINS_TOGGLE" description="COM_MOKOWAAS_ACL_PLUGINS_TOGGLE_DESC" />
|
||||
<action name="mokowaas.cache" title="COM_MOKOWAAS_ACL_CACHE" description="COM_MOKOWAAS_ACL_CACHE_DESC" />
|
||||
</section>
|
||||
</access>
|
||||
+33
-63
@@ -1,63 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Extension catalog for MokoSuite Extension Manager.
|
||||
Each entry points to the extension's own updates.xml. The installer
|
||||
resolves the latest version and download URL at runtime, respecting
|
||||
the site's configured update channel (dev/stable).
|
||||
Extension catalog for MokoWaaS Extension Manager.
|
||||
Each entry points to the extension's own updates.xml — the installer
|
||||
resolves the latest version and download URL at runtime.
|
||||
|
||||
To add an extension: copy an <extension> block and fill in the fields.
|
||||
The updateserver URL must point to a valid Joomla updates.xml file.
|
||||
-->
|
||||
<catalog>
|
||||
<extension>
|
||||
<name>MokoSuite</name>
|
||||
<element>pkg_mokosuite</element>
|
||||
<name>MokoWaaS</name>
|
||||
<element>pkg_mokowaas</element>
|
||||
<type>package</type>
|
||||
<description>Admin dashboard, security firewall, tenant restrictions, health monitoring, and REST API.</description>
|
||||
<icon>icon-shield-alt</icon>
|
||||
<category>Platform</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokosuite-platform</article>
|
||||
<article>https://mokoconsulting.tech/support/products/mokowaas-platform</article>
|
||||
<protected>true</protected>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuite/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoSuiteHQ</name>
|
||||
<element>pkg_mokosuitehq</element>
|
||||
<type>package</type>
|
||||
<description>Centralized control panel for managing all MokoSuite client installations.</description>
|
||||
<icon>icon-tachometer-alt</icon>
|
||||
<category>Platform</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokosuite-base</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteHQ/raw/branch/dev/updates.xml</updateserver>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoOnyx</name>
|
||||
<element>mokoonyx</element>
|
||||
<type>template</type>
|
||||
<description>Modern Joomla site template with dark mode, custom layouts, and MokoSuite integration.</description>
|
||||
<description>Modern Joomla site template with dark mode, custom layouts, and MokoWaaS integration.</description>
|
||||
<icon>icon-paint-brush</icon>
|
||||
<category>Templates</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokoonyx-template</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoJoomOpenGraph</name>
|
||||
<element>pkg_mokoog</element>
|
||||
<type>package</type>
|
||||
<description>Open Graph, Twitter Card, and social sharing meta tags for articles, categories, and pages.</description>
|
||||
<icon>icon-share-alt</icon>
|
||||
<category>SEO</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoJoomBackup</name>
|
||||
<element>pkg_mokojoombackup</element>
|
||||
<type>package</type>
|
||||
<description>Automated backup system with Borg integration, scheduled tasks, and remote storage.</description>
|
||||
<icon>icon-archive</icon>
|
||||
<category>Tools</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokojoombackup</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/dev/updates.xml</updateserver>
|
||||
<name>MokoJoomTOS</name>
|
||||
<element>com_mokojoomtos</element>
|
||||
<type>component</type>
|
||||
<description>Terms of Service and privacy policy component with consent tracking.</description>
|
||||
<icon>icon-file-contract</icon>
|
||||
<category>Components</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokojoomtos</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoJoomHero</name>
|
||||
@@ -70,34 +50,14 @@
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoJoomCommunity</name>
|
||||
<element>pkg_mokojoomcommunity</element>
|
||||
<type>package</type>
|
||||
<description>Community Builder integration package with custom fields and user management.</description>
|
||||
<icon>icon-users</icon>
|
||||
<category>Community</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokojoomcommunity</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCommunity/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoJoomCross</name>
|
||||
<element>plg_system_mokojoomcross</element>
|
||||
<type>plugin</type>
|
||||
<description>Cross-extension integration plugin for Joomla component interoperability.</description>
|
||||
<icon>icon-link</icon>
|
||||
<category>Plugins</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokojoomcross</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomCross/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoJoomStoreLocator</name>
|
||||
<element>mod_mokojoomstorelocator</element>
|
||||
<name>MokoWaaS Announce</name>
|
||||
<element>mod_mokowaas_announce</element>
|
||||
<type>module</type>
|
||||
<description>Store locator module with Google Maps integration and search.</description>
|
||||
<icon>icon-map-marker-alt</icon>
|
||||
<description>Centralized announcement system via admin module.</description>
|
||||
<icon>icon-bullhorn</icon>
|
||||
<category>Modules</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokojoomstorelocator</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomStoreLocator/raw/branch/dev/updates.xml</updateserver>
|
||||
<article>https://mokoconsulting.tech/support/products/mokowaas-announce</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaSAnnounce/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>DPCalendar API</name>
|
||||
@@ -119,4 +79,14 @@
|
||||
<article>https://mokoconsulting.tech/support/products/mokogallerycalendar</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoGalleryCalendar/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
<extension>
|
||||
<name>MokoJoomOpenGraph</name>
|
||||
<element>pkg_mokoog</element>
|
||||
<type>package</type>
|
||||
<description>Open Graph, Twitter Card, and social sharing meta tags for articles, categories, and pages.</description>
|
||||
<icon>icon-share-alt</icon>
|
||||
<category>Components</category>
|
||||
<article>https://mokoconsulting.tech/support/products/mokojoomopengraph</article>
|
||||
<updateserver>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/raw/branch/dev/updates.xml</updateserver>
|
||||
</extension>
|
||||
</catalog>
|
||||
+5
-5
@@ -22,7 +22,7 @@
|
||||
<field name="default_category" type="sql" default=""
|
||||
label="Default Ticket Category"
|
||||
description="Category assigned to tickets without a selection."
|
||||
query="SELECT id AS value, title AS text FROM #__mokosuite_ticket_categories WHERE published = 1 ORDER BY ordering" />
|
||||
query="SELECT id AS value, title AS text FROM #__mokowaas_ticket_categories WHERE published = 1 ORDER BY ordering" />
|
||||
<field name="autoclose_days" type="number" default="7"
|
||||
label="Auto-Close After (days)"
|
||||
description="Resolved tickets are auto-closed after this many days. 0 = disabled." />
|
||||
@@ -35,13 +35,13 @@
|
||||
</field>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="permissions" label="COM_MOKOSUITE_ACL_TITLE"
|
||||
description="COM_MOKOSUITE_ACL_DESC">
|
||||
<fieldset name="permissions" label="COM_MOKOWAAS_ACL_TITLE"
|
||||
description="COM_MOKOWAAS_ACL_DESC">
|
||||
<field name="rules" type="rules"
|
||||
label="COM_MOKOSUITE_ACL_TITLE"
|
||||
label="COM_MOKOWAAS_ACL_TITLE"
|
||||
validate="rules"
|
||||
filter="rules"
|
||||
component="com_mokosuite"
|
||||
component="com_mokowaas"
|
||||
section="component" />
|
||||
</fieldset>
|
||||
</config>
|
||||
@@ -0,0 +1,41 @@
|
||||
; MokoWaaS Admin Dashboard - Language Strings
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
COM_MOKOWAAS_DASHBOARD_TITLE="MokoWaaS Control Panel"
|
||||
COM_MOKOWAAS_SITE="Site"
|
||||
COM_MOKOWAAS_DATABASE="Database"
|
||||
COM_MOKOWAAS_DEBUG_ON="Debug ON"
|
||||
COM_MOKOWAAS_OFFLINE="Offline"
|
||||
COM_MOKOWAAS_CLEAR_CACHE="Clear Cache"
|
||||
COM_MOKOWAAS_CHECK_UPDATES="Check Updates"
|
||||
COM_MOKOWAAS_ENABLED="Enabled"
|
||||
COM_MOKOWAAS_DISABLED="Disabled"
|
||||
COM_MOKOWAAS_PROTECTED="Protected"
|
||||
COM_MOKOWAAS_CONFIGURE="Configure"
|
||||
COM_MOKOWAAS_TOGGLE_SUCCESS="Plugin state updated."
|
||||
COM_MOKOWAAS_TOGGLE_FAIL="Failed to update plugin state."
|
||||
COM_MOKOWAAS_CACHE_CLEARED="Cache cleared successfully."
|
||||
COM_MOKOWAAS_EXTENSIONS_TITLE="Moko Extensions"
|
||||
COM_MOKOWAAS_EXTENSIONS_INFO="Install Moko Consulting Joomla packages from the official release server. Updates are handled through Joomla's native System > Update mechanism — each package registers its own update server."
|
||||
COM_MOKOWAAS_EXTENSIONS_LINK="Moko Extensions"
|
||||
COM_MOKOWAAS_HTACCESS_TITLE=".htaccess Maker"
|
||||
COM_MOKOWAAS_TICKETS_TITLE="Helpdesk"
|
||||
|
||||
; ACL
|
||||
COM_MOKOWAAS_ACL_DASHBOARD="View Dashboard"
|
||||
COM_MOKOWAAS_ACL_DASHBOARD_DESC="Allow viewing the MokoWaaS control panel dashboard."
|
||||
COM_MOKOWAAS_ACL_EXTENSIONS="Manage Extensions"
|
||||
COM_MOKOWAAS_ACL_EXTENSIONS_DESC="Allow installing and uninstalling Moko extensions."
|
||||
COM_MOKOWAAS_ACL_HTACCESS="Manage .htaccess"
|
||||
COM_MOKOWAAS_ACL_HTACCESS_DESC="Allow editing and saving the .htaccess configuration."
|
||||
COM_MOKOWAAS_ACL_TICKETS="View Tickets"
|
||||
COM_MOKOWAAS_ACL_TICKETS_DESC="Allow viewing helpdesk tickets."
|
||||
COM_MOKOWAAS_ACL_TICKETS_CREATE="Create Tickets"
|
||||
COM_MOKOWAAS_ACL_TICKETS_CREATE_DESC="Allow creating new helpdesk tickets."
|
||||
COM_MOKOWAAS_ACL_TICKETS_ASSIGN="Assign Tickets"
|
||||
COM_MOKOWAAS_ACL_TICKETS_ASSIGN_DESC="Allow assigning tickets to other users."
|
||||
COM_MOKOWAAS_ACL_PLUGINS_TOGGLE="Toggle Plugins"
|
||||
COM_MOKOWAAS_ACL_PLUGINS_TOGGLE_DESC="Allow enabling and disabling MokoWaaS feature plugins."
|
||||
COM_MOKOWAAS_ACL_CACHE="Clear Cache"
|
||||
COM_MOKOWAAS_ACL_CACHE_DESC="Allow clearing the Joomla cache from the dashboard."
|
||||
@@ -0,0 +1,19 @@
|
||||
; MokoWaaS Admin Dashboard - System Language Strings
|
||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; License: GPL-3.0-or-later
|
||||
|
||||
COM_MOKOWAAS="MokoWaaS"
|
||||
COM_MOKOWAAS_DESCRIPTION="MokoWaaS admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management."
|
||||
COM_MOKOWAAS_DASHBOARD_TITLE="MokoWaaS Control Panel"
|
||||
COM_MOKOWAAS_MENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOWAAS_MENU_EXTENSIONS="Moko Extensions"
|
||||
COM_MOKOWAAS_MENU_PLUGINS="Feature Plugins"
|
||||
COM_MOKOWAAS_MENU_UPDATES="Joomla Updates"
|
||||
COM_MOKOWAAS_MENU_CHECKIN="Global Check-in"
|
||||
COM_MOKOWAAS_MENU_TICKETS="Helpdesk"
|
||||
COM_MOKOWAAS_MENU_HTACCESS=".htaccess Maker"
|
||||
COM_MOKOWAAS_MENU_PRIVACY="Privacy Guard"
|
||||
COM_MOKOWAAS_MENU_WAFLOG="WAF Log"
|
||||
COM_MOKOWAAS_MENU_DATABASE="Database Tools"
|
||||
COM_MOKOWAAS_MENU_CLEANUP="Cache Cleanup"
|
||||
COM_MOKOWAAS_MENU_CACHE="Cache Management"
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
@@ -20,8 +20,8 @@ return new class implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoSuite'));
|
||||
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoSuite'));
|
||||
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoWaaS'));
|
||||
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoWaaS'));
|
||||
|
||||
$container->set(
|
||||
ComponentInterface::class,
|
||||
+12
-74
@@ -1,8 +1,8 @@
|
||||
--
|
||||
-- MokoSuite Helpdesk Tables
|
||||
-- MokoWaaS Helpdesk Tables
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_categories` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_categories` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`alias` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
@@ -16,53 +16,13 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_categories` (
|
||||
KEY `idx_alias` (`alias`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_statuses` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(100) NOT NULL,
|
||||
`alias` VARCHAR(100) NOT NULL,
|
||||
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0,
|
||||
`is_closed` TINYINT NOT NULL DEFAULT 0,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_alias` (`alias`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_statuses` (`id`, `title`, `alias`, `color`, `is_default`, `is_closed`, `ordering`) VALUES
|
||||
(1, 'Open', 'open', 'bg-primary', 1, 0, 1),
|
||||
(2, 'In Progress', 'in_progress', 'bg-info', 0, 0, 2),
|
||||
(3, 'Waiting', 'waiting', 'bg-warning text-dark', 0, 0, 3),
|
||||
(4, 'Resolved', 'resolved', 'bg-success', 0, 0, 4),
|
||||
(5, 'Closed', 'closed', 'bg-secondary', 0, 1, 5);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_priorities` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(100) NOT NULL,
|
||||
`alias` VARCHAR(100) NOT NULL,
|
||||
`color` VARCHAR(30) NOT NULL DEFAULT 'bg-secondary',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0,
|
||||
`weight` INT NOT NULL DEFAULT 0,
|
||||
`ordering` INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_alias` (`alias`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_priorities` (`id`, `title`, `alias`, `color`, `is_default`, `weight`, `ordering`) VALUES
|
||||
(1, 'Low', 'low', 'bg-secondary', 0, 10, 1),
|
||||
(2, 'Normal', 'normal', 'bg-primary', 1, 20, 2),
|
||||
(3, 'High', 'high', 'bg-warning text-dark', 0, 30, 3),
|
||||
(4, 'Urgent', 'urgent', 'bg-danger', 0, 40, 4);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_tickets` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_tickets` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`subject` VARCHAR(512) NOT NULL,
|
||||
`body` TEXT NOT NULL,
|
||||
`status` ENUM('open','in_progress','waiting','resolved','closed') NOT NULL DEFAULT 'open',
|
||||
`status_id` INT UNSIGNED DEFAULT NULL,
|
||||
`priority` ENUM('low','normal','high','urgent') NOT NULL DEFAULT 'normal',
|
||||
`priority_id` INT UNSIGNED DEFAULT NULL,
|
||||
`category_id` INT UNSIGNED DEFAULT NULL,
|
||||
`contact_id` INT UNSIGNED DEFAULT NULL,
|
||||
`created_by` INT NOT NULL DEFAULT 0,
|
||||
`assigned_to` INT DEFAULT NULL,
|
||||
`created` DATETIME NOT NULL,
|
||||
@@ -74,23 +34,13 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_tickets` (
|
||||
`sla_responded` TINYINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_status_id` (`status_id`),
|
||||
KEY `idx_priority` (`priority`),
|
||||
KEY `idx_priority_id` (`priority_id`),
|
||||
KEY `idx_assigned` (`assigned_to`),
|
||||
KEY `idx_category` (`category_id`),
|
||||
KEY `idx_contact` (`contact_id`),
|
||||
KEY `idx_created` (`created`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_category_field_groups` (
|
||||
`category_id` INT UNSIGNED NOT NULL,
|
||||
`field_group_id` INT NOT NULL,
|
||||
PRIMARY KEY (`category_id`, `field_group_id`),
|
||||
KEY `idx_field_group` (`field_group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_replies` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_replies` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ticket_id` INT UNSIGNED NOT NULL,
|
||||
`user_id` INT NOT NULL DEFAULT 0,
|
||||
@@ -102,7 +52,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_replies` (
|
||||
KEY `idx_created` (`created`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_canned` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_canned` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`body` TEXT NOT NULL,
|
||||
@@ -111,7 +61,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_canned` (
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_automation` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_ticket_automation` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL,
|
||||
`trigger_event` VARCHAR(50) NOT NULL DEFAULT 'ticket_created',
|
||||
@@ -122,25 +72,14 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_automation` (
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_ticket_assignees` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`ticket_id` INT UNSIGNED NOT NULL,
|
||||
`assignee_type` ENUM('user','group') NOT NULL DEFAULT 'user',
|
||||
`assignee_id` INT NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_unique_assignment` (`ticket_id`, `assignee_type`, `assignee_id`),
|
||||
KEY `idx_ticket` (`ticket_id`),
|
||||
KEY `idx_assignee` (`assignee_type`, `assignee_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Default automation rules
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES
|
||||
INSERT IGNORE INTO `#__mokowaas_ticket_automation` (`id`, `title`, `trigger_event`, `conditions`, `actions`, `enabled`, `ordering`) VALUES
|
||||
(1, 'Auto-close resolved tickets after 7 days', 'scheduled', '[{"field":"status","op":"eq","value":"resolved"},{"field":"age_hours","op":"gt","value":"168"}]', '[{"type":"set_status","value":"closed"},{"type":"add_note","value":"Auto-closed after 7 days with no response."}]', 1, 1),
|
||||
(2, 'Escalate urgent tickets with no response in 1 hour', 'scheduled', '[{"field":"priority","op":"eq","value":"urgent"},{"field":"sla_responded","op":"eq","value":"0"},{"field":"age_hours","op":"gt","value":"1"}]', '[{"type":"add_note","value":"SLA BREACH: Urgent ticket has no staff response after 1 hour."}]', 1, 2),
|
||||
(3, 'Notify on high priority ticket creation', 'ticket_created', '[{"field":"priority","op":"in","value":"high,urgent"}]', '[{"type":"add_note","value":"High/urgent ticket created — requires immediate attention."}]', 1, 3);
|
||||
|
||||
-- Default categories
|
||||
INSERT IGNORE INTO `#__mokosuite_ticket_categories` (`id`, `title`, `alias`, `description`, `sla_response_minutes`, `sla_resolution_minutes`, `ordering`) VALUES
|
||||
INSERT IGNORE INTO `#__mokowaas_ticket_categories` (`id`, `title`, `alias`, `description`, `sla_response_minutes`, `sla_resolution_minutes`, `ordering`) VALUES
|
||||
(1, 'General Support', 'general-support', 'General questions and assistance', 480, 2880, 1),
|
||||
(2, 'Bug Report', 'bug-report', 'Report a software bug or issue', 240, 1440, 2),
|
||||
(3, 'Feature Request', 'feature-request', 'Request a new feature or enhancement', 1440, 10080, 3),
|
||||
@@ -151,7 +90,7 @@ INSERT IGNORE INTO `#__mokosuite_ticket_categories` (`id`, `title`, `alias`, `de
|
||||
-- Privacy Guard Tables
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_consent_log` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_consent_log` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`category` VARCHAR(50) NOT NULL,
|
||||
@@ -163,7 +102,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_consent_log` (
|
||||
KEY `idx_category` (`category`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_data_requests` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_data_requests` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`type` ENUM('export','delete','anonymize') NOT NULL,
|
||||
@@ -177,7 +116,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_data_requests` (
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_retention_policies` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokowaas_retention_policies` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`content_type` VARCHAR(100) NOT NULL,
|
||||
`retention_days` INT UNSIGNED NOT NULL DEFAULT 365,
|
||||
@@ -188,10 +127,9 @@ CREATE TABLE IF NOT EXISTS `#__mokosuite_retention_policies` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Default retention policies
|
||||
INSERT IGNORE INTO `#__mokosuite_retention_policies` (`id`, `content_type`, `retention_days`, `action`, `enabled`, `description`) VALUES
|
||||
INSERT IGNORE INTO `#__mokowaas_retention_policies` (`id`, `content_type`, `retention_days`, `action`, `enabled`, `description`) VALUES
|
||||
(1, 'action_logs', 90, 'delete', 1, 'Delete action log entries older than 90 days'),
|
||||
(2, 'waf_logs', 30, 'delete', 1, 'Delete WAF block logs older than 30 days'),
|
||||
(3, 'sessions', 7, 'delete', 1, 'Purge expired sessions older than 7 days'),
|
||||
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
|
||||
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
|
||||
|
||||
+52
-207
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Controller;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -24,18 +24,18 @@ class DisplayController extends BaseController
|
||||
* ACL map: view name => required permission.
|
||||
*/
|
||||
private const VIEW_ACL = [
|
||||
'dashboard' => 'mokosuite.dashboard',
|
||||
'extensions' => 'mokosuite.extensions',
|
||||
'htaccess' => 'mokosuite.htaccess',
|
||||
'tickets' => 'mokosuite.tickets',
|
||||
'ticket' => 'mokosuite.tickets',
|
||||
'dashboard' => 'mokowaas.dashboard',
|
||||
'extensions' => 'mokowaas.extensions',
|
||||
'htaccess' => 'mokowaas.htaccess',
|
||||
'tickets' => 'mokowaas.tickets',
|
||||
'ticket' => 'mokowaas.tickets',
|
||||
'privacy' => 'core.admin',
|
||||
'waflog' => 'core.admin',
|
||||
'categories' => 'mokosuite.tickets',
|
||||
'canned' => 'mokosuite.tickets',
|
||||
'categories' => 'mokowaas.tickets',
|
||||
'canned' => 'mokowaas.tickets',
|
||||
'automation' => 'core.admin',
|
||||
'database' => 'core.admin',
|
||||
'cleanup' => 'mokosuite.cache',
|
||||
'cleanup' => 'mokowaas.cache',
|
||||
];
|
||||
|
||||
public function display($cachable = false, $urlparams = [])
|
||||
@@ -62,7 +62,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.plugins.toggle'))
|
||||
if (!$this->checkAcl('mokowaas.plugins.toggle'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -80,160 +80,6 @@ class DisplayController extends BaseController
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Heartbeat
|
||||
// ==================================================================
|
||||
|
||||
public function sendHeartbeat()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
try
|
||||
{
|
||||
$monitorPlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuite_monitor');
|
||||
|
||||
if (!$monitorPlugin)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Monitor plugin not enabled.']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$params = new \Joomla\Registry\Registry($monitorPlugin->params);
|
||||
$baseUrl = rtrim($params->get('base_url', ''), '/');
|
||||
|
||||
// Fall back to manifest XML default if not yet saved in params
|
||||
if (empty($baseUrl))
|
||||
{
|
||||
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml';
|
||||
|
||||
if (is_file($manifestFile))
|
||||
{
|
||||
$xml = simplexml_load_file($manifestFile);
|
||||
|
||||
if ($xml)
|
||||
{
|
||||
foreach ($xml->xpath('//field[@name="base_url"]') as $field)
|
||||
{
|
||||
$baseUrl = rtrim((string) $field['default'], '/');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($baseUrl))
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'MokoSuiteHQ URL not configured in monitor plugin.']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$corePlugin = \Joomla\CMS\Plugin\PluginHelper::getPlugin('system', 'mokosuite');
|
||||
$coreParams = new \Joomla\Registry\Registry($corePlugin ? $corePlugin->params : '{}');
|
||||
$healthToken = $coreParams->get('health_api_token', '');
|
||||
|
||||
if (empty($healthToken))
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Health token not configured.']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$siteUrl = rtrim(\Joomla\CMS\Uri\Uri::root(), '/');
|
||||
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
|
||||
$timestamp = time();
|
||||
|
||||
$payload = json_encode([
|
||||
'token' => $healthToken,
|
||||
'domain' => $domain,
|
||||
'site_name' => Factory::getConfig()->get('sitename', 'Joomla'),
|
||||
'site_url' => $siteUrl,
|
||||
'joomla_version' => (new \Joomla\CMS\Version())->getShortVersion(),
|
||||
'php_version' => PHP_VERSION,
|
||||
'timestamp' => $timestamp,
|
||||
], JSON_UNESCAPED_SLASHES);
|
||||
|
||||
// RSA sign the request
|
||||
$headers = ['Content-Type: application/json'];
|
||||
$signingKeyB64 = $params->get('signing_key', '');
|
||||
|
||||
// Fall back to manifest XML default if not yet saved in params
|
||||
if (empty($signingKeyB64))
|
||||
{
|
||||
$manifestFile = JPATH_PLUGINS . '/system/mokosuite_monitor/mokosuite_monitor.xml';
|
||||
|
||||
if (is_file($manifestFile))
|
||||
{
|
||||
$xml = simplexml_load_file($manifestFile);
|
||||
|
||||
if ($xml)
|
||||
{
|
||||
foreach ($xml->xpath('//field[@name="signing_key"]') as $field)
|
||||
{
|
||||
$signingKeyB64 = (string) $field['default'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($signingKeyB64))
|
||||
{
|
||||
$privateKeyPem = base64_decode($signingKeyB64);
|
||||
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
||||
|
||||
if ($privateKey !== false)
|
||||
{
|
||||
$message = $domain . '|' . $timestamp . '|' . $healthToken;
|
||||
$signature = '';
|
||||
|
||||
if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256))
|
||||
{
|
||||
$headers[] = 'X-MokoSuite-Signature: ' . base64_encode($signature);
|
||||
$headers[] = 'X-MokoSuite-Timestamp: ' . $timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$endpoint = $baseUrl . '/api/index.php/v1/mokosuitehq/heartbeat';
|
||||
|
||||
$ch = curl_init($endpoint);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Connection failed: ' . $error]);
|
||||
}
|
||||
elseif ($code >= 200 && $code < 300)
|
||||
{
|
||||
$body = json_decode($response, true);
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Heartbeat sent: ' . ($body['status'] ?? 'ok')]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$body = json_decode($response, true);
|
||||
$this->jsonResponse(['success' => false, 'message' => 'HTTP ' . $code . ': ' . ($body['error'] ?? $body['message'] ?? 'Unknown')]);
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache
|
||||
// ==================================================================
|
||||
|
||||
@@ -241,7 +87,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.cache'))
|
||||
if (!$this->checkAcl('mokowaas.cache'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -254,7 +100,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.cache'))
|
||||
if (!$this->checkAcl('mokowaas.cache'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -271,7 +117,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.extensions'))
|
||||
if (!$this->checkAcl('mokowaas.extensions'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -296,7 +142,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.htaccess'))
|
||||
if (!$this->checkAcl('mokowaas.htaccess'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -328,7 +174,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.htaccess'))
|
||||
if (!$this->checkAcl('mokowaas.htaccess'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -356,7 +202,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.tickets.create'))
|
||||
if (!$this->checkAcl('mokowaas.tickets.create'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -376,7 +222,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.tickets'))
|
||||
if (!$this->checkAcl('mokowaas.tickets'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -395,7 +241,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.tickets'))
|
||||
if (!$this->checkAcl('mokowaas.tickets'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -459,7 +305,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
|
||||
$this->jsonResponse($model->optimizeTables());
|
||||
}
|
||||
|
||||
@@ -467,7 +313,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
|
||||
$this->jsonResponse($model->repairTables());
|
||||
}
|
||||
|
||||
@@ -475,16 +321,16 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); return; }
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
|
||||
$this->jsonResponse($model->purgeSessions());
|
||||
}
|
||||
|
||||
public function cleanDirectory()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuite.cache')) { $this->jsonForbidden(); return; }
|
||||
if (!$this->checkAcl('mokowaas.cache')) { $this->jsonForbidden(); return; }
|
||||
$dirKey = Factory::getApplication()->getInput()->getString('dir_key', '');
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
|
||||
$this->jsonResponse($model->cleanDirectory($dirKey));
|
||||
}
|
||||
|
||||
@@ -495,7 +341,7 @@ class DisplayController extends BaseController
|
||||
public function saveCategory()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$db = Factory::getDbo();
|
||||
$id = $input->getInt('id', 0);
|
||||
@@ -509,10 +355,10 @@ class DisplayController extends BaseController
|
||||
];
|
||||
if ($id) {
|
||||
$data->id = $id;
|
||||
$db->updateObject('#__mokosuite_ticket_categories', $data, 'id');
|
||||
$db->updateObject('#__mokowaas_ticket_categories', $data, 'id');
|
||||
} else {
|
||||
$data->ordering = 0;
|
||||
$db->insertObject('#__mokosuite_ticket_categories', $data, 'id');
|
||||
$db->insertObject('#__mokowaas_ticket_categories', $data, 'id');
|
||||
}
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Category saved.', 'id' => (int) $data->id]);
|
||||
}
|
||||
@@ -520,16 +366,16 @@ class DisplayController extends BaseController
|
||||
public function deleteCategory()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_categories')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Category deleted.']);
|
||||
}
|
||||
|
||||
public function saveCanned()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$db = Factory::getDbo();
|
||||
$data = (object) [
|
||||
@@ -539,17 +385,17 @@ class DisplayController extends BaseController
|
||||
'ordering' => 0,
|
||||
];
|
||||
$id = $input->getInt('id', 0);
|
||||
if ($id) { $data->id = $id; $db->updateObject('#__mokosuite_ticket_canned', $data, 'id'); }
|
||||
else { $db->insertObject('#__mokosuite_ticket_canned', $data, 'id'); }
|
||||
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_canned', $data, 'id'); }
|
||||
else { $db->insertObject('#__mokowaas_ticket_canned', $data, 'id'); }
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Canned response saved.', 'id' => (int) $data->id]);
|
||||
}
|
||||
|
||||
public function deleteCanned()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('mokosuite.tickets')) { $this->jsonForbidden(); }
|
||||
if (!$this->checkAcl('mokowaas.tickets')) { $this->jsonForbidden(); }
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_canned')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Canned response deleted.']);
|
||||
}
|
||||
|
||||
@@ -568,8 +414,8 @@ class DisplayController extends BaseController
|
||||
'ordering' => 0,
|
||||
];
|
||||
$id = $input->getInt('id', 0);
|
||||
if ($id) { $data->id = $id; $db->updateObject('#__mokosuite_ticket_automation', $data, 'id'); }
|
||||
else { $db->insertObject('#__mokosuite_ticket_automation', $data, 'id'); }
|
||||
if ($id) { $data->id = $id; $db->updateObject('#__mokowaas_ticket_automation', $data, 'id'); }
|
||||
else { $db->insertObject('#__mokowaas_ticket_automation', $data, 'id'); }
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Rule saved.', 'id' => (int) $data->id]);
|
||||
}
|
||||
|
||||
@@ -578,7 +424,7 @@ class DisplayController extends BaseController
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokosuite_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$db->setQuery($db->getQuery(true)->delete('#__mokowaas_ticket_automation')->where('id = ' . Factory::getApplication()->getInput()->getInt('id', 0)))->execute();
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Rule deleted.']);
|
||||
}
|
||||
|
||||
@@ -588,7 +434,7 @@ class DisplayController extends BaseController
|
||||
if (!$this->checkAcl('core.admin')) { $this->jsonForbidden(); }
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery($db->getQuery(true)->update('#__mokosuite_ticket_automation')
|
||||
$db->setQuery($db->getQuery(true)->update('#__mokowaas_ticket_automation')
|
||||
->set('enabled = ' . $input->getInt('enabled', 0))
|
||||
->where('id = ' . $input->getInt('id', 0)))->execute();
|
||||
$this->jsonResponse(['success' => true, 'message' => 'Rule updated.']);
|
||||
@@ -611,8 +457,8 @@ class DisplayController extends BaseController
|
||||
$db = Factory::getDbo();
|
||||
$settings = [];
|
||||
|
||||
// Export all MokoSuite plugin params
|
||||
$plugins = ['mokosuite', 'mokosuite_firewall', 'mokosuite_tenant', 'mokosuite_devtools', 'mokosuite_offline'];
|
||||
// Export all MokoWaaS plugin params
|
||||
$plugins = ['mokowaas', 'mokowaas_firewall', 'mokowaas_tenant', 'mokowaas_devtools', 'mokowaas_offline'];
|
||||
|
||||
foreach ($plugins as $element)
|
||||
{
|
||||
@@ -632,7 +478,7 @@ class DisplayController extends BaseController
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
);
|
||||
$settings['component'] = json_decode($db->loadResult() ?? '{}', true);
|
||||
@@ -688,7 +534,7 @@ class DisplayController extends BaseController
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component'])))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
)->execute();
|
||||
$count++;
|
||||
@@ -712,7 +558,7 @@ class DisplayController extends BaseController
|
||||
}
|
||||
|
||||
$days = Factory::getApplication()->getInput()->getInt('days', 30);
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
|
||||
|
||||
$this->jsonResponse($model->purgeLogs($days));
|
||||
}
|
||||
@@ -728,7 +574,7 @@ class DisplayController extends BaseController
|
||||
}
|
||||
|
||||
$ip = Factory::getApplication()->getInput()->getString('ip', '');
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
|
||||
|
||||
$this->jsonResponse($model->banIp($ip));
|
||||
}
|
||||
@@ -748,7 +594,7 @@ class DisplayController extends BaseController
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
$action = $input->getString('action', 'deny');
|
||||
|
||||
if ($action === 'create')
|
||||
@@ -794,7 +640,7 @@ class DisplayController extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
|
||||
$this->jsonResponse($model->exportUserData(
|
||||
Factory::getApplication()->getInput()->getInt('user_id', 0)
|
||||
@@ -809,7 +655,7 @@ class DisplayController extends BaseController
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
if (!$this->checkAcl('mokosuite.tickets'))
|
||||
if (!$this->checkAcl('mokowaas.tickets'))
|
||||
{
|
||||
$this->jsonForbidden();
|
||||
return;
|
||||
@@ -836,19 +682,19 @@ class DisplayController extends BaseController
|
||||
// ==================================================================
|
||||
|
||||
/**
|
||||
* Check a MokoSuite ACL permission for the current user.
|
||||
* Check a MokoWaaS ACL permission for the current user.
|
||||
*/
|
||||
private function checkAcl(string $action): bool
|
||||
{
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
// Super admins always pass
|
||||
if ($user->authorise('core.admin', 'com_mokosuite'))
|
||||
if ($user->authorise('core.admin', 'com_mokowaas'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user->authorise($action, 'com_mokosuite');
|
||||
return $user->authorise($action, 'com_mokowaas');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -868,7 +714,6 @@ class DisplayController extends BaseController
|
||||
private function jsonForbidden(): void
|
||||
{
|
||||
$this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
+29
-29
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -21,7 +21,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
* Provides icon, category, and description for dashboard display.
|
||||
*/
|
||||
private const PLUGIN_META = [
|
||||
'mokosuite' => [
|
||||
'mokowaas' => [
|
||||
'icon' => 'icon-shield-alt',
|
||||
'category' => 'core',
|
||||
'label' => 'Core',
|
||||
@@ -29,7 +29,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
'protected' => true,
|
||||
'configure_only' => false,
|
||||
],
|
||||
'mokosuite_firewall' => [
|
||||
'mokowaas_firewall' => [
|
||||
'icon' => 'icon-lock',
|
||||
'category' => 'security',
|
||||
'label' => 'Firewall',
|
||||
@@ -37,7 +37,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
'protected' => false,
|
||||
'configure_only' => false,
|
||||
],
|
||||
'mokosuite_tenant' => [
|
||||
'mokowaas_tenant' => [
|
||||
'icon' => 'icon-users',
|
||||
'category' => 'security',
|
||||
'label' => 'Tenant Restrictions',
|
||||
@@ -45,7 +45,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
'protected' => false,
|
||||
'configure_only' => false,
|
||||
],
|
||||
'mokosuite_offline' => [
|
||||
'mokowaas_offline' => [
|
||||
'icon' => 'icon-globe',
|
||||
'category' => 'security',
|
||||
'label' => 'Offline Bypass',
|
||||
@@ -53,7 +53,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
'protected' => false,
|
||||
'configure_only' => true,
|
||||
],
|
||||
'mokosuite_devtools' => [
|
||||
'mokowaas_devtools' => [
|
||||
'icon' => 'icon-wrench',
|
||||
'category' => 'tools',
|
||||
'label' => 'Developer Tools',
|
||||
@@ -61,7 +61,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
'protected' => false,
|
||||
'configure_only' => true,
|
||||
],
|
||||
'mokosuitedemo' => [
|
||||
'mokowaasdemo' => [
|
||||
'icon' => 'icon-undo',
|
||||
'category' => 'content',
|
||||
'label' => 'Demo Reset Task',
|
||||
@@ -69,11 +69,11 @@ class DashboardModel extends BaseDatabaseModel
|
||||
'protected' => false,
|
||||
'configure_only' => true,
|
||||
],
|
||||
'mokosuitesync' => [
|
||||
'mokowaassync' => [
|
||||
'icon' => 'icon-sync',
|
||||
'category' => 'content',
|
||||
'label' => 'Content Sync Task',
|
||||
'description' => 'Scheduled content synchronisation to remote MokoSuite sites.',
|
||||
'description' => 'Scheduled content synchronisation to remote MokoWaaS sites.',
|
||||
'protected' => false,
|
||||
'configure_only' => true,
|
||||
],
|
||||
@@ -92,7 +92,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
];
|
||||
|
||||
/**
|
||||
* Discover all installed MokoSuite plugins.
|
||||
* Discover all installed MokoWaaS plugins.
|
||||
*
|
||||
* @return array Plugin rows enriched with dashboard metadata.
|
||||
*/
|
||||
@@ -114,20 +114,20 @@ class DashboardModel extends BaseDatabaseModel
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where([
|
||||
'(' .
|
||||
// System plugins: mokosuite, mokosuite_*
|
||||
// System plugins: mokowaas, mokowaas_*
|
||||
'(' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
|
||||
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('system')
|
||||
. ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokosuite')
|
||||
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuite\_%') . ')'
|
||||
. ' AND ' . $db->quoteName('element') . ' != ' . $db->quote('mokosuite_monitor') . ')'
|
||||
. ' AND (' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
|
||||
. ' OR ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas\_%') . ')'
|
||||
. ' AND ' . $db->quoteName('element') . ' != ' . $db->quote('mokowaas_monitor') . ')'
|
||||
// Webservices plugins
|
||||
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
|
||||
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('webservices')
|
||||
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokosuite') . ')'
|
||||
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas') . ')'
|
||||
// Task plugins
|
||||
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('plugin')
|
||||
. ' AND ' . $db->quoteName('folder') . ' = ' . $db->quote('task')
|
||||
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokosuite%') . ')'
|
||||
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mokowaas%') . ')'
|
||||
. ')',
|
||||
])
|
||||
->order($db->quoteName('folder') . ' ASC, ' . $db->quoteName('element') . ' ASC');
|
||||
@@ -190,11 +190,11 @@ class DashboardModel extends BaseDatabaseModel
|
||||
$config = $app->getConfig();
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Get MokoSuite package version
|
||||
// Get MokoWaaS package version
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('manifest_cache'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('package'));
|
||||
$db->setQuery($query);
|
||||
$pkgCache = json_decode($db->loadResult() ?? '{}');
|
||||
@@ -204,7 +204,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
'joomla_version' => (new Version())->getShortVersion(),
|
||||
'php_version' => PHP_VERSION,
|
||||
'db_type' => $db->getServerType(),
|
||||
'mokosuite_version' => $pkgCache->version ?? '—',
|
||||
'mokowaas_version' => $pkgCache->version ?? '—',
|
||||
'debug' => (bool) $config->get('debug'),
|
||||
'offline' => (bool) $config->get('offline'),
|
||||
'sef' => (bool) $config->get('sef'),
|
||||
@@ -213,7 +213,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installed MokoSuite component and modules with versions.
|
||||
* Get installed MokoWaaS component and modules with versions.
|
||||
*
|
||||
* @return array Array of extension objects with name, element, type, version.
|
||||
*/
|
||||
@@ -232,10 +232,10 @@ class DashboardModel extends BaseDatabaseModel
|
||||
->where('('
|
||||
// The component
|
||||
. '(' . $db->quoteName('type') . ' = ' . $db->quote('component')
|
||||
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokosuite') . ')'
|
||||
. ' AND ' . $db->quoteName('element') . ' = ' . $db->quote('com_mokowaas') . ')'
|
||||
// Admin modules
|
||||
. ' OR (' . $db->quoteName('type') . ' = ' . $db->quote('module')
|
||||
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokosuite%') . ')'
|
||||
. ' AND ' . $db->quoteName('element') . ' LIKE ' . $db->quote('mod_mokowaas%') . ')'
|
||||
. ')')
|
||||
->order($db->quoteName('type') . ' ASC, ' . $db->quoteName('element') . ' ASC');
|
||||
|
||||
@@ -272,7 +272,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Verify the extension exists and is a MokoSuite plugin
|
||||
// Verify the extension exists and is a MokoWaaS plugin
|
||||
$query = $db->getQuery(true)
|
||||
->select([$db->quoteName('element'), $db->quoteName('protected')])
|
||||
->from($db->quoteName('#__extensions'))
|
||||
@@ -287,7 +287,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
}
|
||||
|
||||
// Don't allow disabling protected/core plugins
|
||||
if (!$enabled && ((int) $ext->protected || $ext->element === 'mokosuite'))
|
||||
if (!$enabled && ((int) $ext->protected || $ext->element === 'mokowaas'))
|
||||
{
|
||||
return ['success' => false, 'message' => 'This plugin is protected and cannot be disabled.'];
|
||||
}
|
||||
@@ -425,7 +425,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
if (str_contains($row->element, 'sync'))
|
||||
{
|
||||
$meta['label'] = 'Content Sync Task';
|
||||
$meta['description'] = 'Scheduled content synchronisation to remote MokoSuite sites.';
|
||||
$meta['description'] = 'Scheduled content synchronisation to remote MokoWaaS sites.';
|
||||
}
|
||||
elseif (str_contains($row->element, 'demo'))
|
||||
{
|
||||
@@ -544,7 +544,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_waf_log'))
|
||||
->from($db->quoteName('#__mokowaas_waf_log'))
|
||||
->order($db->quoteName('created') . ' DESC')
|
||||
->setLimit($limit);
|
||||
$db->setQuery($query);
|
||||
@@ -567,7 +567,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
"SELECT DATE(" . $db->quoteName('created') . ") AS day, COUNT(*) AS total"
|
||||
. " FROM " . $db->quoteName('#__mokosuite_waf_log')
|
||||
. " FROM " . $db->quoteName('#__mokowaas_waf_log')
|
||||
. " WHERE " . $db->quoteName('created') . " >= DATE_SUB(NOW(), INTERVAL $days DAY)"
|
||||
. " GROUP BY day ORDER BY day"
|
||||
);
|
||||
+7
-77
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -48,12 +48,6 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
$remoteVersion = $release['version'] ?? '';
|
||||
$downloadUrl = $release['download_url'] ?? '';
|
||||
|
||||
// Skip extensions with no release available and not installed
|
||||
if (empty($remoteVersion) && $localVersion === null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = 'not_installed';
|
||||
|
||||
if ($localVersion !== null)
|
||||
@@ -68,9 +62,6 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
|
||||
$extensionId = $this->getExtensionId($entry['element']);
|
||||
|
||||
$needsDlid = $release['needs_dlid'] ?? false;
|
||||
$hasDlid = $needsDlid && $extensionId ? $this->hasDownloadKey($entry['element']) : false;
|
||||
|
||||
$packages[] = (object) [
|
||||
'label' => $entry['name'],
|
||||
'description' => $entry['description'],
|
||||
@@ -85,9 +76,6 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
'article_url' => $entry['article'] ?? '',
|
||||
'protected' => ($entry['protected'] ?? 'false') === 'true',
|
||||
'extension_id' => $extensionId,
|
||||
'needs_dlid' => $needsDlid,
|
||||
'has_dlid' => $hasDlid,
|
||||
'has_stable' => $release['has_stable'] ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -104,7 +92,7 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
public function installFromUrl(string $url): array
|
||||
{
|
||||
$tmpPath = Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp');
|
||||
$tmpFile = $tmpPath . '/mokosuite_install_' . md5($url) . '.zip';
|
||||
$tmpFile = $tmpPath . '/mokowaas_install_' . md5($url) . '.zip';
|
||||
|
||||
try
|
||||
{
|
||||
@@ -160,7 +148,7 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
return $this->catalogCache;
|
||||
}
|
||||
|
||||
$catalogFile = JPATH_ADMINISTRATOR . '/components/com_mokosuite/catalog.xml';
|
||||
$catalogFile = JPATH_ADMINISTRATOR . '/components/com_mokowaas/catalog.xml';
|
||||
|
||||
if (!file_exists($catalogFile))
|
||||
{
|
||||
@@ -238,36 +226,13 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
return [];
|
||||
}
|
||||
|
||||
// Determine site's update channel preference
|
||||
$channel = 'dev'; // default to dev — show everything
|
||||
$hasStable = false;
|
||||
$hasDev = false;
|
||||
|
||||
// Find the best version entry, preferring the site's channel
|
||||
// Find the highest version entry
|
||||
$bestVersion = '0.0.0';
|
||||
$downloadUrl = '';
|
||||
$needsDlid = false;
|
||||
|
||||
foreach ($xml->update as $update)
|
||||
{
|
||||
$ver = (string) ($update->version ?? '');
|
||||
$tag = '';
|
||||
|
||||
// Check for <tags><tag> element
|
||||
if (isset($update->tags->tag))
|
||||
{
|
||||
$tag = (string) $update->tags->tag;
|
||||
}
|
||||
|
||||
if ($tag === 'stable')
|
||||
{
|
||||
$hasStable = true;
|
||||
}
|
||||
|
||||
if ($tag === 'dev')
|
||||
{
|
||||
$hasDev = true;
|
||||
}
|
||||
|
||||
if ($ver === '' || version_compare($ver, $bestVersion, '<='))
|
||||
{
|
||||
@@ -276,15 +241,10 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
|
||||
$bestVersion = $ver;
|
||||
|
||||
// Get download URL from <downloads><downloadurl>
|
||||
if (isset($update->downloads->downloadurl))
|
||||
{
|
||||
$downloadUrl = (string) $update->downloads->downloadurl;
|
||||
|
||||
// Check if download URL contains dlid placeholder
|
||||
if (str_contains($downloadUrl, 'dlid='))
|
||||
{
|
||||
$needsDlid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,9 +256,6 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
return [
|
||||
'version' => $bestVersion,
|
||||
'download_url' => $downloadUrl,
|
||||
'has_stable' => $hasStable,
|
||||
'has_dev' => $hasDev,
|
||||
'needs_dlid' => $needsDlid,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -342,33 +299,6 @@ class ExtensionsModel extends BaseDatabaseModel
|
||||
return $versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an extension has a download key configured.
|
||||
*/
|
||||
private function hasDownloadKey(string $element): bool
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('us.extra_query'))
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON us.update_site_id = use.update_site_id')
|
||||
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
|
||||
->where($db->quoteName('e.element') . ' = ' . $db->quote($element));
|
||||
|
||||
$db->setQuery($query, 0, 1);
|
||||
$extraQuery = (string) $db->loadResult();
|
||||
|
||||
return !empty($extraQuery) && str_contains($extraQuery, 'dlid=');
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extension_id for an element (for uninstall links).
|
||||
*
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -63,7 +63,7 @@ class HtaccessModel extends BaseDatabaseModel
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
|
||||
$db->setQuery($query);
|
||||
$params = new Registry($db->loadResult() ?? '{}');
|
||||
@@ -89,7 +89,7 @@ class HtaccessModel extends BaseDatabaseModel
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
|
||||
$db->setQuery($query);
|
||||
$params = new Registry($db->loadResult() ?? '{}');
|
||||
@@ -107,7 +107,7 @@ class HtaccessModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
)->execute();
|
||||
|
||||
@@ -135,7 +135,7 @@ class HtaccessModel extends BaseDatabaseModel
|
||||
public function saveHtaccess(string $content): array
|
||||
{
|
||||
$path = JPATH_ROOT . '/.htaccess';
|
||||
$backup = JPATH_ROOT . '/.htaccess.mokosuite.bak';
|
||||
$backup = JPATH_ROOT . '/.htaccess.mokowaas.bak';
|
||||
|
||||
try
|
||||
{
|
||||
@@ -158,7 +158,7 @@ class HtaccessModel extends BaseDatabaseModel
|
||||
return ['success' => false, 'message' => '.htaccess is not writable.'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => '.htaccess saved. Backup at .htaccess.mokosuite.bak'];
|
||||
return ['success' => true, 'message' => '.htaccess saved. Backup at .htaccess.mokowaas.bak'];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
@@ -178,9 +178,9 @@ class HtaccessModel extends BaseDatabaseModel
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = '##';
|
||||
$lines[] = '## MokoSuite Generated .htaccess';
|
||||
$lines[] = '## MokoWaaS Generated .htaccess';
|
||||
$lines[] = '## Generated: ' . gmdate('Y-m-d H:i:s') . ' UTC';
|
||||
$lines[] = '## DO NOT EDIT — regenerate from MokoSuite > .htaccess Maker';
|
||||
$lines[] = '## DO NOT EDIT — regenerate from MokoWaaS > .htaccess Maker';
|
||||
$lines[] = '##';
|
||||
$lines[] = '';
|
||||
|
||||
@@ -412,7 +412,7 @@ class HtaccessModel extends BaseDatabaseModel
|
||||
public function generateNginx(array $opts): string
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = '## MokoSuite Generated NginX Configuration';
|
||||
$lines[] = '## MokoWaaS Generated NginX Configuration';
|
||||
$lines[] = '## Add these directives inside your server { } block';
|
||||
$lines[] = '';
|
||||
|
||||
+19
-19
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -16,10 +16,10 @@ use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
/**
|
||||
* Importer for migrating from Akeeba Admin Tools to MokoSuite.
|
||||
* Importer for migrating from Akeeba Admin Tools to MokoWaaS.
|
||||
*
|
||||
* Reads Admin Tools WAF config, htaccess settings, IP blocklists,
|
||||
* and security headers — maps them to MokoSuite firewall plugin params
|
||||
* and security headers — maps them to MokoWaaS firewall plugin params
|
||||
* and htaccess maker options.
|
||||
*
|
||||
* @since 02.32.00
|
||||
@@ -94,7 +94,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Import Admin Tools settings into MokoSuite.
|
||||
* Import Admin Tools settings into MokoWaaS.
|
||||
*/
|
||||
public function importAdminTools(): array
|
||||
{
|
||||
@@ -111,7 +111,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
|
||||
if (!empty($firewallParams))
|
||||
{
|
||||
$this->mergePluginParams('mokosuite_firewall', 'system', $firewallParams);
|
||||
$this->mergePluginParams('mokowaas_firewall', 'system', $firewallParams);
|
||||
$results['firewall'] = \count($firewallParams);
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Admin Tools WAF config to MokoSuite firewall plugin params.
|
||||
* Map Admin Tools WAF config to MokoWaaS firewall plugin params.
|
||||
*/
|
||||
private function mapWafToFirewall(array $waf): array
|
||||
{
|
||||
@@ -332,7 +332,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Admin Tools config to MokoSuite htaccess maker options.
|
||||
* Map Admin Tools config to MokoWaaS htaccess maker options.
|
||||
*/
|
||||
private function mapToHtaccess(array $storage, array $waf): array
|
||||
{
|
||||
@@ -448,7 +448,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
|
||||
$db->setQuery($query);
|
||||
$params = new Registry($db->loadResult() ?? '{}');
|
||||
@@ -466,7 +466,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
)->execute();
|
||||
}
|
||||
@@ -481,7 +481,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
|
||||
$db->setQuery($query);
|
||||
@@ -513,7 +513,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
)->execute();
|
||||
@@ -541,7 +541,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
)->execute();
|
||||
|
||||
Log::add('Admin Tools component and plugins disabled after MokoSuite import', Log::INFO, 'mokosuite');
|
||||
Log::add('Admin Tools component and plugins disabled after MokoWaaS import', Log::INFO, 'mokowaas');
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
@@ -619,7 +619,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
)->execute();
|
||||
|
||||
$result['message'] .= ' Akeeba Ticket System has been disabled.';
|
||||
Log::add('Akeeba Ticket System disabled after MokoSuite import', Log::INFO, 'mokosuite');
|
||||
Log::add('Akeeba Ticket System disabled after MokoWaaS import', Log::INFO, 'mokowaas');
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
@@ -644,7 +644,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
);
|
||||
$params = new Registry($db->loadResult() ?? '{}');
|
||||
@@ -666,7 +666,7 @@ class ImportModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
);
|
||||
$params = new Registry($db->loadResult() ?? '{}');
|
||||
@@ -676,13 +676,13 @@ class ImportModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
)->execute();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Import marker error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
|
||||
Log::add('Import marker error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -36,7 +36,7 @@ class MaintenanceModel extends BaseDatabaseModel
|
||||
'engine' => $t->Engine,
|
||||
'size_mb' => $sizeMb,
|
||||
'overhead_kb' => $overheadKb,
|
||||
'is_moko' => str_contains($t->Name, 'mokosuite'),
|
||||
'is_moko' => str_contains($t->Name, 'mokowaas'),
|
||||
];
|
||||
}
|
||||
|
||||
+28
-28
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -30,7 +30,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->quoteName('u.username'),
|
||||
$db->quoteName('p.name', 'processed_by_name'),
|
||||
])
|
||||
->from($db->quoteName('#__mokosuite_data_requests', 'r'))
|
||||
->from($db->quoteName('#__mokowaas_data_requests', 'r'))
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
|
||||
->leftJoin($db->quoteName('#__users', 'p') . ' ON p.id = r.processed_by');
|
||||
|
||||
@@ -68,7 +68,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuite_data_requests', $row, 'id');
|
||||
$db->insertObject('#__mokowaas_data_requests', $row, 'id');
|
||||
|
||||
return ['success' => true, 'message' => ucfirst($type) . ' request #' . $row->id . ' created.', 'id' => (int) $row->id];
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_data_requests'))
|
||||
->from($db->quoteName('#__mokowaas_data_requests'))
|
||||
->where($db->quoteName('id') . ' = ' . $requestId)
|
||||
);
|
||||
$request = $db->loadObject();
|
||||
@@ -104,7 +104,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_data_requests'))
|
||||
->update($db->quoteName('#__mokowaas_data_requests'))
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('denied'))
|
||||
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
|
||||
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
|
||||
@@ -117,7 +117,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
// Mark as processing
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_data_requests'))
|
||||
->update($db->quoteName('#__mokowaas_data_requests'))
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('processing'))
|
||||
->where($db->quoteName('id') . ' = ' . $requestId)
|
||||
)->execute();
|
||||
@@ -143,7 +143,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
// Mark completed
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_data_requests'))
|
||||
->update($db->quoteName('#__mokowaas_data_requests'))
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('completed'))
|
||||
->set($db->quoteName('processed_by') . ' = ' . (int) Factory::getApplication()->getIdentity()->id)
|
||||
->set($db->quoteName('processed') . ' = ' . $db->quote(Factory::getDate()->toSql()))
|
||||
@@ -201,7 +201,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['id', 'subject', 'body', 'status', 'priority', 'created'])
|
||||
->from($db->quoteName('#__mokosuite_tickets'))
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
);
|
||||
$data['tickets'] = $db->loadObjectList() ?: [];
|
||||
@@ -210,7 +210,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select(['r.id', 'r.ticket_id', 'r.body', 'r.created'])
|
||||
->from($db->quoteName('#__mokosuite_ticket_replies', 'r'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_replies', 'r'))
|
||||
->where($db->quoteName('r.user_id') . ' = ' . $userId)
|
||||
);
|
||||
$data['ticket_replies'] = $db->loadObjectList() ?: [];
|
||||
@@ -219,7 +219,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_consent_log'))
|
||||
->from($db->quoteName('#__mokowaas_consent_log'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
->order('created ASC')
|
||||
);
|
||||
@@ -295,7 +295,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
// Anonymize ticket replies
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_ticket_replies'))
|
||||
->update($db->quoteName('#__mokowaas_ticket_replies'))
|
||||
->set($db->quoteName('body') . ' = ' . $db->quote('[Content removed per data request]'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
@@ -374,7 +374,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__mokosuite_tickets'))
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
);
|
||||
$ticketIds = $db->loadColumn() ?: [];
|
||||
@@ -383,13 +383,13 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_ticket_replies'))
|
||||
->delete($db->quoteName('#__mokowaas_ticket_replies'))
|
||||
->where($db->quoteName('ticket_id') . ' IN (' . implode(',', $ticketIds) . ')')
|
||||
)->execute();
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_tickets'))
|
||||
->delete($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
)->execute();
|
||||
}
|
||||
@@ -397,7 +397,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
// Delete consent log
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_consent_log'))
|
||||
->delete($db->quoteName('#__mokowaas_consent_log'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
)->execute();
|
||||
|
||||
@@ -429,7 +429,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_consent_log'))
|
||||
->from($db->quoteName('#__mokowaas_consent_log'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
->order($db->quoteName('created') . ' DESC')
|
||||
);
|
||||
@@ -450,7 +450,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
|
||||
'created' => Factory::getDate()->toSql(),
|
||||
];
|
||||
$db->insertObject('#__mokosuite_consent_log', $row, 'id');
|
||||
$db->insertObject('#__mokowaas_consent_log', $row, 'id');
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
@@ -466,7 +466,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_retention_policies'))
|
||||
->from($db->quoteName('#__mokowaas_retention_policies'))
|
||||
->order($db->quoteName('id') . ' ASC')
|
||||
);
|
||||
|
||||
@@ -508,7 +508,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
case 'waf_logs':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_waf_log'))
|
||||
->delete($db->quoteName('#__mokowaas_waf_log'))
|
||||
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
|
||||
)->execute();
|
||||
$count = $db->getAffectedRows();
|
||||
@@ -528,7 +528,7 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_tickets'))
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('body') . ' = ' . $db->quote('[Removed per retention policy]'))
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('closed'))
|
||||
->where($db->quoteName('closed') . ' < ' . $db->quote($cutoff))
|
||||
@@ -565,12 +565,12 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
{
|
||||
$results['policies_run']++;
|
||||
$results['items_affected'] += $count;
|
||||
Log::add(\sprintf('Retention: %s — %d items affected', $policy->content_type, $count), Log::INFO, 'mokosuite');
|
||||
Log::add(\sprintf('Retention: %s — %d items affected', $policy->content_type, $count), Log::INFO, 'mokowaas');
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Retention policy error (' . $policy->content_type . '): ' . $e->getMessage(), Log::WARNING, 'mokosuite');
|
||||
Log::add('Retention policy error (' . $policy->content_type . '): ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,16 +593,16 @@ class PrivacyModel extends BaseDatabaseModel
|
||||
|
||||
try
|
||||
{
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokosuite_data_requests WHERE status = ' . $db->quote('pending'));
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests WHERE status = ' . $db->quote('pending'));
|
||||
$summary->pending_requests = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokosuite_data_requests');
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_data_requests');
|
||||
$summary->total_requests = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokosuite_consent_log');
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_consent_log');
|
||||
$summary->consent_entries = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokosuite_retention_policies WHERE enabled = 1');
|
||||
$db->setQuery('SELECT COUNT(*) FROM #__mokowaas_retention_policies WHERE enabled = 1');
|
||||
$summary->policies_active = (int) $db->loadResult();
|
||||
}
|
||||
catch (\Throwable $e) {}
|
||||
+98
-457
@@ -1,18 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
use Moko\Component\MokoSuite\Administrator\Service\NotificationService;
|
||||
use Moko\Component\MokoWaaS\Administrator\Service\NotificationService;
|
||||
|
||||
class TicketsModel extends BaseDatabaseModel
|
||||
{
|
||||
@@ -26,40 +26,30 @@ class TicketsModel extends BaseDatabaseModel
|
||||
->select([
|
||||
$db->quoteName('t.id'),
|
||||
$db->quoteName('t.subject'),
|
||||
$db->quoteName('t.status_id'),
|
||||
$db->quoteName('t.priority_id'),
|
||||
$db->quoteName('t.status'),
|
||||
$db->quoteName('t.priority'),
|
||||
$db->quoteName('t.created'),
|
||||
$db->quoteName('t.modified'),
|
||||
$db->quoteName('t.contact_id'),
|
||||
$db->quoteName('t.sla_response_due'),
|
||||
$db->quoteName('t.sla_resolution_due'),
|
||||
$db->quoteName('t.sla_responded'),
|
||||
$db->quoteName('c.title', 'category_title'),
|
||||
$db->quoteName('u.name', 'created_by_name'),
|
||||
$db->quoteName('ct.name', 'contact_name'),
|
||||
$db->quoteName('st.title', 'status_title'),
|
||||
$db->quoteName('st.alias', 'status_alias'),
|
||||
$db->quoteName('st.color', 'status_color'),
|
||||
$db->quoteName('pr.title', 'priority_title'),
|
||||
$db->quoteName('pr.alias', 'priority_alias'),
|
||||
$db->quoteName('pr.color', 'priority_color'),
|
||||
$db->quoteName('st.is_closed', 'status_is_closed'),
|
||||
$db->quoteName('a.name', 'assigned_to_name'),
|
||||
])
|
||||
->from($db->quoteName('#__mokosuite_tickets', 't'))
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
||||
->from($db->quoteName('#__mokowaas_tickets', 't'))
|
||||
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
|
||||
->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id')
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 'st') . ' ON st.id = t.status_id')
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'pr') . ' ON pr.id = t.priority_id');
|
||||
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to');
|
||||
|
||||
if (!empty($filters['status_id']))
|
||||
if (!empty($filters['status']))
|
||||
{
|
||||
$query->where($db->quoteName('t.status_id') . ' = ' . (int) $filters['status_id']);
|
||||
$query->where($db->quoteName('t.status') . ' = ' . $db->quote($filters['status']));
|
||||
}
|
||||
|
||||
if (!empty($filters['priority_id']))
|
||||
if (!empty($filters['priority']))
|
||||
{
|
||||
$query->where($db->quoteName('t.priority_id') . ' = ' . (int) $filters['priority_id']);
|
||||
$query->where($db->quoteName('t.priority') . ' = ' . $db->quote($filters['priority']));
|
||||
}
|
||||
|
||||
if (!empty($filters['assigned_to']))
|
||||
@@ -72,24 +62,12 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$query->where($db->quoteName('t.category_id') . ' = ' . (int) $filters['category_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['contact_id']))
|
||||
{
|
||||
$query->where($db->quoteName('t.contact_id') . ' = ' . (int) $filters['contact_id']);
|
||||
}
|
||||
|
||||
$query->order($db->quoteName('t.created') . ' DESC');
|
||||
$query->setLimit(50);
|
||||
|
||||
$db->setQuery($query);
|
||||
$tickets = $db->loadObjectList() ?: [];
|
||||
|
||||
// Load assignees for each ticket
|
||||
foreach ($tickets as $ticket)
|
||||
{
|
||||
$ticket->assignees = $this->getTicketAssignees((int) $ticket->id);
|
||||
}
|
||||
|
||||
return $tickets;
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,23 +82,12 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->quoteName('c.title', 'category_title'),
|
||||
$db->quoteName('u.name', 'created_by_name'),
|
||||
$db->quoteName('u.email', 'created_by_email'),
|
||||
$db->quoteName('ct.name', 'contact_name'),
|
||||
$db->quoteName('ct.email_to', 'contact_email'),
|
||||
$db->quoteName('ct.telephone', 'contact_phone'),
|
||||
$db->quoteName('st.title', 'status_title'),
|
||||
$db->quoteName('st.alias', 'status_alias'),
|
||||
$db->quoteName('st.color', 'status_color'),
|
||||
$db->quoteName('st.is_closed', 'status_is_closed'),
|
||||
$db->quoteName('pr.title', 'priority_title'),
|
||||
$db->quoteName('pr.alias', 'priority_alias'),
|
||||
$db->quoteName('pr.color', 'priority_color'),
|
||||
$db->quoteName('a.name', 'assigned_to_name'),
|
||||
])
|
||||
->from($db->quoteName('#__mokosuite_tickets', 't'))
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
||||
->from($db->quoteName('#__mokowaas_tickets', 't'))
|
||||
->leftJoin($db->quoteName('#__mokowaas_ticket_categories', 'c') . ' ON c.id = t.category_id')
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = t.created_by')
|
||||
->leftJoin($db->quoteName('#__contact_details', 'ct') . ' ON ct.id = t.contact_id')
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 'st') . ' ON st.id = t.status_id')
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_priorities', 'pr') . ' ON pr.id = t.priority_id')
|
||||
->leftJoin($db->quoteName('#__users', 'a') . ' ON a.id = t.assigned_to')
|
||||
->where($db->quoteName('t.id') . ' = ' . $id);
|
||||
$db->setQuery($query);
|
||||
$ticket = $db->loadObject();
|
||||
@@ -136,7 +103,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->quoteName('r') . '.*',
|
||||
$db->quoteName('u.name', 'user_name'),
|
||||
])
|
||||
->from($db->quoteName('#__mokosuite_ticket_replies', 'r'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_replies', 'r'))
|
||||
->leftJoin($db->quoteName('#__users', 'u') . ' ON u.id = r.user_id')
|
||||
->where($db->quoteName('r.ticket_id') . ' = ' . $id)
|
||||
->order($db->quoteName('r.created') . ' ASC');
|
||||
@@ -146,9 +113,6 @@ class TicketsModel extends BaseDatabaseModel
|
||||
// Reply count
|
||||
$ticket->reply_count = \count($ticket->replies);
|
||||
|
||||
// Load assignees (users + groups)
|
||||
$ticket->assignees = $this->getTicketAssignees($id);
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
@@ -163,23 +127,16 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
// Resolve default status/priority from lookup tables
|
||||
$defaultStatus = $this->getDefaultStatus();
|
||||
$defaultPriority = $this->getDefaultPriority();
|
||||
|
||||
$ticket = (object) [
|
||||
'subject' => $data['subject'] ?? '',
|
||||
'body' => $data['body'] ?? '',
|
||||
'status' => $defaultStatus->alias ?? 'open',
|
||||
'status_id' => (int) ($data['status_id'] ?? $defaultStatus->id ?? 1),
|
||||
'priority' => $defaultPriority->alias ?? 'normal',
|
||||
'priority_id' => (int) ($data['priority_id'] ?? $defaultPriority->id ?? 2),
|
||||
'category_id' => (int) ($data['category_id'] ?? 0) ?: null,
|
||||
'contact_id' => (int) ($data['contact_id'] ?? 0) ?: null,
|
||||
'created_by' => $user->id,
|
||||
'assigned_to' => (int) ($data['assigned_to'] ?? 0) ?: null,
|
||||
'created' => $now,
|
||||
'modified' => $now,
|
||||
'subject' => $data['subject'] ?? '',
|
||||
'body' => $data['body'] ?? '',
|
||||
'status' => 'open',
|
||||
'priority' => $data['priority'] ?? 'normal',
|
||||
'category_id' => (int) ($data['category_id'] ?? 0) ?: null,
|
||||
'created_by' => $user->id,
|
||||
'assigned_to' => (int) ($data['assigned_to'] ?? 0) ?: null,
|
||||
'created' => $now,
|
||||
'modified' => $now,
|
||||
];
|
||||
|
||||
// Auto-assign from category
|
||||
@@ -187,7 +144,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('auto_assign_user'))
|
||||
->from($db->quoteName('#__mokosuite_ticket_categories'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_categories'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id);
|
||||
$db->setQuery($query);
|
||||
$autoAssign = (int) $db->loadResult();
|
||||
@@ -203,7 +160,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->select([$db->quoteName('sla_response_minutes'), $db->quoteName('sla_resolution_minutes')])
|
||||
->from($db->quoteName('#__mokosuite_ticket_categories'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_categories'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $ticket->category_id);
|
||||
$db->setQuery($query);
|
||||
$sla = $db->loadObject();
|
||||
@@ -215,30 +172,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
}
|
||||
}
|
||||
|
||||
$db->insertObject('#__mokosuite_tickets', $ticket, 'id');
|
||||
|
||||
// Handle multi-assignee (users and groups)
|
||||
$assignUsers = array_filter(array_map('intval', (array) ($data['assign_users'] ?? [])));
|
||||
$assignGroups = array_filter(array_map('intval', (array) ($data['assign_groups'] ?? [])));
|
||||
|
||||
// Backward compat: single assigned_to becomes a user assignee
|
||||
if (empty($assignUsers) && $ticket->assigned_to)
|
||||
{
|
||||
$assignUsers = [$ticket->assigned_to];
|
||||
}
|
||||
|
||||
if (!empty($assignUsers) || !empty($assignGroups))
|
||||
{
|
||||
$this->setTicketAssignees((int) $ticket->id, $assignUsers, $assignGroups);
|
||||
}
|
||||
|
||||
// Save custom field values
|
||||
$fieldValues = (array) ($data['custom_fields'] ?? []);
|
||||
|
||||
if (!empty($fieldValues))
|
||||
{
|
||||
$this->saveFieldValues((int) $ticket->id, $fieldValues);
|
||||
}
|
||||
$db->insertObject('#__mokowaas_tickets', $ticket, 'id');
|
||||
|
||||
// Run automation + notifications
|
||||
$this->runAutomation('ticket_created', (int) $ticket->id);
|
||||
@@ -271,14 +205,14 @@ class TicketsModel extends BaseDatabaseModel
|
||||
'created' => $now,
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuite_ticket_replies', $reply, 'id');
|
||||
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
|
||||
|
||||
// Mark SLA as responded only for staff replies (not customer self-replies)
|
||||
$ticket = $this->getTicket($ticketId);
|
||||
$isStaffReply = $ticket && (int) $user->id !== (int) $ticket->created_by;
|
||||
|
||||
$updateQuery = $db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_tickets'))
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId);
|
||||
|
||||
@@ -306,81 +240,62 @@ class TicketsModel extends BaseDatabaseModel
|
||||
}
|
||||
}
|
||||
|
||||
/** @var bool Guard against automation recursion */
|
||||
private bool $automationRunning = false;
|
||||
|
||||
/**
|
||||
* Update ticket status by status ID (lookup table).
|
||||
* Update ticket status.
|
||||
*/
|
||||
public function updateStatus(int $ticketId, int $statusId): array
|
||||
public function updateStatus(int $ticketId, string $status): array
|
||||
{
|
||||
$valid = ['open', 'in_progress', 'waiting', 'resolved', 'closed'];
|
||||
|
||||
if (!\in_array($status, $valid, true))
|
||||
{
|
||||
return ['success' => false, 'message' => 'Invalid status.'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
// Validate status ID against lookup table
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_statuses'))
|
||||
->where($db->quoteName('id') . ' = ' . $statusId)
|
||||
);
|
||||
$status = $db->loadObject();
|
||||
|
||||
if (!$status)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Invalid status.'];
|
||||
}
|
||||
|
||||
// Capture old status for notification
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('status_id'))
|
||||
->from($db->quoteName('#__mokosuite_tickets'))
|
||||
->select($db->quoteName('status'))
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
);
|
||||
$oldStatusId = (int) $db->loadResult();
|
||||
$oldStatus = $db->loadResult() ?? '';
|
||||
|
||||
$sets = [
|
||||
$db->quoteName('status') . ' = ' . $db->quote($status->alias),
|
||||
$db->quoteName('status_id') . ' = ' . $statusId,
|
||||
$db->quoteName('status') . ' = ' . $db->quote($status),
|
||||
$db->quoteName('modified') . ' = ' . $db->quote($now),
|
||||
];
|
||||
|
||||
if ($status->is_closed)
|
||||
{
|
||||
$sets[] = $db->quoteName('closed') . ' = ' . $db->quote($now);
|
||||
}
|
||||
|
||||
// Set resolved timestamp for "resolved" alias (backward compat)
|
||||
if ($status->alias === 'resolved')
|
||||
if ($status === 'resolved')
|
||||
{
|
||||
$sets[] = $db->quoteName('resolved') . ' = ' . $db->quote($now);
|
||||
}
|
||||
|
||||
if ($status === 'closed')
|
||||
{
|
||||
$sets[] = $db->quoteName('closed') . ' = ' . $db->quote($now);
|
||||
}
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_tickets'))
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($sets)
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
)->execute();
|
||||
|
||||
// Run automation + notifications (with recursion guard)
|
||||
if (!$this->automationRunning)
|
||||
{
|
||||
$this->automationRunning = true;
|
||||
$this->runAutomation('status_changed', $ticketId);
|
||||
NotificationService::notify('status_changed', $this->getTicket($ticketId), ['old_status_id' => $oldStatusId]);
|
||||
$this->automationRunning = false;
|
||||
}
|
||||
// Run automation + notifications
|
||||
$this->runAutomation('status_changed', $ticketId);
|
||||
NotificationService::notify('status_changed', $this->getTicket($ticketId), ['old_status' => $oldStatus]);
|
||||
|
||||
return ['success' => true, 'message' => 'Status updated to ' . $status->title . '.'];
|
||||
return ['success' => true, 'message' => 'Status updated to ' . $status . '.'];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->automationRunning = false;
|
||||
|
||||
return ['success' => false, 'message' => 'Failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
@@ -394,7 +309,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_categories'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_categories'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC')
|
||||
);
|
||||
@@ -402,278 +317,6 @@ class TicketsModel extends BaseDatabaseModel
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignees for a ticket (users and groups with resolved names).
|
||||
*/
|
||||
public function getTicketAssignees(int $ticketId): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_assignees'))
|
||||
->where($db->quoteName('ticket_id') . ' = ' . $ticketId)
|
||||
);
|
||||
$rows = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
if ($row->assignee_type === 'user')
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('name'))
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id)
|
||||
);
|
||||
$row->name = (string) $db->loadResult() ?: 'User #' . $row->assignee_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('title'))
|
||||
->from($db->quoteName('#__usergroups'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $row->assignee_id)
|
||||
);
|
||||
$row->name = (string) $db->loadResult() ?: 'Group #' . $row->assignee_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set assignees for a ticket (replaces existing assignments).
|
||||
*
|
||||
* @param int $ticketId Ticket ID
|
||||
* @param array $users Array of user IDs
|
||||
* @param array $groups Array of user group IDs
|
||||
*/
|
||||
public function setTicketAssignees(int $ticketId, array $users = [], array $groups = []): void
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
// Clear existing
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_ticket_assignees'))
|
||||
->where($db->quoteName('ticket_id') . ' = ' . $ticketId)
|
||||
)->execute();
|
||||
|
||||
// Insert users
|
||||
foreach ($users as $uid)
|
||||
{
|
||||
$uid = (int) $uid;
|
||||
|
||||
if ($uid > 0)
|
||||
{
|
||||
$db->insertObject('#__mokosuite_ticket_assignees', (object) [
|
||||
'ticket_id' => $ticketId,
|
||||
'assignee_type' => 'user',
|
||||
'assignee_id' => $uid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert groups
|
||||
foreach ($groups as $gid)
|
||||
{
|
||||
$gid = (int) $gid;
|
||||
|
||||
if ($gid > 0)
|
||||
{
|
||||
$db->insertObject('#__mokosuite_ticket_assignees', (object) [
|
||||
'ticket_id' => $ticketId,
|
||||
'assignee_type' => 'group',
|
||||
'assignee_id' => $gid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all published Joomla contact records for ticket linking.
|
||||
*/
|
||||
public function getContacts(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('id'), $db->quoteName('name')])
|
||||
->from($db->quoteName('#__contact_details'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order($db->quoteName('name') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default ticket status.
|
||||
*/
|
||||
public function getDefaultStatus(): ?object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_statuses'))
|
||||
->where($db->quoteName('is_default') . ' = 1')
|
||||
->setLimit(1)
|
||||
);
|
||||
|
||||
return $db->loadObject() ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default ticket priority.
|
||||
*/
|
||||
public function getDefaultPriority(): ?object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_priorities'))
|
||||
->where($db->quoteName('is_default') . ' = 1')
|
||||
->setLimit(1)
|
||||
);
|
||||
|
||||
return $db->loadObject() ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all ticket statuses.
|
||||
*/
|
||||
public function getStatuses(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_statuses'))
|
||||
->order($db->quoteName('ordering') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all ticket priorities.
|
||||
*/
|
||||
public function getPriorities(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_priorities'))
|
||||
->order($db->quoteName('ordering') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Joomla custom field groups assigned to a ticket category.
|
||||
*/
|
||||
public function getFieldGroupsForCategory(int $categoryId): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('fg.id'), $db->quoteName('fg.title')])
|
||||
->from($db->quoteName('#__mokosuite_ticket_category_field_groups', 'cfg'))
|
||||
->innerJoin($db->quoteName('#__fields_groups', 'fg') . ' ON fg.id = cfg.field_group_id')
|
||||
->where($db->quoteName('cfg.category_id') . ' = ' . $categoryId)
|
||||
->where($db->quoteName('fg.state') . ' = 1')
|
||||
->order($db->quoteName('fg.ordering') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Joomla custom fields for given field group IDs (context: com_mokosuite.ticket).
|
||||
*/
|
||||
public function getFieldsForGroups(array $groupIds): array
|
||||
{
|
||||
if (empty($groupIds))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$db = $this->getDatabase();
|
||||
$ids = implode(',', array_map('intval', $groupIds));
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__fields'))
|
||||
->where($db->quoteName('context') . ' = ' . $db->quote('com_mokosuite.ticket'))
|
||||
->where($db->quoteName('group_id') . ' IN (' . $ids . ')')
|
||||
->where($db->quoteName('state') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC')
|
||||
);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom field values for a ticket.
|
||||
*/
|
||||
public function getFieldValues(int $ticketId): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('field_id'), $db->quoteName('value')])
|
||||
->from($db->quoteName('#__fields_values'))
|
||||
->where($db->quoteName('item_id') . ' = ' . $db->quote((string) $ticketId))
|
||||
);
|
||||
$rows = $db->loadObjectList() ?: [];
|
||||
|
||||
$values = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$values[(int) $row->field_id] = $row->value;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save custom field values for a ticket.
|
||||
*/
|
||||
public function saveFieldValues(int $ticketId, array $fieldValues): void
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
|
||||
foreach ($fieldValues as $fieldId => $value)
|
||||
{
|
||||
$fieldId = (int) $fieldId;
|
||||
|
||||
// Delete existing
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__fields_values'))
|
||||
->where($db->quoteName('field_id') . ' = ' . $fieldId)
|
||||
->where($db->quoteName('item_id') . ' = ' . $db->quote((string) $ticketId))
|
||||
)->execute();
|
||||
|
||||
// Insert new value (skip empty)
|
||||
if ($value !== '' && $value !== null)
|
||||
{
|
||||
$db->insertObject('#__fields_values', (object) [
|
||||
'field_id' => $fieldId,
|
||||
'item_id' => (string) $ticketId,
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canned responses, optionally filtered by category.
|
||||
*/
|
||||
@@ -682,7 +325,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_canned'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_canned'))
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
|
||||
if ($categoryId)
|
||||
@@ -699,26 +342,25 @@ class TicketsModel extends BaseDatabaseModel
|
||||
/**
|
||||
* Get ticket counts by status for dashboard.
|
||||
*/
|
||||
public function getStatusCounts(): array
|
||||
public function getStatusCounts(): object
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('s.id'),
|
||||
$db->quoteName('s.title'),
|
||||
$db->quoteName('s.alias'),
|
||||
$db->quoteName('s.color'),
|
||||
$db->quoteName('s.is_closed'),
|
||||
'COUNT(' . $db->quoteName('t.id') . ') AS ' . $db->quoteName('cnt'),
|
||||
])
|
||||
->from($db->quoteName('#__mokosuite_ticket_statuses', 's'))
|
||||
->leftJoin($db->quoteName('#__mokosuite_tickets', 't') . ' ON t.status_id = s.id')
|
||||
->group($db->quoteName('s.id'))
|
||||
->order($db->quoteName('s.ordering') . ' ASC')
|
||||
->select([$db->quoteName('status'), 'COUNT(*) AS ' . $db->quoteName('cnt')])
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->group($db->quoteName('status'))
|
||||
);
|
||||
$rows = $db->loadObjectList('status') ?: [];
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
return (object) [
|
||||
'open' => (int) ($rows['open']->cnt ?? 0),
|
||||
'in_progress' => (int) ($rows['in_progress']->cnt ?? 0),
|
||||
'waiting' => (int) ($rows['waiting']->cnt ?? 0),
|
||||
'resolved' => (int) ($rows['resolved']->cnt ?? 0),
|
||||
'closed' => (int) ($rows['closed']->cnt ?? 0),
|
||||
'total' => array_sum(array_map(fn($r) => (int) $r->cnt, $rows)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -730,11 +372,10 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select(['t.' . $db->quoteName('id'), $db->quoteName('t.subject'), $db->quoteName('t.priority'),
|
||||
$db->quoteName('t.sla_response_due'), $db->quoteName('t.sla_resolution_due'), $db->quoteName('t.sla_responded')])
|
||||
->from($db->quoteName('#__mokosuite_tickets', 't'))
|
||||
->leftJoin($db->quoteName('#__mokosuite_ticket_statuses', 's') . ' ON s.id = t.status_id')
|
||||
->where('(' . $db->quoteName('s.is_closed') . ' = 0 OR ' . $db->quoteName('s.is_closed') . ' IS NULL)')
|
||||
->select([$db->quoteName('id'), $db->quoteName('subject'), $db->quoteName('priority'),
|
||||
$db->quoteName('sla_response_due'), $db->quoteName('sla_resolution_due'), $db->quoteName('sla_responded')])
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
|
||||
->where('((' . $db->quoteName('sla_response_due') . ' < ' . $db->quote($now) . ' AND ' . $db->quoteName('sla_responded') . ' = 0)'
|
||||
. ' OR ' . $db->quoteName('sla_resolution_due') . ' < ' . $db->quote($now) . ')')
|
||||
->order($db->quoteName('sla_resolution_due') . ' ASC');
|
||||
@@ -762,7 +403,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
// Load enabled rules for this event
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_automation'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_automation'))
|
||||
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
|
||||
->where($db->quoteName('enabled') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
@@ -798,7 +439,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
\Joomla\CMS\Log\Log::add('Automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuite');
|
||||
\Joomla\CMS\Log\Log::add('Automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,7 +454,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
// Load scheduled rules
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_automation'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_automation'))
|
||||
->where($db->quoteName('trigger_event') . ' = ' . $db->quote('scheduled'))
|
||||
->where($db->quoteName('enabled') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
@@ -828,7 +469,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
// Load all non-closed tickets
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_tickets'))
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('status') . ' != ' . $db->quote('closed'));
|
||||
$db->setQuery($query);
|
||||
$tickets = $db->loadObjectList() ?: [];
|
||||
@@ -925,7 +566,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
case 'set_priority':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_tickets'))
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('priority') . ' = ' . $db->quote($value))
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
@@ -935,7 +576,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
case 'assign':
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuite_tickets'))
|
||||
->update($db->quoteName('#__mokowaas_tickets'))
|
||||
->set($db->quoteName('assigned_to') . ' = ' . (int) $value)
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $ticketId)
|
||||
@@ -950,7 +591,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
'is_internal' => 1,
|
||||
'created' => $now,
|
||||
];
|
||||
$db->insertObject('#__mokosuite_ticket_replies', $reply, 'id');
|
||||
$db->insertObject('#__mokowaas_ticket_replies', $reply, 'id');
|
||||
break;
|
||||
|
||||
case 'send_email':
|
||||
@@ -970,7 +611,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
\Joomla\CMS\Log\Log::add('Automation email failed: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuite');
|
||||
\Joomla\CMS\Log\Log::add('Automation email failed: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -988,7 +629,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__mokosuite_tickets'))
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
|
||||
->where($catId ? $db->quoteName('category_id') . ' = ' . $catId : '1=1')
|
||||
@@ -1008,7 +649,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokosuite_tickets'))
|
||||
->from($db->quoteName('#__mokowaas_tickets'))
|
||||
->where($db->quoteName('created_by') . ' = ' . $userId)
|
||||
->where($db->quoteName('status') . ' NOT IN (' . $db->quote('resolved') . ',' . $db->quote('closed') . ')')
|
||||
);
|
||||
@@ -1043,7 +684,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_automation'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_automation'))
|
||||
->where($db->quoteName('trigger_event') . ' = ' . $db->quote($event))
|
||||
->where($db->quoteName('enabled') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
@@ -1080,7 +721,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
\Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokosuite');
|
||||
\Joomla\CMS\Log\Log::add('System event automation error: ' . $e->getMessage(), \Joomla\CMS\Log\Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,7 +734,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_ticket_automation'))
|
||||
->from($db->quoteName('#__mokowaas_ticket_automation'))
|
||||
->order($db->quoteName('ordering') . ' ASC')
|
||||
);
|
||||
|
||||
@@ -1140,7 +781,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
|
||||
try
|
||||
{
|
||||
// Status mapping: ATS → MokoSuite
|
||||
// Status mapping: ATS → MokoWaaS
|
||||
$statusMap = [
|
||||
'O' => 'open', // Open
|
||||
'P' => 'in_progress', // Pending (staff action needed)
|
||||
@@ -1174,7 +815,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$exists = $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuite_ticket_canned')
|
||||
->from('#__mokowaas_ticket_canned')
|
||||
->where($db->quoteName('title') . ' = ' . $db->quote($c->title))
|
||||
)->loadResult();
|
||||
|
||||
@@ -1189,7 +830,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
'category_id' => null,
|
||||
'ordering' => (int) ($c->ordering ?? 0),
|
||||
];
|
||||
$db->insertObject('#__mokosuite_ticket_canned', $row, 'id');
|
||||
$db->insertObject('#__mokowaas_ticket_canned', $row, 'id');
|
||||
$results['canned']++;
|
||||
}
|
||||
|
||||
@@ -1197,7 +838,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$db->setQuery('SELECT * FROM #__ats_tickets ORDER BY id');
|
||||
$atsTickets = $db->loadObjectList() ?: [];
|
||||
|
||||
$ticketIdMap = []; // ATS id → MokoSuite id
|
||||
$ticketIdMap = []; // ATS id → MokoWaaS id
|
||||
|
||||
foreach ($atsTickets as $t)
|
||||
{
|
||||
@@ -1205,7 +846,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$exists = $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from('#__mokosuite_tickets')
|
||||
->from('#__mokowaas_tickets')
|
||||
->where($db->quoteName('subject') . ' = ' . $db->quote($t->title))
|
||||
->where($db->quoteName('created_by') . ' = ' . (int) $t->created_by)
|
||||
)->loadResult();
|
||||
@@ -1233,7 +874,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
'sla_responded' => 1,
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuite_tickets', $row, 'id');
|
||||
$db->insertObject('#__mokowaas_tickets', $row, 'id');
|
||||
$ticketIdMap[(int) $t->id] = (int) $row->id;
|
||||
$results['tickets']++;
|
||||
}
|
||||
@@ -1258,7 +899,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
$body = strip_tags($p->content_html ?? '');
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update('#__mokosuite_tickets')
|
||||
->update('#__mokowaas_tickets')
|
||||
->set($db->quoteName('body') . ' = ' . $db->quote($body))
|
||||
->where($db->quoteName('id') . ' = ' . $newTicketId)
|
||||
)->execute();
|
||||
@@ -1274,7 +915,7 @@ class TicketsModel extends BaseDatabaseModel
|
||||
'created' => $p->created ?: Factory::getDate()->toSql(),
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuite_ticket_replies', $row, 'id');
|
||||
$db->insertObject('#__mokowaas_ticket_replies', $row, 'id');
|
||||
$results['replies']++;
|
||||
}
|
||||
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Model;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -23,7 +23,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokosuite_waf_log'));
|
||||
->from($db->quoteName('#__mokowaas_waf_log'));
|
||||
|
||||
if (!empty($filters['rule']))
|
||||
{
|
||||
@@ -69,7 +69,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
$db = $this->getDatabase();
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokosuite_waf_log'));
|
||||
->from($db->quoteName('#__mokowaas_waf_log'));
|
||||
|
||||
if (!empty($filters['rule']))
|
||||
{
|
||||
@@ -95,7 +95,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('rule'), 'COUNT(*) AS ' . $db->quoteName('cnt')])
|
||||
->from($db->quoteName('#__mokosuite_waf_log'))
|
||||
->from($db->quoteName('#__mokowaas_waf_log'))
|
||||
->group($db->quoteName('rule'))
|
||||
->order($db->quoteName('cnt') . ' DESC')
|
||||
);
|
||||
@@ -113,7 +113,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('ip'), 'COUNT(*) AS ' . $db->quoteName('cnt'),
|
||||
'MAX(' . $db->quoteName('created') . ') AS ' . $db->quoteName('last_seen')])
|
||||
->from($db->quoteName('#__mokosuite_waf_log'))
|
||||
->from($db->quoteName('#__mokowaas_waf_log'))
|
||||
->group($db->quoteName('ip'))
|
||||
->order($db->quoteName('cnt') . ' DESC')
|
||||
->setLimit($limit)
|
||||
@@ -131,7 +131,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('DISTINCT ' . $db->quoteName('rule'))
|
||||
->from($db->quoteName('#__mokosuite_waf_log'))
|
||||
->from($db->quoteName('#__mokowaas_waf_log'))
|
||||
->order($db->quoteName('rule') . ' ASC')
|
||||
);
|
||||
|
||||
@@ -150,7 +150,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__mokosuite_waf_log'))
|
||||
->delete($db->quoteName('#__mokowaas_waf_log'))
|
||||
->where($db->quoteName('created') . ' < ' . $db->quote($cutoff))
|
||||
)->execute();
|
||||
|
||||
@@ -176,7 +176,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'));
|
||||
$db->setQuery($query);
|
||||
@@ -200,7 +200,7 @@ class WaflogModel extends BaseDatabaseModel
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote($params->toString()))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuite_firewall'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
)->execute();
|
||||
+11
-11
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\Service;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\Service;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -67,13 +67,13 @@ class NotificationService
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokosuite');
|
||||
Log::add('Notification send failed to ' . $email . ': ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
|
||||
Log::add('Notification error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ class NotificationService
|
||||
{
|
||||
$siteName = Factory::getConfig()->get('sitename', 'Support');
|
||||
$siteUrl = rtrim(Uri::root(), '/');
|
||||
$ticketUrl = $siteUrl . '/index.php?option=com_mokosuite&view=ticket&id=' . $ticket->id;
|
||||
$ticketUrl = $siteUrl . '/index.php?option=com_mokowaas&view=ticket&id=' . $ticket->id;
|
||||
|
||||
$lines = [];
|
||||
$lines[] = $siteName . ' Support';
|
||||
@@ -273,7 +273,7 @@ class NotificationService
|
||||
$lines[] = 'View ticket: ' . $ticketUrl;
|
||||
$lines[] = '';
|
||||
$lines[] = '-- ';
|
||||
$lines[] = $siteName . ' | Powered by MokoSuite';
|
||||
$lines[] = $siteName . ' | Powered by MokoWaaS';
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
@@ -318,7 +318,7 @@ class NotificationService
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
);
|
||||
|
||||
@@ -386,7 +386,7 @@ class NotificationService
|
||||
$body,
|
||||
'',
|
||||
'-- ',
|
||||
$siteName . ' | MokoSuite Security',
|
||||
$siteName . ' | MokoWaaS Security',
|
||||
];
|
||||
|
||||
$mailer = Factory::getMailer();
|
||||
@@ -404,13 +404,13 @@ class NotificationService
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
|
||||
Log::add('Security alert send failed: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokosuite');
|
||||
Log::add('Security alert error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Automation;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Automation;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -13,14 +13,14 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\TicketsModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\TicketsModel();
|
||||
$this->rules = $model->getAutomationRules();
|
||||
|
||||
ToolbarHelper::title('Automation Rules', 'cogs');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Canned;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Canned;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -16,17 +16,17 @@ class HtmlView extends BaseHtmlView
|
||||
{
|
||||
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
|
||||
|
||||
$db->setQuery('SELECT * FROM #__mokosuite_ticket_canned ORDER BY ordering ASC');
|
||||
$db->setQuery('SELECT * FROM #__mokowaas_ticket_canned ORDER BY ordering ASC');
|
||||
$this->responses = $db->loadObjectList() ?: [];
|
||||
|
||||
$db->setQuery('SELECT id, title FROM #__mokosuite_ticket_categories WHERE published = 1 ORDER BY ordering');
|
||||
$db->setQuery('SELECT id, title FROM #__mokowaas_ticket_categories WHERE published = 1 ORDER BY ordering');
|
||||
$this->categories = $db->loadObjectList() ?: [];
|
||||
|
||||
ToolbarHelper::title('Canned Responses', 'comment');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Categories;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Categories;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -16,7 +16,7 @@ class HtmlView extends BaseHtmlView
|
||||
{
|
||||
$db = Factory::getContainer()->get('Joomla\Database\DatabaseInterface');
|
||||
|
||||
$db->setQuery('SELECT * FROM #__mokosuite_ticket_categories ORDER BY ordering ASC');
|
||||
$db->setQuery('SELECT * FROM #__mokowaas_ticket_categories ORDER BY ordering ASC');
|
||||
$this->categories = $db->loadObjectList() ?: [];
|
||||
|
||||
// Get admin users for auto-assign dropdown
|
||||
@@ -31,10 +31,10 @@ class HtmlView extends BaseHtmlView
|
||||
$this->users = $db->loadObjectList() ?: [];
|
||||
|
||||
ToolbarHelper::title('Ticket Categories', 'folder');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Cleanup;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Cleanup;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -13,14 +13,14 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
|
||||
$this->dirs = $model->getCleanupInfo();
|
||||
|
||||
ToolbarHelper::title('Cache & Temp Cleanup', 'trash');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
+9
-9
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Dashboard;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Dashboard;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -44,7 +44,7 @@ class HtmlView extends BaseHtmlView
|
||||
// Check for importable Akeeba data
|
||||
try
|
||||
{
|
||||
$importModel = new \Moko\Component\MokoSuite\Administrator\Model\ImportModel();
|
||||
$importModel = new \Moko\Component\MokoWaaS\Administrator\Model\ImportModel();
|
||||
$this->adminToolsAvailable = $importModel->checkAdminToolsAvailable();
|
||||
$this->atsAvailable = $importModel->checkAtsAvailable();
|
||||
}
|
||||
@@ -57,21 +57,21 @@ class HtmlView extends BaseHtmlView
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseScript('com_mokosuite.dashboard', 'com_mokosuite/dashboard.js', [], ['defer' => true]);
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
$wa->registerAndUseScript('com_mokowaas.dashboard', 'com_mokowaas/dashboard.js', [], ['defer' => true]);
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOSUITE_DASHBOARD_TITLE'), 'cogs');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOWAAS_DASHBOARD_TITLE'), 'cogs');
|
||||
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
if ($user->authorise('core.admin', 'com_mokosuite'))
|
||||
if ($user->authorise('core.admin', 'com_mokowaas'))
|
||||
{
|
||||
ToolbarHelper::preferences('com_mokosuite');
|
||||
ToolbarHelper::preferences('com_mokowaas');
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Database;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Database;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -13,14 +13,14 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\MaintenanceModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\MaintenanceModel();
|
||||
$this->tableData = $model->getTableStatus();
|
||||
|
||||
ToolbarHelper::title('Database Tools', 'database');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Extensions;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Extensions;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -28,14 +28,14 @@ class HtmlView extends BaseHtmlView
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOSUITE_EXTENSIONS_TITLE'), 'puzzle-piece');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOWAAS_EXTENSIONS_TITLE'), 'puzzle-piece');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Htaccess;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Htaccess;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -34,14 +34,14 @@ class HtmlView extends BaseHtmlView
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOSUITE_HTACCESS_TITLE'), 'file-code');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOWAAS_HTACCESS_TITLE'), 'file-code');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Privacy;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Privacy;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -16,7 +16,7 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\PrivacyModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
|
||||
$filterStatus = Factory::getApplication()->getInput()->getString('filter_status', '');
|
||||
$this->requests = $model->getDataRequests($filterStatus);
|
||||
@@ -26,7 +26,7 @@ class HtmlView extends BaseHtmlView
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
@@ -34,6 +34,6 @@ class HtmlView extends BaseHtmlView
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title('Privacy Guard', 'lock');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
}
|
||||
}
|
||||
+6
-21
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Ticket;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Ticket;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -19,10 +19,6 @@ class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $ticket;
|
||||
protected $cannedResponses = [];
|
||||
protected $statuses = [];
|
||||
protected $priorities = [];
|
||||
protected $customFields = [];
|
||||
protected $fieldValues = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
@@ -31,22 +27,11 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
$this->ticket = $model->getTicket($id);
|
||||
$this->cannedResponses = $model->getCannedResponses((int) ($this->ticket->category_id ?? 0));
|
||||
$this->statuses = $model->getStatuses();
|
||||
$this->priorities = $model->getPriorities();
|
||||
|
||||
// Load custom fields for this ticket's category
|
||||
if ($this->ticket && $this->ticket->category_id)
|
||||
{
|
||||
$groups = $model->getFieldGroupsForCategory((int) $this->ticket->category_id);
|
||||
$groupIds = array_column($groups, 'id');
|
||||
$this->customFields = $model->getFieldsForGroups($groupIds);
|
||||
$this->fieldValues = $model->getFieldValues($id);
|
||||
}
|
||||
|
||||
if (!$this->ticket)
|
||||
{
|
||||
Factory::getApplication()->enqueueMessage('Ticket not found.', 'error');
|
||||
Factory::getApplication()->redirect('index.php?option=com_mokosuite&view=tickets');
|
||||
Factory::getApplication()->redirect('index.php?option=com_mokowaas&view=tickets');
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +39,7 @@ class HtmlView extends BaseHtmlView
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
@@ -63,6 +48,6 @@ class HtmlView extends BaseHtmlView
|
||||
{
|
||||
$title = $this->ticket ? 'Ticket #' . $this->ticket->id . ' — ' . $this->ticket->subject : 'Ticket';
|
||||
ToolbarHelper::title($title, 'headphones');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite&view=tickets');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas&view=tickets');
|
||||
}
|
||||
}
|
||||
+8
-15
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuite
|
||||
* @subpackage com_mokosuite
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Tickets;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Tickets;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -22,9 +22,6 @@ class HtmlView extends BaseHtmlView
|
||||
protected $statusCounts;
|
||||
protected $overdue = [];
|
||||
protected $atsAvailable = null;
|
||||
protected $contacts = [];
|
||||
protected $statuses = [];
|
||||
protected $priorities = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
@@ -32,32 +29,28 @@ class HtmlView extends BaseHtmlView
|
||||
$app = Factory::getApplication();
|
||||
|
||||
$filters = [
|
||||
'status_id' => $app->getInput()->getInt('filter_status', 0),
|
||||
'priority_id' => $app->getInput()->getInt('filter_priority', 0),
|
||||
'status' => $app->getInput()->getString('filter_status', ''),
|
||||
'priority' => $app->getInput()->getString('filter_priority', ''),
|
||||
'category_id' => $app->getInput()->getInt('filter_category', 0),
|
||||
'contact_id' => $app->getInput()->getInt('filter_contact', 0),
|
||||
];
|
||||
|
||||
$this->tickets = $model->getTickets($filters);
|
||||
$this->categories = $model->getCategories();
|
||||
$this->statuses = $model->getStatuses();
|
||||
$this->priorities = $model->getPriorities();
|
||||
$this->statusCounts = $model->getStatusCounts();
|
||||
$this->overdue = $model->getOverdueTickets();
|
||||
$this->atsAvailable = $model->checkAtsAvailable();
|
||||
$this->contacts = $model->getContacts();
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOSUITE_TICKETS_TITLE'), 'headphones');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
|
||||
ToolbarHelper::title(Text::_('COM_MOKOWAAS_TICKETS_TITLE'), 'headphones');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuite\Administrator\View\Waflog;
|
||||
namespace Moko\Component\MokoWaaS\Administrator\View\Waflog;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -18,7 +18,7 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = new \Moko\Component\MokoSuite\Administrator\Model\WaflogModel();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel();
|
||||
$input = Factory::getApplication()->getInput();
|
||||
|
||||
$this->filters = [
|
||||
@@ -42,7 +42,7 @@ class HtmlView extends BaseHtmlView
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokosuite.dashboard', 'com_mokosuite/dashboard.css');
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
@@ -50,6 +50,6 @@ class HtmlView extends BaseHtmlView
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title('WAF Log Viewer', 'shield-alt');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuite');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -6,14 +6,14 @@ use Joomla\CMS\Session\Session;
|
||||
|
||||
$rules = $this->rules;
|
||||
$token = Session::getFormToken();
|
||||
$saveUrl = Route::_('index.php?option=com_mokosuite&task=display.saveAutomation&format=json');
|
||||
$deleteUrl = Route::_('index.php?option=com_mokosuite&task=display.deleteAutomation&format=json');
|
||||
$toggleUrl = Route::_('index.php?option=com_mokosuite&task=display.toggleAutomation&format=json');
|
||||
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveAutomation&format=json');
|
||||
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteAutomation&format=json');
|
||||
$toggleUrl = Route::_('index.php?option=com_mokowaas&task=display.toggleAutomation&format=json');
|
||||
|
||||
$triggerLabels = ['ticket_created' => 'On Ticket Created', 'ticket_replied' => 'On Reply', 'status_changed' => 'On Status Change', 'scheduled' => 'Scheduled (Cron)'];
|
||||
?>
|
||||
|
||||
<div id="mokosuite-automation">
|
||||
<div id="mokowaas-automation">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4><?php echo count($rules); ?> Automation Rules</h4>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newRuleModal">
|
||||
+3
-3
@@ -7,11 +7,11 @@ use Joomla\CMS\Session\Session;
|
||||
$responses = $this->responses;
|
||||
$categories = $this->categories;
|
||||
$token = Session::getFormToken();
|
||||
$saveUrl = Route::_('index.php?option=com_mokosuite&task=display.saveCanned&format=json');
|
||||
$deleteUrl = Route::_('index.php?option=com_mokosuite&task=display.deleteCanned&format=json');
|
||||
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveCanned&format=json');
|
||||
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCanned&format=json');
|
||||
?>
|
||||
|
||||
<div id="mokosuite-canned">
|
||||
<div id="mokowaas-canned">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4><?php echo count($responses); ?> Canned Responses</h4>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newCannedModal">
|
||||
+3
-3
@@ -7,11 +7,11 @@ use Joomla\CMS\Session\Session;
|
||||
$categories = $this->categories;
|
||||
$users = $this->users;
|
||||
$token = Session::getFormToken();
|
||||
$saveUrl = Route::_('index.php?option=com_mokosuite&task=display.saveCategory&format=json');
|
||||
$deleteUrl = Route::_('index.php?option=com_mokosuite&task=display.deleteCategory&format=json');
|
||||
$saveUrl = Route::_('index.php?option=com_mokowaas&task=display.saveCategory&format=json');
|
||||
$deleteUrl = Route::_('index.php?option=com_mokowaas&task=display.deleteCategory&format=json');
|
||||
?>
|
||||
|
||||
<div id="mokosuite-categories">
|
||||
<div id="mokowaas-categories">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4><?php echo count($categories); ?> Categories</h4>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn-add-cat">
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user