Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1ee2ef3f4 | |||
| 7f9b59a36d | |||
| 79047e37b5 | |||
| 3d5f9346c6 | |||
| 93c82a9cee | |||
| 384b8824c6 | |||
| e01791ae68 | |||
| e42d6e7596 | |||
| 36658fa8ca | |||
| 5645516845 | |||
| 4ce8c6b4ea | |||
| 01056afe74 | |||
| 3cc39cfa8f | |||
| 0956757445 | |||
| 9c75d0254e | |||
| c847b4a274 | |||
| c93ae27b64 | |||
| 0e28958ede | |||
| 46bb7c31c2 | |||
| 04af4a93a8 | |||
| 3b99c5b6bc | |||
| 1b47876a6c | |||
| 48ff2b2109 | |||
| 0c4857d6e0 | |||
| 9f3e4b9d31 | |||
| 3834ba4c1c | |||
| a8a41e9bad | |||
| 8c927b0a1b | |||
| 21e57eaadc | |||
| fadd3a01cd | |||
| 95097c4d3f | |||
| e71b075d94 | |||
| 1ecc8be8d1 | |||
| 361a58f8cd | |||
| 51ac178281 | |||
| b46da78e6c | |||
| 57a54e8959 | |||
| 1c8625f828 | |||
| 66b19f184c | |||
| 4694e67e1c | |||
| e2e2ac8b56 | |||
| 415eeaac56 | |||
| 4c8bb93952 | |||
| 561fdcd881 | |||
| 0d096acfa8 | |||
| 3db14d29ef | |||
| dfff3c327f | |||
| 4ab3b163f6 | |||
| 60910c2b8b | |||
| 4e0151be1b | |||
| 36d958f31f | |||
| e482a293c9 | |||
| 04a4bf8aba | |||
| b3082f27e3 | |||
| f30d7dd7af | |||
| ecd5b6c786 | |||
| 1f7419f33d | |||
| 171f489e3d | |||
| e808a168cb | |||
| 1f89a323d5 | |||
| 329eca3db6 | |||
| 42b47be564 | |||
| 79ac068bc4 | |||
| c5aef3c939 | |||
| 0d96174f75 | |||
| 6acae6d20f | |||
| 9dacc01a67 | |||
| c7d914f786 | |||
| 729aa3850d |
@@ -1,4 +0,0 @@
|
||||
[submodule "src/packages/tpl_mokoonyx"]
|
||||
path = src/packages/tpl_mokoonyx
|
||||
url = https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
|
||||
branch = main
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<display-name>Package - MokoWaaS</display-name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
|
||||
<version>02.33.01</version>
|
||||
<version>02.34.10</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -48,15 +48,12 @@ jobs:
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
|
||||
@@ -1,318 +1,316 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +========================================================================+
|
||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: false
|
||||
type: choice
|
||||
default: release
|
||||
options:
|
||||
- release
|
||||
- promote-rc
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
run: |
|
||||
git fetch origin rc
|
||||
git checkout rc
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found — aborting release"
|
||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
# Ensure PHP + Composer are available
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||
else
|
||||
NOTES="Stable release"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
--branch main 2>&1 || true
|
||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||
- name: "Step 10: Push main to GitHub mirror"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||
git fetch origin main --depth=1
|
||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
|
||||
# Delete old version branch if it exists (same version re-release)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||
|
||||
# Create version/XX.YY.ZZ from main
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||
|
||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
|
||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||
- name: "Post-release: Reset dev version"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +========================================================================+
|
||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
# | Platform-specific: |
|
||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
description: 'Action to perform'
|
||||
required: false
|
||||
type: choice
|
||||
default: release
|
||||
options:
|
||||
- release
|
||||
- promote-rc
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Rename branch to rc
|
||||
run: |
|
||||
php ${MOKO_CLI}/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
run: |
|
||||
git fetch origin rc
|
||||
git checkout rc
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Publish RC release
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability rc --bump minor --branch rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event.pull_request.merged == true ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git for bot pushes
|
||||
run: |
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
- name: Check for merge conflict markers
|
||||
run: |
|
||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||
if [ -n "$CONFLICTS" ]; then
|
||||
echo "::error::Merge conflict markers found - aborting release"
|
||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
echo "No conflict markers found"
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api
|
||||
composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Publish stable release"
|
||||
run: |
|
||||
php ${MOKO_CLI}/release_publish.php \
|
||||
--path . --stability stable --bump minor --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--skip-update-stream
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||
else
|
||||
NOTES="Stable release"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$RELEASE_ID" ]; then
|
||||
python3 -c "
|
||||
import json, urllib.request
|
||||
body = open('/dev/stdin').read()
|
||||
payload = json.dumps({'body': body}).encode()
|
||||
req = urllib.request.Request(
|
||||
'${API_BASE}/releases/${RELEASE_ID}',
|
||||
data=payload, method='PATCH',
|
||||
headers={
|
||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
urllib.request.urlopen(req)
|
||||
" <<< "$NOTES"
|
||||
echo "Release notes updated from CHANGELOG.md"
|
||||
fi
|
||||
|
||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||
- name: "Step 9: Mirror release to GitHub"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||
--branch main 2>&1 || true
|
||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||
- name: "Step 10: Push main to GitHub mirror"
|
||||
if: >-
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
secrets.GH_MIRROR_TOKEN != ''
|
||||
continue-on-error: true
|
||||
run: |
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||
git fetch origin main --depth=1
|
||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||
&& echo "main branch pushed to GitHub mirror" \
|
||||
|| echo "WARNING: GitHub mirror push failed"
|
||||
|
||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral - created by promote-rc)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||
|
||||
# Delete dev branch
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||
|
||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/branches" \
|
||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||
|
||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: "Step 12: Create version branch from main"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
MAIN_SHA=$(git rev-parse HEAD)
|
||||
|
||||
# Delete old version branch if it exists (same version re-release)
|
||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||
|
||||
# Create version/XX.YY.ZZ from main
|
||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||
|
||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
||||
|
||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||
- name: "Post-release: Reset dev version"
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
|
||||
# -- Summary --------------------------------------------------------------
|
||||
- name: Pipeline Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||
echo "## Already Released - ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 02.33.01
|
||||
# VERSION: 02.34.10
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -66,7 +66,6 @@ jobs:
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||
rm -rf /tmp/moko-platform-api
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||
@@ -96,31 +95,23 @@ jobs:
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Read current version (bump already handled by push workflow)
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||
|
||||
# Strip any existing suffix from version before applying stability
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
# RC and stable consolidate dev patches into a clean minor bump
|
||||
# e.g. 02.33.15 → 02.34.00 (not 02.33.15-rc)
|
||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||
case "$STABILITY" in
|
||||
release-candidate)
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
MINOR=$(printf "%02d" $((10#$MINOR + 1)))
|
||||
VERSION="${MAJOR}.${MINOR}.00"
|
||||
;;
|
||||
release-candidate) BUMP="minor" ;;
|
||||
*) BUMP="patch" ;;
|
||||
esac
|
||||
|
||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||
|
||||
# Set stability suffix and verify consistency
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||
|
||||
# Verify version consistency across all files
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Update VERSION variable with suffix
|
||||
# Append suffix for output
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
@@ -212,55 +203,8 @@ jobs:
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
- name: Update updates.xml
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
|
||||
if [ ! -f "updates.xml" ]; then
|
||||
echo "No updates.xml -- skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHA_FLAG=""
|
||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||
|
||||
php ${MOKO_CLI}/updates_xml_build.php \
|
||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||
${SHA_FLAG}
|
||||
|
||||
# Commit and push
|
||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git add updates.xml
|
||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||
fi
|
||||
|
||||
- name: "Sync updates.xml to all branches"
|
||||
if: steps.platform.outputs.platform == 'joomla'
|
||||
run: |
|
||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
|
||||
for BRANCH in main dev; do
|
||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||
echo "Syncing updates.xml -> ${BRANCH}"
|
||||
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
||||
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
||||
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||
git add updates.xml
|
||||
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
||||
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
||||
fi
|
||||
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
||||
done
|
||||
# updates.xml is generated dynamically by MokoGitea license server
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
continue-on-error: true
|
||||
|
||||
+2
-1
@@ -14,7 +14,7 @@
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: ./CHANGELOG.md
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
BRIEF: Version history using `Keep a Changelog`
|
||||
-->
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
|
||||
### Changed
|
||||
- Move security hardening methods (protectPlugin, ensureProtectedFlag, isOurExtension) from core plugin to firewall plugin (#155)
|
||||
- Admin menu module uses native Joomla MetisMenu CSS classes
|
||||
- Helpdesk icon changed to fa-handshake-angle, .htaccess to fa-solid fa-file-code
|
||||
- clearCache purges all cache files recursively (replaces Regular Labs Cache Cleaner behavior)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||
-->
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
|
||||
-->
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: ./LICENSE.md
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
BRIEF: Project license (GPL-3.0-or-later)
|
||||
-->
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /README.md
|
||||
BRIEF: MokoWaaS platform plugin for Joomla
|
||||
-->
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
||||
INGROUP: [PROJECT_NAME].Documentation
|
||||
REPO: [REPOSITORY_URL]
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
INGROUP: MokoWaaS.Build
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
FILE: build-guide.md
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/
|
||||
BRIEF: Build and packaging guide for the MokoWaaS system plugin
|
||||
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
|
||||
-->
|
||||
|
||||
# MokoWaaS Build Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Build Guide (VERSION: 02.34.10)
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/configuration-guide.md
|
||||
BRIEF: Configuration guide for the MokoWaaS system plugin
|
||||
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
|
||||
-->
|
||||
|
||||
# MokoWaaS Configuration Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Configuration Guide (VERSION: 02.34.10)
|
||||
|
||||
## 1. Objective
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/installation-guide.md
|
||||
BRIEF: Installation guide for the MokoWaaS system plugin
|
||||
NOTE: First document in the guide set
|
||||
-->
|
||||
|
||||
# MokoWaaS Installation Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Installation Guide (VERSION: 02.34.10)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/operations-guide.md
|
||||
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
|
||||
NOTE: Defines lifecycle, responsibilities, and operational behaviors
|
||||
-->
|
||||
|
||||
# MokoWaaS Operations Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Operations Guide (VERSION: 02.34.10)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/rollback-and-recovery-guide.md
|
||||
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
|
||||
NOTE: Completes the core guide set for WaaS plugin governance
|
||||
-->
|
||||
|
||||
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.34.10)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/testing-guide.md
|
||||
BRIEF: Testing guide for MokoWaaS v02.01.08
|
||||
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
|
||||
-->
|
||||
|
||||
# MokoWaaS Testing Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Testing Guide (VERSION: 02.34.10)
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/troubleshooting-guide.md
|
||||
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
|
||||
NOTE: Designed for administrators and WaaS operations teams
|
||||
-->
|
||||
|
||||
# MokoWaaS Troubleshooting Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Troubleshooting Guide (VERSION: 02.34.10)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/guides/upgrade-and-versioning-guide.md
|
||||
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
|
||||
NOTE: Defines release flow, version rules, and upgrade validation
|
||||
-->
|
||||
|
||||
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.33.01)
|
||||
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.34.10)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
+2
-2
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
PATH: /docs/index.md
|
||||
BRIEF: Master index of all documentation for the MokoWaaS plugin
|
||||
NOTE: Automatically maintained index for all guide canvases
|
||||
-->
|
||||
|
||||
# MokoWaaS Documentation Index (VERSION: 02.33.01)
|
||||
# MokoWaaS Documentation Index (VERSION: 02.34.10)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: /docs/plugin-basic.md
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
BRIEF: Baseline documentation for the MokoWaaS system plugin
|
||||
NOTE: Foundational reference for internal and external stakeholders
|
||||
-->
|
||||
|
||||
# MokoWaaS Plugin Overview (VERSION: 02.33.01)
|
||||
# MokoWaaS Plugin Overview (VERSION: 02.34.10)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
|
||||
PATH: /docs/update-server.md
|
||||
VERSION: 02.33.01
|
||||
VERSION: 02.34.10
|
||||
BRIEF: How this extension's Joomla update server file (update.xml) is managed
|
||||
-->
|
||||
|
||||
|
||||
@@ -580,12 +580,40 @@ class DisplayController extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
$input = Factory::getApplication()->getInput();
|
||||
$model = new \Moko\Component\MokoWaaS\Administrator\Model\PrivacyModel();
|
||||
$action = $input->getString('action', 'deny');
|
||||
|
||||
if ($action === 'create')
|
||||
{
|
||||
$result = $model->createRequest(
|
||||
$input->getInt('user_id', 0),
|
||||
$input->getString('type', 'export')
|
||||
);
|
||||
$this->jsonResponse($result);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'approve' && !$input->getInt('request_id', 0) && $input->getInt('user_id', 0))
|
||||
{
|
||||
// Auto-process: create then immediately approve
|
||||
$result = $model->createRequest(
|
||||
$input->getInt('user_id', 0),
|
||||
$input->getString('type', 'export')
|
||||
);
|
||||
|
||||
if ($result['success'] && !empty($result['id']))
|
||||
{
|
||||
$result = $model->processRequest((int) $result['id'], 'approve');
|
||||
}
|
||||
|
||||
$this->jsonResponse($result);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->jsonResponse($model->processRequest(
|
||||
$input->getInt('request_id', 0),
|
||||
$input->getString('action', 'deny')
|
||||
$action
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -267,48 +267,18 @@ class DashboardModel extends BaseDatabaseModel
|
||||
{
|
||||
try
|
||||
{
|
||||
// Purge all file-based cache directories
|
||||
$root = JPATH_ROOT;
|
||||
$dirs = [
|
||||
$root . '/cache',
|
||||
$root . '/administrator/cache',
|
||||
// Use Joomla's native cache API — same as com_cache
|
||||
$cache = Factory::getContainer()->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class);
|
||||
$cache->createCacheController('', ['defaultgroup' => ''])->cache->clean('');
|
||||
|
||||
// Also clean admin cache
|
||||
$conf = Factory::getApplication()->get('cache_handler', 'file');
|
||||
$options = [
|
||||
'defaultgroup' => '',
|
||||
'cachebase' => JPATH_ADMINISTRATOR . '/cache',
|
||||
'storage' => $conf,
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir)
|
||||
{
|
||||
if (!is_dir($dir))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$it = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($it as $file)
|
||||
{
|
||||
$name = $file->getFilename();
|
||||
|
||||
if ($name === 'index.html' || $name === '.htaccess')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($file->isDir())
|
||||
{
|
||||
@rmdir($file->getPathname());
|
||||
}
|
||||
else
|
||||
{
|
||||
@unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also run Joomla's built-in cache GC for non-file handlers
|
||||
Factory::getCache('', '')->gc();
|
||||
Factory::getCache('', '', 'administrator')->gc();
|
||||
$cache->createCacheController('', $options)->cache->clean('');
|
||||
|
||||
// Clear opcache if available
|
||||
if (\function_exists('opcache_reset'))
|
||||
|
||||
@@ -83,6 +83,26 @@ $statusBadge = [
|
||||
Install
|
||||
</button>
|
||||
<?php elseif ($pkg->status === 'installed'): ?>
|
||||
<?php
|
||||
$dashLink = '';
|
||||
if ($pkg->type === 'component')
|
||||
{
|
||||
$dashLink = 'index.php?option=' . $pkg->element;
|
||||
}
|
||||
elseif ($pkg->type === 'package' && strpos($pkg->element, 'pkg_') === 0)
|
||||
{
|
||||
$comElement = 'com_' . substr($pkg->element, 4);
|
||||
if (is_dir(JPATH_ADMINISTRATOR . '/components/' . $comElement))
|
||||
{
|
||||
$dashLink = 'index.php?option=' . $comElement;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php if ($dashLink): ?>
|
||||
<a href="<?php echo Route::_($dashLink); ?>" class="btn btn-sm btn-outline-primary" title="Open">
|
||||
<span class="icon-arrow-right" aria-hidden="true"></span> Open
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<span class="btn btn-sm btn-outline-success disabled">
|
||||
<span class="icon-check" aria-hidden="true"></span> Installed
|
||||
</span>
|
||||
|
||||
@@ -53,6 +53,63 @@ $typeBadge = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Request Form -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong><span class="icon-plus"></span> Create Data Request</strong>
|
||||
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#newRequestForm" aria-expanded="false">
|
||||
<span class="icon-plus"></span> New Request
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse" id="newRequestForm">
|
||||
<div class="card-body">
|
||||
<form id="formNewRequest" class="row g-3">
|
||||
<div class="col-12 col-md-5">
|
||||
<label for="req_user_id" class="form-label">User</label>
|
||||
<select id="req_user_id" class="form-select" required>
|
||||
<option value="">Select a user...</option>
|
||||
<?php
|
||||
$db = Factory::getDbo();
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('id'), $db->quoteName('name'), $db->quoteName('email')])
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('block') . ' = 0')
|
||||
->order($db->quoteName('name'))
|
||||
);
|
||||
foreach ($db->loadObjectList() as $u):
|
||||
?>
|
||||
<option value="<?php echo (int) $u->id; ?>"><?php echo $this->escape($u->name); ?> (<?php echo $this->escape($u->email); ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label for="req_type" class="form-label">Request Type</label>
|
||||
<select id="req_type" class="form-select" required>
|
||||
<option value="export">Export Data</option>
|
||||
<option value="delete">Delete Data</option>
|
||||
<option value="anonymize">Anonymize Data</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<label for="req_auto" class="form-label">Auto-process</label>
|
||||
<select id="req_auto" class="form-select">
|
||||
<option value="0">No (pending)</option>
|
||||
<option value="1">Yes (immediate)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100" id="btnCreateRequest"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.processDataRequest&format=json'); ?>"
|
||||
data-token="<?php echo $token; ?>">
|
||||
<span class="icon-check"></span> Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Data Requests -->
|
||||
<div class="col-12 col-xl-8">
|
||||
@@ -158,6 +215,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Create new request
|
||||
var form = document.getElementById('formNewRequest');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = document.getElementById('btnCreateRequest');
|
||||
var userId = document.getElementById('req_user_id').value;
|
||||
var type = document.getElementById('req_type').value;
|
||||
var auto = document.getElementById('req_auto').value;
|
||||
if (!userId) { Joomla.renderMessages({warning:['Please select a user.']}); return; }
|
||||
btn.disabled = true;
|
||||
var fd = new FormData();
|
||||
fd.append('user_id', userId);
|
||||
fd.append('type', type);
|
||||
fd.append('action', auto === '1' ? 'approve' : 'create');
|
||||
fd.append(btn.dataset.token, '1');
|
||||
fetch(btn.dataset.url, {method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'}})
|
||||
.then(function(r){return r.json()})
|
||||
.then(function(d){
|
||||
if (d.success) { Joomla.renderMessages({message:[d.message || 'Request created.']}); location.reload(); }
|
||||
else { Joomla.renderMessages({error:[d.message || 'Failed.']}); btn.disabled = false; }
|
||||
})
|
||||
.catch(function(){ btn.disabled = false; });
|
||||
});
|
||||
}
|
||||
|
||||
// Export download
|
||||
document.querySelectorAll('.btn-export-download').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
|
||||
@@ -29,7 +29,7 @@ class CacheController extends BaseController
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function execute(): void
|
||||
public function execute($task = 'cache'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class InstallController extends BaseController
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function execute(): void
|
||||
public function execute($task = 'install'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class PluginsController extends BaseController
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function execute(): void
|
||||
public function execute($task = 'plugins'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$user = $app->getIdentity();
|
||||
|
||||
@@ -35,7 +35,7 @@ class ResetController extends BaseController
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function execute(): void
|
||||
public function execute($task = 'reset'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class SnapshotController extends BaseController
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function execute(): void
|
||||
public function execute($task = 'snapshot'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ use Joomla\Registry\Registry;
|
||||
*/
|
||||
class SyncController extends BaseController
|
||||
{
|
||||
public function execute(): void
|
||||
public function execute($task = 'sync'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use Joomla\CMS\MVC\Controller\BaseController;
|
||||
*/
|
||||
class SyncReceiveController extends BaseController
|
||||
{
|
||||
public function execute(): void
|
||||
public function execute($task = 'syncReceive'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class UpdateController extends BaseController
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function execute(): void
|
||||
public function execute($task = 'update'): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
DEFGROUP: Joomla.Component
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
VERSION: 02.32.04
|
||||
VERSION: 02.34.10
|
||||
PATH: /mokowaas.xml
|
||||
BRIEF: Component manifest for MokoWaaS admin dashboard and REST API
|
||||
-->
|
||||
@@ -20,7 +20,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>MokoWaaS admin dashboard and REST API. Provides a control panel for managing MokoWaaS feature plugins, site health monitoring, and remote management endpoints.</description>
|
||||
|
||||
<namespace path="src">Moko\Component\MokoWaaS</namespace>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>MOD_MOKOWAAS_CACHE_DESC</description>
|
||||
<namespace path="src">Moko\Module\MokoWaaSCache</namespace>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>MOD_MOKOWAAS_CPANEL_DESC</description>
|
||||
<namespace path="src">Moko\Module\MokoWaaSCpanel</namespace>
|
||||
|
||||
|
||||
@@ -58,35 +58,47 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
|
||||
?>
|
||||
|
||||
<div class="mod-mokowaas-cpanel card p-3 mb-4">
|
||||
<!-- Header row (always visible, acts as collapse toggle) -->
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<a class="d-flex align-items-center gap-2 text-decoration-none text-reset" data-bs-toggle="collapse" href="#mokowaas-cpanel-body" role="button" aria-expanded="<?php echo $collapsed ? 'false' : 'true'; ?>" aria-controls="mokowaas-cpanel-body">
|
||||
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span>
|
||||
<strong>MokoWaaS</strong>
|
||||
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokowaas_version ?? ''); ?></span>
|
||||
<?php if (!empty($siteInfo->debug)): ?>
|
||||
<span class="badge bg-warning text-dark">Debug</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($siteInfo->offline)): ?>
|
||||
<span class="badge bg-danger">Offline</span>
|
||||
<?php endif; ?>
|
||||
<?php if (($counts->moko_updates ?? 0) > 0): ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-info text-decoration-none" title="MokoWaaS updates available">
|
||||
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->moko_updates; ?> MokoWaaS update<?php echo $counts->moko_updates > 1 ? 's' : ''; ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($counts->updates > 0 && $counts->updates !== ($counts->moko_updates ?? 0)): ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none" title="Other updates available">
|
||||
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates - ($counts->moko_updates ?? 0); ?> update<?php echo ($counts->updates - ($counts->moko_updates ?? 0)) > 1 ? 's' : ''; ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<span class="icon-chevron-down small text-muted" aria-hidden="true"></span>
|
||||
</a>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokowaas'); ?>" class="btn btn-sm btn-primary">
|
||||
<span class="icon-cogs" aria-hidden="true"></span>
|
||||
<?php echo Text::_('MOD_MOKOWAAS_CPANEL_OPEN_DASHBOARD'); ?>
|
||||
</a>
|
||||
<!-- Header row -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" class="btn btn-sm btn-link p-0 text-muted" data-bs-toggle="collapse" data-bs-target="#mokowaas-cpanel-body" aria-expanded="<?php echo $collapsed ? 'false' : 'true'; ?>" aria-controls="mokowaas-cpanel-body" id="mokowaas-cpanel-toggle" style="font-size:1rem;line-height:1;width:1.5rem;">
|
||||
<span class="fa-solid fa-caret-<?php echo $collapsed ? 'right' : 'down'; ?>" aria-hidden="true" id="mokowaas-cpanel-caret"></span>
|
||||
</button>
|
||||
<span class="icon-shield-alt" aria-hidden="true" style="font-size:1.25rem;color:#1a2744"></span>
|
||||
<strong>MokoWaaS</strong>
|
||||
<span class="badge bg-primary"><?php echo htmlspecialchars($siteInfo->mokowaas_version ?? ''); ?></span>
|
||||
<?php if (!empty($siteInfo->debug)): ?>
|
||||
<span class="badge bg-warning text-dark">Debug</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($siteInfo->offline)): ?>
|
||||
<span class="badge bg-danger">Offline</span>
|
||||
<?php endif; ?>
|
||||
<?php if (($counts->moko_updates ?? 0) > 0): ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-info text-decoration-none" title="MokoWaaS updates available">
|
||||
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->moko_updates; ?> MokoWaaS update<?php echo $counts->moko_updates > 1 ? 's' : ''; ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($counts->updates > 0 && $counts->updates !== ($counts->moko_updates ?? 0)): ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_installer&view=update'); ?>" class="badge bg-warning text-dark text-decoration-none" title="Other updates available">
|
||||
<span class="icon-upload" aria-hidden="true"></span> <?php echo $counts->updates - ($counts->moko_updates ?? 0); ?> update<?php echo ($counts->updates - ($counts->moko_updates ?? 0)) > 1 ? 's' : ''; ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<span class="ms-auto">
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokowaas'); ?>" class="btn btn-sm btn-primary">
|
||||
<span class="icon-cogs" aria-hidden="true"></span>
|
||||
<?php echo Text::_('MOD_MOKOWAAS_CPANEL_OPEN_DASHBOARD'); ?>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var target = document.getElementById('mokowaas-cpanel-body');
|
||||
var caret = document.getElementById('mokowaas-cpanel-caret');
|
||||
if (target && caret) {
|
||||
target.addEventListener('show.bs.collapse', function() { caret.className = 'fa-solid fa-caret-down'; });
|
||||
target.addEventListener('hide.bs.collapse', function() { caret.className = 'fa-solid fa-caret-right'; });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Collapsible body -->
|
||||
<div class="collapse<?php echo $collapsed ? '' : ' show'; ?> mt-3" id="mokowaas-cpanel-body">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>MokoWaaS admin sidebar menu — renders a dedicated MokoWaaS section in the admin menu before Joomla's default menu.</description>
|
||||
<namespace path="src">Moko\Module\MokoWaaSMenu</namespace>
|
||||
|
||||
|
||||
@@ -2,45 +2,130 @@
|
||||
/**
|
||||
* MokoWaaS Admin Sidebar Menu
|
||||
*
|
||||
* Uses native Joomla admin menu classes (MetisMenu) so it renders
|
||||
* identically to Joomla's own sidebar menu items.
|
||||
* Renders MokoWaaS static views first, then auto-discovers installed
|
||||
* Moko components from #__menu and renders their submenu items as
|
||||
* nested MetisMenu collapsible sections.
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
$items = [
|
||||
['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokowaas'],
|
||||
['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokowaas&view=tickets'],
|
||||
['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokowaas&view=extensions'],
|
||||
['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokowaas&view=htaccess'],
|
||||
['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokowaas&view=privacy'],
|
||||
['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokowaas&view=waflog'],
|
||||
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokowaas&view=database'],
|
||||
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokowaas&view=cleanup'],
|
||||
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokowaas'],
|
||||
];
|
||||
|
||||
$app = \Joomla\CMS\Factory::getApplication();
|
||||
$app = Factory::getApplication();
|
||||
$currentOption = $app->getInput()->get('option', '');
|
||||
$currentView = $app->getInput()->get('view', '');
|
||||
|
||||
// Determine if any child is active (auto-expand)
|
||||
$anyActive = ($currentOption === 'com_mokowaas');
|
||||
$parentClass = 'item parent item-level-1' . ($anyActive ? ' mm-active' : '');
|
||||
$collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : '');
|
||||
// ── Static MokoWaaS views ────────────────────────────────────────────
|
||||
$mokowaasItems = [
|
||||
['icon' => 'icon-cogs', 'title' => 'Dashboard', 'link' => 'index.php?option=com_mokowaas'],
|
||||
['icon' => 'fa-solid fa-handshake-angle', 'title' => 'Helpdesk', 'link' => 'index.php?option=com_mokowaas&view=tickets'],
|
||||
['icon' => 'icon-puzzle-piece', 'title' => 'Extensions', 'link' => 'index.php?option=com_mokowaas&view=extensions'],
|
||||
['icon' => 'fa-solid fa-file-code', 'title' => '.htaccess Maker', 'link' => 'index.php?option=com_mokowaas&view=htaccess'],
|
||||
['icon' => 'icon-lock', 'title' => 'Privacy Guard', 'link' => 'index.php?option=com_mokowaas&view=privacy'],
|
||||
['icon' => 'icon-shield-alt', 'title' => 'WAF Log', 'link' => 'index.php?option=com_mokowaas&view=waflog'],
|
||||
['icon' => 'icon-database', 'title' => 'Database Tools', 'link' => 'index.php?option=com_mokowaas&view=database'],
|
||||
['icon' => 'icon-trash', 'title' => 'Cache Cleanup', 'link' => 'index.php?option=com_mokowaas&view=cleanup'],
|
||||
['icon' => 'icon-power-off', 'title' => 'Feature Plugins', 'link' => 'index.php?option=com_plugins&filter[folder]=system&filter[search]=mokowaas'],
|
||||
];
|
||||
|
||||
// ── Auto-discover Moko component menus from #__menu ──────────────────
|
||||
$mokoComponents = [];
|
||||
|
||||
try
|
||||
{
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
|
||||
// Find all Moko component menu items (exclude com_mokowaas — handled above)
|
||||
$db->setQuery(
|
||||
"SELECT m.id, m.title, m.link, m.level, m.parent_id, m.img, e.element"
|
||||
. " FROM " . $db->quoteName('#__menu') . " m"
|
||||
. " LEFT JOIN " . $db->quoteName('#__extensions') . " e ON m.component_id = e.extension_id"
|
||||
. " WHERE m.client_id = 1 AND m.level >= 1 AND m.published = 1"
|
||||
. " AND e.element LIKE 'com_moko%'"
|
||||
. " AND e.element != 'com_mokowaas'"
|
||||
. " AND e.enabled = 1"
|
||||
. " ORDER BY e.element, m.level, m.lft"
|
||||
);
|
||||
$menuItems = $db->loadObjectList() ?: [];
|
||||
|
||||
// Load sys.ini language files for discovered components
|
||||
$lang = Factory::getLanguage();
|
||||
$loadedLangs = [];
|
||||
foreach ($menuItems as $m)
|
||||
{
|
||||
if (!isset($loadedLangs[$m->element]))
|
||||
{
|
||||
$lang->load($m->element . '.sys', JPATH_ADMINISTRATOR);
|
||||
$lang->load($m->element, JPATH_ADMINISTRATOR);
|
||||
$loadedLangs[$m->element] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Group: level 1 = component parent, level 2 = children
|
||||
foreach ($menuItems as $m)
|
||||
{
|
||||
if ((int) $m->level === 1)
|
||||
{
|
||||
$mokoComponents[$m->element] = [
|
||||
'id' => $m->id,
|
||||
'title' => Text::_($m->title),
|
||||
'link' => $m->link,
|
||||
'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:puzzle-piece'),
|
||||
'element' => $m->element,
|
||||
'children' => [],
|
||||
];
|
||||
}
|
||||
elseif ((int) $m->level === 2 && isset($mokoComponents[$m->element]))
|
||||
{
|
||||
$mokoComponents[$m->element]['children'][] = [
|
||||
'title' => Text::_($m->title),
|
||||
'link' => $m->link,
|
||||
'icon' => str_replace('class:', 'icon-', $m->img ?: 'class:cog'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Silent — menu works without auto-discovered components
|
||||
}
|
||||
|
||||
// ── Determine active state ───────────────────────────────────────────
|
||||
$mokowaasActive = ($currentOption === 'com_mokowaas');
|
||||
$anyMokoActive = $mokowaasActive;
|
||||
|
||||
foreach ($mokoComponents as $comp)
|
||||
{
|
||||
$parsed = [];
|
||||
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $parsed);
|
||||
if (($parsed['option'] ?? '') === $currentOption)
|
||||
{
|
||||
$anyMokoActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
$topClass = 'item parent item-level-1' . ($anyMokoActive ? ' mm-active' : '');
|
||||
$topCollapse = 'collapse-level-1 mm-collapse' . ($anyMokoActive ? ' mm-show' : '');
|
||||
?>
|
||||
|
||||
<style>
|
||||
.sidebar-wrapper .item-level-1 > a { padding-inline-start: 1.5rem; }
|
||||
.sidebar-wrapper .mokowaas-menu-item > a { padding-inline-start: 2rem; }
|
||||
.sidebar-wrapper .mokowaas-menu-child > a { padding-inline-start: 2.5rem; }
|
||||
</style>
|
||||
|
||||
<ul class="nav flex-column main-nav">
|
||||
<li class="<?php echo $parentClass; ?>">
|
||||
<li class="<?php echo $topClass; ?>">
|
||||
<a class="has-arrow" href="#" aria-label="MokoWaaS">
|
||||
<span class="icon-shield-alt" aria-hidden="true"></span>
|
||||
<span class="sidebar-item-title">MokoWaaS</span>
|
||||
</a>
|
||||
<ul class="<?php echo $collapseClass; ?>">
|
||||
<?php foreach ($items as $item): ?>
|
||||
<ul class="<?php echo $topCollapse; ?>" style="padding-inline-start:0.5rem;">
|
||||
|
||||
<?php // ── MokoWaaS static items ── ?>
|
||||
<?php foreach ($mokowaasItems as $item): ?>
|
||||
<?php
|
||||
$active = false;
|
||||
$parsed = [];
|
||||
@@ -51,7 +136,7 @@ $collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : '')
|
||||
? ($currentView === '' || $currentView === 'dashboard')
|
||||
: ($currentView === ($parsed['view'] ?? ''));
|
||||
}
|
||||
$liClass = 'item item-level-2' . ($active ? ' mm-active' : '');
|
||||
$liClass = 'item mokowaas-menu-item' . ($active ? ' mm-active' : '');
|
||||
$aClass = 'no-dropdown' . ($active ? ' mm-active' : '');
|
||||
?>
|
||||
<li class="<?php echo $liClass; ?>">
|
||||
@@ -61,6 +146,46 @@ $collapseClass = 'collapse-level-1 mm-collapse' . ($anyActive ? ' mm-show' : '')
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php // ── Auto-discovered Moko components with submenus ── ?>
|
||||
<?php foreach ($mokoComponents as $comp): ?>
|
||||
<?php
|
||||
$compParsed = [];
|
||||
parse_str(parse_url($comp['link'], PHP_URL_QUERY) ?? '', $compParsed);
|
||||
$compActive = ($compParsed['option'] ?? '') === $currentOption;
|
||||
$hasChildren = !empty($comp['children']);
|
||||
$compLiClass = 'item mokowaas-menu-item' . ($hasChildren ? ' parent' : '') . ($compActive ? ' mm-active' : '');
|
||||
$compAClass = ($hasChildren ? 'has-arrow' : 'no-dropdown') . ($compActive ? ' mm-active' : '');
|
||||
$childCollapse = 'collapse-level-2 mm-collapse' . ($compActive ? ' mm-show' : '');
|
||||
?>
|
||||
<li class="<?php echo $compLiClass; ?>">
|
||||
<a class="<?php echo $compAClass; ?>" href="<?php echo $hasChildren ? '#' : Route::_($comp['link']); ?>"<?php echo ($compActive && !$hasChildren) ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $comp['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $comp['title']; ?></span>
|
||||
</a>
|
||||
<?php if ($hasChildren): ?>
|
||||
<ul class="<?php echo $childCollapse; ?>" style="padding-inline-start:0.75rem;">
|
||||
<?php foreach ($comp['children'] as $child): ?>
|
||||
<?php
|
||||
$childParsed = [];
|
||||
parse_str(parse_url($child['link'], PHP_URL_QUERY) ?? '', $childParsed);
|
||||
$childActive = ($childParsed['option'] ?? '') === $currentOption
|
||||
&& ($childParsed['view'] ?? '') === $currentView;
|
||||
$childLiClass = 'item mokowaas-menu-child' . ($childActive ? ' mm-active' : '');
|
||||
$childAClass = 'no-dropdown' . ($childActive ? ' mm-active' : '');
|
||||
?>
|
||||
<li class="<?php echo $childLiClass; ?>">
|
||||
<a class="<?php echo $childAClass; ?>" href="<?php echo Route::_($child['link']); ?>"<?php echo $childActive ? ' aria-current="page"' : ''; ?>>
|
||||
<span class="<?php echo $child['icon']; ?>" aria-hidden="true" style="display:inline-block!important;width:1.25em;text-align:center;margin-inline-end:0.4em;"></span>
|
||||
<span class="sidebar-item-title"><?php echo $child['title']; ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.33.01
|
||||
* PATH: /src/Field/AllowedIpsField.php
|
||||
* BRIEF: Custom form field that displays the current IP whitelist
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\FormField;
|
||||
|
||||
class AllowedIpsField extends FormField
|
||||
{
|
||||
protected $type = 'AllowedIps';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
$config = Factory::getApplication()->getConfig();
|
||||
$allowedRaw = $config->get('mokowaas_allowed_ips', '');
|
||||
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
|
||||
if (empty($allowedRaw))
|
||||
{
|
||||
$status = '<span class="badge bg-danger">Not configured</span>';
|
||||
$ipList = '<em>No IPs set — emergency access is blocked.</em>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$ips = array_map('trim', explode(',', $allowedRaw));
|
||||
$status = '<span class="badge bg-success">'
|
||||
. count($ips) . ' IP(s) configured</span>';
|
||||
$ipItems = [];
|
||||
|
||||
foreach ($ips as $ip)
|
||||
{
|
||||
$match = ($ip === $currentIp)
|
||||
? ' <span class="badge bg-info">your IP</span>'
|
||||
: '';
|
||||
$ipItems[] = '<code>' . htmlspecialchars($ip)
|
||||
. '</code>' . $match;
|
||||
}
|
||||
|
||||
$ipList = implode(', ', $ipItems);
|
||||
}
|
||||
|
||||
$yourIp = '<code>' . htmlspecialchars($currentIp) . '</code>';
|
||||
|
||||
return '<div class="alert alert-info mb-0">'
|
||||
. '<strong>IP Whitelist:</strong> ' . $status . '<br>'
|
||||
. '<strong>Allowed IPs:</strong> ' . $ipList . '<br>'
|
||||
. '<strong>Your current IP:</strong> ' . $yourIp . '<br>'
|
||||
. '<small class="text-muted">Set <code>public '
|
||||
. '$mokowaas_allowed_ips = \'1.2.3.4,5.6.7.8\';</code>'
|
||||
. ' in configuration.php to change.</small>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
protected function getLabel()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.33.01
|
||||
* VERSION: 02.34.10
|
||||
* PATH: /src/Field/CopyableTokenField.php
|
||||
* BRIEF: Read-only token field with a copy-to-clipboard button
|
||||
*/
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
*
|
||||
* SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.33.01
|
||||
* PATH: /src/Field/CurrentIpField.php
|
||||
* BRIEF: Read-only field that displays the current user's IP address
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Form\FormField;
|
||||
|
||||
class CurrentIpField extends FormField
|
||||
{
|
||||
protected $type = 'CurrentIp';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
$currentIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
|
||||
return '<div class="alert alert-info mb-0 py-2">'
|
||||
. '<strong>Your current IP:</strong> '
|
||||
. '<code>' . htmlspecialchars($currentIp) . '</code> '
|
||||
. '<small class="text-muted">— add this to the table below to keep your session alive.</small>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
protected function getLabel()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_system_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.33.01
|
||||
* PATH: /src/Field/DemoTaskInfoField.php
|
||||
* BRIEF: Read-only field showing scheduled task info with link to manage it
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\FormField;
|
||||
use Joomla\CMS\Router\Route;
|
||||
|
||||
/**
|
||||
* Displays the demo reset scheduled task status: schedule, next run,
|
||||
* last run, and a direct link to edit the task in Joomla's Scheduler.
|
||||
*
|
||||
* @since 02.29.00
|
||||
*/
|
||||
class DemoTaskInfoField extends FormField
|
||||
{
|
||||
protected $type = 'DemoTaskInfo';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
// Query the scheduled task — if it exists and is enabled, demo mode is on
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__scheduler_tasks'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$task = $db->loadAssoc();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$task = null;
|
||||
}
|
||||
|
||||
$newTaskLink = Route::_('index.php?option=com_scheduler&task=task.add');
|
||||
|
||||
if (!$task)
|
||||
{
|
||||
return '<div class="alert alert-info mb-0 py-2">'
|
||||
. 'No demo reset task configured. '
|
||||
. '<a href="' . $newTaskLink . '" class="alert-link">Create a Scheduled Task</a> '
|
||||
. 'and select <strong>MokoWaaS Demo Reset</strong> to enable demo mode.</div>';
|
||||
}
|
||||
|
||||
$taskId = (int) $task['id'];
|
||||
$state = (int) $task['state'];
|
||||
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
|
||||
|
||||
// Parse schedule from execution_rules
|
||||
$rules = json_decode($task['execution_rules'] ?? '{}', true);
|
||||
$ruleType = $rules['rule-type'] ?? '';
|
||||
|
||||
switch ($ruleType)
|
||||
{
|
||||
case 'cron-expression':
|
||||
$schedule = $rules['cron-expression'] ?? '';
|
||||
$friendlySchedule = $this->friendlySchedule($schedule);
|
||||
break;
|
||||
|
||||
case 'interval-minutes':
|
||||
$mins = (int) ($rules['interval-minutes'] ?? 0);
|
||||
|
||||
if ($mins >= 1440 && $mins % 1440 === 0)
|
||||
{
|
||||
$days = $mins / 1440;
|
||||
$schedule = 'Every ' . $days . ' day' . ($days > 1 ? 's' : '');
|
||||
}
|
||||
elseif ($mins >= 60 && $mins % 60 === 0)
|
||||
{
|
||||
$hours = $mins / 60;
|
||||
$schedule = 'Every ' . $hours . ' hour' . ($hours > 1 ? 's' : '');
|
||||
}
|
||||
else
|
||||
{
|
||||
$schedule = 'Every ' . $mins . ' minute' . ($mins !== 1 ? 's' : '');
|
||||
}
|
||||
|
||||
$friendlySchedule = $schedule;
|
||||
break;
|
||||
|
||||
case 'interval-hours':
|
||||
$hours = (int) ($rules['interval-hours'] ?? 0);
|
||||
$schedule = 'Every ' . $hours . ' hour' . ($hours !== 1 ? 's' : '');
|
||||
$friendlySchedule = $schedule;
|
||||
break;
|
||||
|
||||
case 'interval-days':
|
||||
$days = (int) ($rules['interval-days'] ?? 0);
|
||||
$schedule = 'Every ' . $days . ' day' . ($days !== 1 ? 's' : '');
|
||||
$friendlySchedule = $schedule;
|
||||
break;
|
||||
|
||||
default:
|
||||
$schedule = $ruleType ?: 'Not set';
|
||||
$friendlySchedule = 'Custom';
|
||||
}
|
||||
|
||||
// Next execution
|
||||
$nextExec = $task['next_execution'] ?? '';
|
||||
$nextFormatted = 'Not scheduled';
|
||||
$nextBadge = '';
|
||||
|
||||
if (!empty($nextExec) && $nextExec !== '0000-00-00 00:00:00')
|
||||
{
|
||||
try
|
||||
{
|
||||
$dt = new \DateTime($nextExec, new \DateTimeZone('UTC'));
|
||||
$dt->setTimezone(new \DateTimeZone($siteTimezone));
|
||||
$nextFormatted = $dt->format('M j, Y g:i A T');
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$nextFormatted = $nextExec;
|
||||
}
|
||||
|
||||
$diff = strtotime($nextExec . ' UTC') - time();
|
||||
|
||||
if ($diff <= 0)
|
||||
{
|
||||
$nextBadge = '<span class="badge bg-warning text-dark">DUE</span>';
|
||||
}
|
||||
elseif ($diff < 3600)
|
||||
{
|
||||
$nextBadge = '<span class="badge bg-info">in ' . (int) ceil($diff / 60) . ' min</span>';
|
||||
}
|
||||
elseif ($diff < 86400)
|
||||
{
|
||||
$nextBadge = '<span class="badge bg-info">in ' . round($diff / 3600, 1) . 'h</span>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$nextBadge = '<span class="badge bg-secondary">in ' . round($diff / 86400, 1) . 'd</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Last execution
|
||||
$lastExec = $task['last_execution'] ?? '';
|
||||
$lastFormatted = 'Never';
|
||||
|
||||
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
|
||||
{
|
||||
try
|
||||
{
|
||||
$dt = new \DateTime($lastExec, new \DateTimeZone('UTC'));
|
||||
$dt->setTimezone(new \DateTimeZone($siteTimezone));
|
||||
$lastFormatted = $dt->format('M j, Y g:i A T');
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$lastFormatted = $lastExec;
|
||||
}
|
||||
}
|
||||
|
||||
// State badge
|
||||
$stateBadge = $state === 1
|
||||
? '<span class="badge bg-success">Enabled</span>'
|
||||
: '<span class="badge bg-danger">Disabled</span>';
|
||||
|
||||
// Link to edit the task
|
||||
$editLink = Route::_('index.php?option=com_scheduler&task=task.edit&id=' . $taskId);
|
||||
|
||||
// Task params — default to On when keys are missing (matches form defaults)
|
||||
$taskParams = json_decode($task['params'] ?? '{}', true) ?: [];
|
||||
$bannerOn = !isset($taskParams['banner_enabled']) || (int) $taskParams['banner_enabled'] === 1;
|
||||
$mediaOn = !isset($taskParams['include_media']) || (int) $taskParams['include_media'] === 1;
|
||||
$countdownOn = !isset($taskParams['show_countdown']) || (int) $taskParams['show_countdown'] === 1;
|
||||
|
||||
// Check if snapshot exists
|
||||
$snapshotExists = is_dir(JPATH_ROOT . '/mokowaas-snapshots/default');
|
||||
|
||||
// Build info card
|
||||
return '<div class="card card-body bg-light py-2 px-3 mb-0">'
|
||||
. '<table class="table table-sm table-borderless mb-1" style="max-width:550px">'
|
||||
. '<tr><td class="text-muted" style="width:130px">Status</td><td>' . $stateBadge . '</td></tr>'
|
||||
. '<tr><td class="text-muted">Schedule</td><td>' . htmlspecialchars($friendlySchedule) . '</td></tr>'
|
||||
. '<tr><td class="text-muted">Next Reset</td><td>' . htmlspecialchars($nextFormatted) . ' ' . $nextBadge . '</td></tr>'
|
||||
. '<tr><td class="text-muted">Last Reset</td><td>' . htmlspecialchars($lastFormatted) . '</td></tr>'
|
||||
. '<tr><td class="text-muted">Runs</td><td>' . (int) ($task['times_executed'] ?? 0) . ' executed, ' . (int) ($task['times_failed'] ?? 0) . ' failed</td></tr>'
|
||||
. '<tr><td class="text-muted">Baseline</td><td>' . ($snapshotExists ? '<span class="badge bg-success">Saved</span>' : '<span class="badge bg-warning text-dark">Not taken yet</span>') . '</td></tr>'
|
||||
. '<tr><td class="text-muted">Banner</td><td>' . ($bannerOn ? 'On' : 'Off') . ($countdownOn ? ' + countdown' : '') . '</td></tr>'
|
||||
. '<tr><td class="text-muted">Images</td><td>' . ($mediaOn ? 'Included' : 'Excluded') . '</td></tr>'
|
||||
. '</table>'
|
||||
. '<a href="' . $editLink . '" class="btn btn-sm btn-outline-primary">'
|
||||
. '<span class="icon-cog" aria-hidden="true"></span> Manage Scheduled Task</a>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
protected function getLabel()
|
||||
{
|
||||
return '<label class="form-label"><strong>Scheduled Reset</strong></label>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a cron expression to a human-readable string.
|
||||
*
|
||||
* @param string $cron Cron expression
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function friendlySchedule(string $cron): string
|
||||
{
|
||||
$map = [
|
||||
'* * * * *' => 'Every minute',
|
||||
'*/5 * * * *' => 'Every 5 minutes',
|
||||
'*/15 * * * *' => 'Every 15 minutes',
|
||||
'*/30 * * * *' => 'Every 30 minutes',
|
||||
'0 */1 * * *' => 'Every hour',
|
||||
'0 */4 * * *' => 'Every 4 hours',
|
||||
'0 */6 * * *' => 'Every 6 hours',
|
||||
'0 */12 * * *' => 'Every 12 hours',
|
||||
'0 0 * * *' => 'Daily at midnight',
|
||||
'0 6 * * *' => 'Daily at 6:00 AM',
|
||||
'0 0 * * 0' => 'Weekly (Sunday)',
|
||||
'0 0 1 * *' => 'Monthly (1st)',
|
||||
];
|
||||
|
||||
return $map[$cron] ?? 'Custom';
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_system_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.33.01
|
||||
* PATH: /src/Field/NextResetField.php
|
||||
* BRIEF: Read-only field showing next reset time from Joomla scheduled task
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\FormField;
|
||||
|
||||
/**
|
||||
* Pulls the next execution time directly from the Joomla scheduled task
|
||||
* (#__scheduler_tasks) and displays it formatted in the site timezone.
|
||||
*
|
||||
* @since 02.29.00
|
||||
*/
|
||||
class NextResetField extends FormField
|
||||
{
|
||||
protected $type = 'NextReset';
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
// Check if demo mode is enabled
|
||||
$demoEnabled = false;
|
||||
|
||||
if ($this->form)
|
||||
{
|
||||
$demoEnabled = (int) $this->form->getValue('demo_mode_enabled', 'params', 0) === 1;
|
||||
}
|
||||
|
||||
if (!$demoEnabled)
|
||||
{
|
||||
return '<span class="form-control-plaintext text-muted">Demo mode is off</span>'
|
||||
. '<input type="hidden" name="' . $this->name . '" value="" />';
|
||||
}
|
||||
|
||||
// Query the actual next_execution from the scheduled task
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('next_execution'),
|
||||
$db->quoteName('last_execution'),
|
||||
$db->quoteName('state'),
|
||||
])
|
||||
->from($db->quoteName('#__scheduler_tasks'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('mokowaas.demo.reset'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$task = $db->loadAssoc();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$task = null;
|
||||
}
|
||||
|
||||
if (!$task)
|
||||
{
|
||||
return '<div class="alert alert-secondary mb-0 py-2">No scheduled task found — save to create one automatically.</div>'
|
||||
. '<input type="hidden" name="' . $this->name . '" value="" />';
|
||||
}
|
||||
|
||||
if ((int) $task['state'] !== 1)
|
||||
{
|
||||
return '<div class="alert alert-warning mb-0 py-2">Scheduled task is disabled.</div>'
|
||||
. '<input type="hidden" name="' . $this->name . '" value="" />';
|
||||
}
|
||||
|
||||
$nextExec = $task['next_execution'];
|
||||
$lastExec = $task['last_execution'];
|
||||
|
||||
if (empty($nextExec) || $nextExec === '0000-00-00 00:00:00')
|
||||
{
|
||||
return '<div class="alert alert-secondary mb-0 py-2">Waiting for first run...</div>'
|
||||
. '<input type="hidden" name="' . $this->name . '" value="" />';
|
||||
}
|
||||
|
||||
// Convert to site timezone
|
||||
$utcTimestamp = strtotime($nextExec);
|
||||
$siteTimezone = Factory::getApplication()->get('offset', 'UTC');
|
||||
|
||||
try
|
||||
{
|
||||
$dt = new \DateTime('@' . $utcTimestamp);
|
||||
$dt->setTimezone(new \DateTimeZone($siteTimezone));
|
||||
$formatted = $dt->format('l, F j, Y \a\t g:i A T');
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$formatted = $nextExec . ' UTC';
|
||||
}
|
||||
|
||||
// Relative time
|
||||
$diff = $utcTimestamp - time();
|
||||
$relative = '';
|
||||
|
||||
if ($diff <= 0)
|
||||
{
|
||||
$relative = '<span class="badge bg-warning text-dark">overdue</span>';
|
||||
}
|
||||
elseif ($diff < 3600)
|
||||
{
|
||||
$mins = (int) ceil($diff / 60);
|
||||
$relative = '<span class="badge bg-info">in ' . $mins . ' min</span>';
|
||||
}
|
||||
elseif ($diff < 86400)
|
||||
{
|
||||
$hours = round($diff / 3600, 1);
|
||||
$relative = '<span class="badge bg-info">in ' . $hours . 'h</span>';
|
||||
}
|
||||
else
|
||||
{
|
||||
$days = round($diff / 86400, 1);
|
||||
$relative = '<span class="badge bg-secondary">in ' . $days . 'd</span>';
|
||||
}
|
||||
|
||||
// Last run info
|
||||
$lastInfo = '';
|
||||
|
||||
if (!empty($lastExec) && $lastExec !== '0000-00-00 00:00:00')
|
||||
{
|
||||
try
|
||||
{
|
||||
$lastDt = new \DateTime($lastExec);
|
||||
$lastDt->setTimezone(new \DateTimeZone($siteTimezone));
|
||||
$lastInfo = '<small class="text-muted ms-2">Last run: ' . $lastDt->format('M j, g:i A') . '</small>';
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
return '<div class="d-flex align-items-center gap-2 flex-wrap">'
|
||||
. '<span class="form-control-plaintext" style="font-weight:500">'
|
||||
. '<span class="icon-calendar" aria-hidden="true"></span> '
|
||||
. htmlspecialchars($formatted) . '</span> '
|
||||
. $relative
|
||||
. $lastInfo
|
||||
. '<input type="hidden" name="' . $this->name . '" value="' . htmlspecialchars($nextExec) . '" />'
|
||||
. '</div>';
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_system_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.33.01
|
||||
* PATH: /src/Field/SnapshotTablesField.php
|
||||
* BRIEF: Multi-select list field that loads DB tables with sensible defaults
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\FormField;
|
||||
|
||||
/**
|
||||
* Renders a multi-select list box of all Joomla database tables, with
|
||||
* content-related tables pre-selected by default.
|
||||
*
|
||||
* @since 02.26.00
|
||||
*/
|
||||
class SnapshotTablesField extends FormField
|
||||
{
|
||||
protected $type = 'SnapshotTables';
|
||||
|
||||
/**
|
||||
* Tables selected by default when no value is stored yet.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.25.00
|
||||
*/
|
||||
private const DEFAULT_TABLES = [
|
||||
'#__content',
|
||||
'#__categories',
|
||||
'#__fields',
|
||||
'#__fields_values',
|
||||
'#__fields_groups',
|
||||
'#__menu',
|
||||
'#__menu_types',
|
||||
'#__modules',
|
||||
'#__modules_menu',
|
||||
'#__users',
|
||||
'#__user_usergroup_map',
|
||||
'#__user_profiles',
|
||||
'#__tags',
|
||||
'#__contentitem_tag_map',
|
||||
'#__assets',
|
||||
];
|
||||
|
||||
/**
|
||||
* Table suffixes grouped by category.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.25.00
|
||||
*/
|
||||
private const TABLE_GROUPS = [
|
||||
'Content' => ['content', 'categories', 'fields', 'fields_values', 'fields_groups', 'tags', 'contentitem_tag_map', 'ucm_content', 'ucm_history'],
|
||||
'Users' => ['users', 'user_usergroup_map', 'user_profiles', 'usergroups', 'user_keys', 'user_mfa'],
|
||||
'Menus' => ['menu', 'menu_types'],
|
||||
'Modules' => ['modules', 'modules_menu'],
|
||||
'Assets' => ['assets'],
|
||||
];
|
||||
|
||||
protected function getInput()
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$prefix = $db->getPrefix();
|
||||
$tables = $db->getTableList();
|
||||
|
||||
// Resolve selected values
|
||||
$selected = $this->value;
|
||||
|
||||
if ($selected === null || $selected === '')
|
||||
{
|
||||
$selected = self::DEFAULT_TABLES;
|
||||
}
|
||||
elseif (is_string($selected))
|
||||
{
|
||||
$selected = array_filter(array_map('trim', explode("\n", $selected)));
|
||||
}
|
||||
|
||||
$selected = (array) $selected;
|
||||
|
||||
// Flatten nested arrays from broken save format [["#__content"],["#__categories"]]
|
||||
$selected = array_map(function ($v) {
|
||||
return is_array($v) ? reset($v) : $v;
|
||||
}, $selected);
|
||||
|
||||
// Group tables
|
||||
$grouped = [];
|
||||
|
||||
foreach ($tables as $table)
|
||||
{
|
||||
if (strpos($table, $prefix) !== 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$suffix = substr($table, strlen($prefix));
|
||||
$logical = '#__' . $suffix;
|
||||
$group = 'Other';
|
||||
|
||||
foreach (self::TABLE_GROUPS as $groupName => $patterns)
|
||||
{
|
||||
if (in_array($suffix, $patterns, true))
|
||||
{
|
||||
$group = $groupName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$grouped[$group][] = $logical;
|
||||
}
|
||||
|
||||
// Build HTML select with optgroups
|
||||
$size = (int) ($this->element['size'] ?? 15);
|
||||
$html = '<select name="' . $this->name . '" id="' . $this->id . '"'
|
||||
. ' multiple="multiple" size="' . $size . '"'
|
||||
. ' class="form-select">';
|
||||
|
||||
$priority = ['Content', 'Users', 'Menus', 'Modules', 'Assets'];
|
||||
|
||||
foreach ($priority as $g)
|
||||
{
|
||||
if (!empty($grouped[$g]))
|
||||
{
|
||||
$html .= '<optgroup label="' . $g . '">';
|
||||
|
||||
foreach ($grouped[$g] as $t)
|
||||
{
|
||||
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
|
||||
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
|
||||
}
|
||||
|
||||
$html .= '</optgroup>';
|
||||
unset($grouped[$g]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($grouped['Other']))
|
||||
{
|
||||
$html .= '<optgroup label="Other">';
|
||||
|
||||
foreach ($grouped['Other'] as $t)
|
||||
{
|
||||
$sel = in_array($t, $selected, true) ? ' selected="selected"' : '';
|
||||
$html .= '<option value="' . htmlspecialchars($t) . '"' . $sel . '>' . htmlspecialchars($t) . '</option>';
|
||||
}
|
||||
|
||||
$html .= '</optgroup>';
|
||||
}
|
||||
|
||||
$html .= '</select>';
|
||||
|
||||
// "Reset to defaults" link
|
||||
$defaultsJson = htmlspecialchars(json_encode(self::DEFAULT_TABLES), ENT_QUOTES, 'UTF-8');
|
||||
$html .= '<div class="mt-1">'
|
||||
. '<a href="#" class="small" onclick="'
|
||||
. 'var sel=document.getElementById(\'' . $this->id . '\');'
|
||||
. 'var defs=' . $defaultsJson . ';'
|
||||
. 'Array.from(sel.options).forEach(function(o){o.selected=defs.indexOf(o.value)!==-1;});'
|
||||
. 'return false;'
|
||||
. '"><span class="icon-refresh" aria-hidden="true"></span> Reset to defaults</a>'
|
||||
. '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -1,819 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_system_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncReceiver.php
|
||||
* VERSION: 02.33.01
|
||||
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Service;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
/**
|
||||
* Content Sync Receiver — applies incoming sync payload to the local site.
|
||||
*
|
||||
* Processes categories, articles, menu types, menu items, and modules
|
||||
* from a JSON payload sent by a source MokoWaaS site. Content is matched
|
||||
* by alias (upsert pattern): existing content is updated, new content
|
||||
* is inserted.
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
class ContentSyncReceiver
|
||||
{
|
||||
/**
|
||||
* @var \Joomla\Database\DatabaseInterface
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* Warnings collected during sync.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private array $warnings = [];
|
||||
|
||||
/**
|
||||
* Cache of resolved category paths → local IDs.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private array $catPathCache = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function __construct($db = null)
|
||||
{
|
||||
$this->db = $db ?: Factory::getDbo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming sync payload.
|
||||
*
|
||||
* @param array $payload Decoded JSON payload from the source site
|
||||
*
|
||||
* @return array Result summary with per-type counts and warnings
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function receive(array $payload): array
|
||||
{
|
||||
if (empty($payload['mokowaas_sync']))
|
||||
{
|
||||
return ['status' => 'error', 'message' => 'Invalid payload — missing mokowaas_sync version'];
|
||||
}
|
||||
|
||||
$this->warnings = [];
|
||||
$results = [];
|
||||
|
||||
// Apply in dependency order
|
||||
try
|
||||
{
|
||||
$results['categories'] = $this->applyCategories($payload['categories'] ?? []);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$results['categories'] = ['error' => $e->getMessage()];
|
||||
$this->warnings[] = 'Categories failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$results['articles'] = $this->applyArticles($payload['articles'] ?? []);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$results['articles'] = ['error' => $e->getMessage()];
|
||||
$this->warnings[] = 'Articles failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$results['menu_types'] = $this->applyMenuTypes($payload['menu_types'] ?? []);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$results['menu_types'] = ['error' => $e->getMessage()];
|
||||
$this->warnings[] = 'Menu types failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$results['menu_items'] = $this->applyMenuItems($payload['menu_items'] ?? []);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$results['menu_items'] = ['error' => $e->getMessage()];
|
||||
$this->warnings[] = 'Menu items failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$results['modules'] = $this->applyModules($payload['modules'] ?? []);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$results['modules'] = ['error' => $e->getMessage()];
|
||||
$this->warnings[] = 'Modules failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
Log::add(
|
||||
sprintf('Content sync received from %s', $payload['source_site'] ?? 'unknown'),
|
||||
Log::INFO,
|
||||
'mokowaas'
|
||||
);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'Sync applied',
|
||||
'source_site' => $payload['source_site'] ?? '',
|
||||
'results' => $results,
|
||||
'warnings' => $this->warnings,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply categories — sorted by path depth (shallow first).
|
||||
*
|
||||
* @param array $categories Category data from payload
|
||||
*
|
||||
* @return array ['inserted' => N, 'updated' => N]
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function applyCategories(array $categories): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
|
||||
// Sort by path depth — parents before children
|
||||
usort($categories, function ($a, $b) {
|
||||
return substr_count($a['path'], '/') - substr_count($b['path'], '/');
|
||||
});
|
||||
|
||||
foreach ($categories as $cat)
|
||||
{
|
||||
$alias = $cat['alias'] ?? '';
|
||||
$path = $cat['path'] ?? $alias;
|
||||
|
||||
if (empty($alias) || !preg_match('/^[a-z0-9\-\/]+$/i', $path))
|
||||
{
|
||||
$this->warnings[] = 'Skipped category with invalid alias/path: ' . $alias;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve parent ID from path
|
||||
$parentId = 1; // Root
|
||||
$pathParts = explode('/', $path);
|
||||
|
||||
if (count($pathParts) > 1)
|
||||
{
|
||||
$parentPath = implode('/', array_slice($pathParts, 0, -1));
|
||||
$parentId = $this->resolveCategoryPath($parentPath);
|
||||
|
||||
if ($parentId === 0)
|
||||
{
|
||||
$this->warnings[] = 'Parent category not found for: ' . $path;
|
||||
$parentId = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if category exists
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__categories'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
|
||||
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
|
||||
->where($db->quoteName('parent_id') . ' = ' . (int) $parentId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$existingId = (int) $db->loadResult();
|
||||
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
if ($existingId)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__categories'))
|
||||
->set($db->quoteName('title') . ' = ' . $db->quote($cat['title']))
|
||||
->set($db->quoteName('description') . ' = ' . $db->quote($cat['description'] ?? ''))
|
||||
->set($db->quoteName('published') . ' = ' . (int) ($cat['published'] ?? 1))
|
||||
->set($db->quoteName('access') . ' = ' . (int) ($cat['access'] ?? 1))
|
||||
->set($db->quoteName('language') . ' = ' . $db->quote($cat['language'] ?? '*'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($cat['params'] ?? new \stdClass)))
|
||||
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($cat['metadata'] ?? new \stdClass)))
|
||||
->set($db->quoteName('modified_time') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $existingId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
$updated++;
|
||||
|
||||
$this->catPathCache[$path] = $existingId;
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj = (object) [
|
||||
'title' => $cat['title'],
|
||||
'alias' => $alias,
|
||||
'path' => $path,
|
||||
'extension' => 'com_content',
|
||||
'description' => $cat['description'] ?? '',
|
||||
'published' => (int) ($cat['published'] ?? 1),
|
||||
'access' => (int) ($cat['access'] ?? 1),
|
||||
'language' => $cat['language'] ?? '*',
|
||||
'params' => json_encode($cat['params'] ?? new \stdClass),
|
||||
'metadata' => json_encode($cat['metadata'] ?? new \stdClass),
|
||||
'parent_id' => $parentId,
|
||||
'level' => count($pathParts),
|
||||
'lft' => 0,
|
||||
'rgt' => 0,
|
||||
'created_time' => $now,
|
||||
'modified_time' => $now,
|
||||
];
|
||||
|
||||
$db->insertObject('#__categories', $obj, 'id');
|
||||
$inserted++;
|
||||
|
||||
$this->catPathCache[$path] = (int) $obj->id;
|
||||
|
||||
// Rebuild category tree for this extension
|
||||
$this->rebuildCategoryTree();
|
||||
}
|
||||
}
|
||||
|
||||
return ['inserted' => $inserted, 'updated' => $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply articles — resolve category by alias path, upsert by alias.
|
||||
*
|
||||
* @param array $articles Article data from payload
|
||||
*
|
||||
* @return array ['inserted' => N, 'updated' => N]
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function applyArticles(array $articles): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
|
||||
foreach ($articles as $article)
|
||||
{
|
||||
$alias = $article['alias'] ?? '';
|
||||
|
||||
if (empty($alias))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve category
|
||||
$catPath = $article['catid_alias_path'] ?? 'uncategorised';
|
||||
$catId = $this->resolveCategoryPath($catPath);
|
||||
|
||||
if ($catId === 0)
|
||||
{
|
||||
$catId = 2; // Joomla's built-in Uncategorised
|
||||
$this->warnings[] = 'Category "' . $catPath . '" not found for article "' . $alias . '" — assigned to Uncategorised';
|
||||
}
|
||||
|
||||
// Check if article exists
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__content'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($alias));
|
||||
|
||||
$db->setQuery($query);
|
||||
$existingId = (int) $db->loadResult();
|
||||
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
if ($existingId)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__content'))
|
||||
->set($db->quoteName('title') . ' = ' . $db->quote($article['title']))
|
||||
->set($db->quoteName('introtext') . ' = ' . $db->quote($article['introtext'] ?? ''))
|
||||
->set($db->quoteName('fulltext') . ' = ' . $db->quote($article['fulltext'] ?? ''))
|
||||
->set($db->quoteName('state') . ' = ' . (int) ($article['state'] ?? 1))
|
||||
->set($db->quoteName('catid') . ' = ' . $catId)
|
||||
->set($db->quoteName('access') . ' = ' . (int) ($article['access'] ?? 1))
|
||||
->set($db->quoteName('language') . ' = ' . $db->quote($article['language'] ?? '*'))
|
||||
->set($db->quoteName('featured') . ' = ' . (int) ($article['featured'] ?? 0))
|
||||
->set($db->quoteName('metadata') . ' = ' . $db->quote(json_encode($article['metadata'] ?? new \stdClass)))
|
||||
->set($db->quoteName('attribs') . ' = ' . $db->quote(json_encode($article['attribs'] ?? new \stdClass)))
|
||||
->set($db->quoteName('images') . ' = ' . $db->quote(json_encode($article['images'] ?? new \stdClass)))
|
||||
->set($db->quoteName('urls') . ' = ' . $db->quote(json_encode($article['urls'] ?? new \stdClass)))
|
||||
->set($db->quoteName('modified') . ' = ' . $db->quote($now))
|
||||
->where($db->quoteName('id') . ' = ' . $existingId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
$updated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj = (object) [
|
||||
'title' => $article['title'],
|
||||
'alias' => $alias,
|
||||
'introtext' => $article['introtext'] ?? '',
|
||||
'fulltext' => $article['fulltext'] ?? '',
|
||||
'state' => (int) ($article['state'] ?? 1),
|
||||
'catid' => $catId,
|
||||
'access' => (int) ($article['access'] ?? 1),
|
||||
'language' => $article['language'] ?? '*',
|
||||
'featured' => (int) ($article['featured'] ?? 0),
|
||||
'publish_up' => $article['publish_up'] ?? $now,
|
||||
'publish_down' => $article['publish_down'],
|
||||
'metadata' => json_encode($article['metadata'] ?? new \stdClass),
|
||||
'attribs' => json_encode($article['attribs'] ?? new \stdClass),
|
||||
'images' => json_encode($article['images'] ?? new \stdClass),
|
||||
'urls' => json_encode($article['urls'] ?? new \stdClass),
|
||||
'created' => $now,
|
||||
'modified' => $now,
|
||||
];
|
||||
|
||||
$db->insertObject('#__content', $obj, 'id');
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
return ['inserted' => $inserted, 'updated' => $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply menu types — insert if not exists.
|
||||
*
|
||||
* @param array $menuTypes Menu type data from payload
|
||||
*
|
||||
* @return array ['inserted' => N, 'updated' => N]
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function applyMenuTypes(array $menuTypes): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
|
||||
foreach ($menuTypes as $mt)
|
||||
{
|
||||
$menutype = $mt['menutype'] ?? '';
|
||||
|
||||
if (empty($menutype))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__menu_types'))
|
||||
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
if ($db->loadResult())
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__menu_types'))
|
||||
->set($db->quoteName('title') . ' = ' . $db->quote($mt['title'] ?? $menutype))
|
||||
->set($db->quoteName('description') . ' = ' . $db->quote($mt['description'] ?? ''))
|
||||
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype));
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
$updated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj = (object) [
|
||||
'title' => $mt['title'] ?? $menutype,
|
||||
'menutype' => $menutype,
|
||||
'description' => $mt['description'] ?? '',
|
||||
];
|
||||
|
||||
$db->insertObject('#__menu_types', $obj);
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
return ['inserted' => $inserted, 'updated' => $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply menu items — resolve parent aliases and {catid:path} tokens.
|
||||
*
|
||||
* @param array $items Menu item data from payload
|
||||
*
|
||||
* @return array ['inserted' => N, 'updated' => N]
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function applyMenuItems(array $items): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
|
||||
// Sort: root items first, then children
|
||||
usort($items, function ($a, $b) {
|
||||
$aIsRoot = empty($a['parent_alias']);
|
||||
$bIsRoot = empty($b['parent_alias']);
|
||||
|
||||
if ($aIsRoot && !$bIsRoot) return -1;
|
||||
if (!$aIsRoot && $bIsRoot) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Resolve component IDs
|
||||
$compQuery = $db->getQuery(true)
|
||||
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
|
||||
$db->setQuery($compQuery);
|
||||
$compMap = [];
|
||||
|
||||
foreach ($db->loadAssocList() ?: [] as $c)
|
||||
{
|
||||
$compMap[$c['element']] = (int) $c['extension_id'];
|
||||
}
|
||||
|
||||
foreach ($items as $item)
|
||||
{
|
||||
$alias = $item['alias'] ?? '';
|
||||
$menutype = $item['menutype'] ?? '';
|
||||
|
||||
if (empty($alias) || empty($menutype))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve parent
|
||||
$parentId = 1; // Root menu item
|
||||
|
||||
if (!empty($item['parent_alias']))
|
||||
{
|
||||
$parentId = $this->resolveMenuAlias($menutype, $item['parent_alias']);
|
||||
|
||||
if ($parentId === 0)
|
||||
{
|
||||
$this->warnings[] = 'Parent menu item "' . $item['parent_alias'] . '" not found for "' . $alias . '"';
|
||||
$parentId = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve {catid:path} tokens in link
|
||||
$link = $item['link'] ?? '';
|
||||
|
||||
if (preg_match_all('/\{catid:([^}]+)\}/', $link, $matches))
|
||||
{
|
||||
foreach ($matches[1] as $i => $catPath)
|
||||
{
|
||||
$localCatId = $this->resolveCategoryPath($catPath);
|
||||
|
||||
if ($localCatId > 0)
|
||||
{
|
||||
$link = str_replace($matches[0][$i], (string) $localCatId, $link);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->warnings[] = 'Could not resolve {catid:' . $catPath . '} in menu item "' . $alias . '"';
|
||||
$link = str_replace($matches[0][$i], '0', $link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$componentId = $compMap[$item['component_name'] ?? ''] ?? 0;
|
||||
|
||||
// Check if menu item exists
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__menu'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
|
||||
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
|
||||
->where($db->quoteName('client_id') . ' = 0');
|
||||
|
||||
$db->setQuery($query);
|
||||
$existingId = (int) $db->loadResult();
|
||||
|
||||
if ($existingId)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__menu'))
|
||||
->set($db->quoteName('title') . ' = ' . $db->quote($item['title']))
|
||||
->set($db->quoteName('link') . ' = ' . $db->quote($link))
|
||||
->set($db->quoteName('type') . ' = ' . $db->quote($item['type'] ?? 'component'))
|
||||
->set($db->quoteName('published') . ' = ' . (int) ($item['published'] ?? 1))
|
||||
->set($db->quoteName('access') . ' = ' . (int) ($item['access'] ?? 1))
|
||||
->set($db->quoteName('language') . ' = ' . $db->quote($item['language'] ?? '*'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($item['params'] ?? new \stdClass)))
|
||||
->set($db->quoteName('parent_id') . ' = ' . $parentId)
|
||||
->set($db->quoteName('component_id') . ' = ' . $componentId)
|
||||
->set($db->quoteName('home') . ' = ' . (int) ($item['home'] ?? 0))
|
||||
->where($db->quoteName('id') . ' = ' . $existingId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
$updated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj = (object) [
|
||||
'menutype' => $menutype,
|
||||
'title' => $item['title'],
|
||||
'alias' => $alias,
|
||||
'path' => $alias,
|
||||
'link' => $link,
|
||||
'type' => $item['type'] ?? 'component',
|
||||
'published' => (int) ($item['published'] ?? 1),
|
||||
'parent_id' => $parentId,
|
||||
'level' => $parentId <= 1 ? 1 : 2,
|
||||
'component_id' => $componentId,
|
||||
'access' => (int) ($item['access'] ?? 1),
|
||||
'language' => $item['language'] ?? '*',
|
||||
'params' => json_encode($item['params'] ?? new \stdClass),
|
||||
'home' => (int) ($item['home'] ?? 0),
|
||||
'client_id' => 0,
|
||||
'lft' => 0,
|
||||
'rgt' => 0,
|
||||
];
|
||||
|
||||
$db->insertObject('#__menu', $obj, 'id');
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild menu tree for each affected menutype
|
||||
$affectedMenuTypes = array_unique(array_column($items, 'menutype'));
|
||||
|
||||
foreach ($affectedMenuTypes as $mt)
|
||||
{
|
||||
$this->rebuildMenuTree($mt);
|
||||
}
|
||||
|
||||
return ['inserted' => $inserted, 'updated' => $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply modules — upsert by title+position+client_id, rebuild menu assignments.
|
||||
*
|
||||
* @param array $modules Module data from payload
|
||||
*
|
||||
* @return array ['inserted' => N, 'updated' => N]
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function applyModules(array $modules): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$inserted = 0;
|
||||
$updated = 0;
|
||||
|
||||
foreach ($modules as $mod)
|
||||
{
|
||||
$title = $mod['title'] ?? '';
|
||||
$position = $mod['position'] ?? '';
|
||||
$clientId = (int) ($mod['client_id'] ?? 0);
|
||||
|
||||
if (empty($title))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check existence by title + position + client_id
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__modules'))
|
||||
->where($db->quoteName('title') . ' = ' . $db->quote($title))
|
||||
->where($db->quoteName('position') . ' = ' . $db->quote($position))
|
||||
->where($db->quoteName('client_id') . ' = ' . $clientId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$existingId = (int) $db->loadResult();
|
||||
|
||||
if ($existingId)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__modules'))
|
||||
->set($db->quoteName('module') . ' = ' . $db->quote($mod['module'] ?? ''))
|
||||
->set($db->quoteName('content') . ' = ' . $db->quote($mod['content'] ?? ''))
|
||||
->set($db->quoteName('published') . ' = ' . (int) ($mod['published'] ?? 1))
|
||||
->set($db->quoteName('access') . ' = ' . (int) ($mod['access'] ?? 1))
|
||||
->set($db->quoteName('language') . ' = ' . $db->quote($mod['language'] ?? '*'))
|
||||
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($mod['params'] ?? new \stdClass)))
|
||||
->where($db->quoteName('id') . ' = ' . $existingId);
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
$updated++;
|
||||
|
||||
$moduleId = $existingId;
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj = (object) [
|
||||
'title' => $title,
|
||||
'module' => $mod['module'] ?? '',
|
||||
'position' => $position,
|
||||
'content' => $mod['content'] ?? '',
|
||||
'published' => (int) ($mod['published'] ?? 1),
|
||||
'access' => (int) ($mod['access'] ?? 1),
|
||||
'language' => $mod['language'] ?? '*',
|
||||
'params' => json_encode($mod['params'] ?? new \stdClass),
|
||||
'client_id' => $clientId,
|
||||
'ordering' => 0,
|
||||
];
|
||||
|
||||
$db->insertObject('#__modules', $obj, 'id');
|
||||
$inserted++;
|
||||
$moduleId = (int) $obj->id;
|
||||
}
|
||||
|
||||
// Rebuild menu assignments
|
||||
$query = $db->getQuery(true)
|
||||
->delete($db->quoteName('#__modules_menu'))
|
||||
->where($db->quoteName('moduleid') . ' = ' . $moduleId);
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
$assignment = $mod['menu_assignment'] ?? [];
|
||||
$assignType = (int) ($assignment['assignment'] ?? 0);
|
||||
$aliases = $assignment['menu_item_aliases'] ?? [];
|
||||
|
||||
if ($assignType === 0 || empty($aliases))
|
||||
{
|
||||
// All pages
|
||||
$obj = (object) ['moduleid' => $moduleId, 'menuid' => 0];
|
||||
$db->insertObject('#__modules_menu', $obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($aliases as $aliasRef)
|
||||
{
|
||||
// Format: "menutype:alias"
|
||||
$parts = explode(':', $aliasRef, 2);
|
||||
|
||||
if (count($parts) !== 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$menuId = $this->resolveMenuAlias($parts[0], $parts[1]);
|
||||
|
||||
if ($menuId > 0)
|
||||
{
|
||||
$menuidValue = $assignType === -1 ? -$menuId : $menuId;
|
||||
$obj = (object) ['moduleid' => $moduleId, 'menuid' => $menuidValue];
|
||||
$db->insertObject('#__modules_menu', $obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->warnings[] = 'Module "' . $title . '": menu item "' . $aliasRef . '" not found';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['inserted' => $inserted, 'updated' => $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a category alias path to a local category ID.
|
||||
*
|
||||
* @param string $path Slash-delimited alias path (e.g. "blog/news")
|
||||
*
|
||||
* @return int Category ID, or 0 if not found
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function resolveCategoryPath(string $path): int
|
||||
{
|
||||
if (isset($this->catPathCache[$path]))
|
||||
{
|
||||
return $this->catPathCache[$path];
|
||||
}
|
||||
|
||||
$db = $this->db;
|
||||
$segments = explode('/', $path);
|
||||
$parentId = 1;
|
||||
|
||||
foreach ($segments as $segment)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__categories'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($segment))
|
||||
->where($db->quoteName('parent_id') . ' = ' . $parentId)
|
||||
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$id = (int) $db->loadResult();
|
||||
|
||||
if ($id === 0)
|
||||
{
|
||||
$this->catPathCache[$path] = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$parentId = $id;
|
||||
}
|
||||
|
||||
$this->catPathCache[$path] = $parentId;
|
||||
|
||||
return $parentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a menu item alias to a local menu ID.
|
||||
*
|
||||
* @param string $menutype Menu type key
|
||||
* @param string $alias Menu item alias
|
||||
*
|
||||
* @return int Menu item ID, or 0 if not found
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function resolveMenuAlias(string $menutype, string $alias): int
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__menu'))
|
||||
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
|
||||
->where($db->quoteName('menutype') . ' = ' . $db->quote($menutype))
|
||||
->where($db->quoteName('client_id') . ' = 0');
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
return (int) $db->loadResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the nested set (lft/rgt) for the category tree.
|
||||
*
|
||||
* Uses Joomla's built-in Table rebuild method.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function rebuildCategoryTree(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$table = \Joomla\CMS\Table\Table::getInstance('Category');
|
||||
$table->rebuild();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->warnings[] = 'Category tree rebuild failed: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the nested set (lft/rgt) for a menu type.
|
||||
*
|
||||
* @param string $menutype Menu type to rebuild
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function rebuildMenuTree(string $menutype): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$table = \Joomla\CMS\Table\Table::getInstance('Menu');
|
||||
$table->rebuild();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->warnings[] = 'Menu tree rebuild failed for "' . $menutype . '": ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,634 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_system_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_system_mokowaas/Service/ContentSyncService.php
|
||||
* VERSION: 02.33.01
|
||||
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Service;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
|
||||
/**
|
||||
* Content Sync Service — builds a JSON payload of site content and pushes
|
||||
* it to one or more remote MokoWaaS sites.
|
||||
*
|
||||
* Content is matched by alias on the receiving end (upsert-by-alias).
|
||||
* Category IDs in menu item links are encoded as {catid:alias/path} tokens
|
||||
* so the receiver can resolve them to local IDs.
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
class ContentSyncService
|
||||
{
|
||||
/**
|
||||
* Maximum items per content type to prevent unbounded memory.
|
||||
*
|
||||
* @var int
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private const MAX_ITEMS = 2000;
|
||||
|
||||
/**
|
||||
* HTTP timeout for push requests in seconds.
|
||||
*
|
||||
* @var int
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private const HTTP_TIMEOUT = 60;
|
||||
|
||||
/**
|
||||
* @var \Joomla\Database\DatabaseInterface
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* Category ID → alias path map cache.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private array $catPathMap = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Joomla\Database\DatabaseInterface|null $db Database driver
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function __construct($db = null)
|
||||
{
|
||||
$this->db = $db ?: Factory::getDbo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full sync payload from local content.
|
||||
*
|
||||
* @return array Structured payload ready for JSON encoding
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function buildPayload(): array
|
||||
{
|
||||
$this->catPathMap = $this->buildCategoryPathMap();
|
||||
|
||||
return [
|
||||
'mokowaas_sync' => '1.0',
|
||||
'source_site' => rtrim(Uri::root(), '/'),
|
||||
'generated_at' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'categories' => $this->buildCategoryPayload(),
|
||||
'articles' => $this->buildArticlePayload(),
|
||||
'menu_types' => $this->buildMenuTypePayload(),
|
||||
'menu_items' => $this->buildMenuItemPayload(),
|
||||
'modules' => $this->buildModulePayload(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Push the sync payload to a single target site.
|
||||
*
|
||||
* @param string $targetUrl Base URL of the target site
|
||||
* @param string $token health_api_token for the target
|
||||
*
|
||||
* @return array Result with status, message, and per-type counts
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function pushToTarget(string $targetUrl, string $token): array
|
||||
{
|
||||
$payload = $this->buildPayload();
|
||||
$jsonBody = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
$endpoint = rtrim($targetUrl, '/') . '/?mokowaas=sync-receive';
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n",
|
||||
'content' => $jsonBody,
|
||||
'timeout' => self::HTTP_TIMEOUT,
|
||||
'ignore_errors' => true,
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($endpoint, false, $context);
|
||||
|
||||
if ($response === false)
|
||||
{
|
||||
return [
|
||||
'status' => 'error',
|
||||
'target' => $targetUrl,
|
||||
'message' => 'Connection failed — target unreachable',
|
||||
];
|
||||
}
|
||||
|
||||
// Parse HTTP status from response headers
|
||||
$httpCode = 0;
|
||||
|
||||
if (isset($http_response_header[0]))
|
||||
{
|
||||
preg_match('/\d{3}/', $http_response_header[0], $matches);
|
||||
$httpCode = (int) ($matches[0] ?? 0);
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if ($httpCode >= 400 || !$result)
|
||||
{
|
||||
return [
|
||||
'status' => 'error',
|
||||
'target' => $targetUrl,
|
||||
'http_code' => $httpCode,
|
||||
'message' => $result['error'] ?? $result['message'] ?? 'Unknown error from target',
|
||||
];
|
||||
}
|
||||
|
||||
$result['target'] = $targetUrl;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push content to all configured sync targets.
|
||||
*
|
||||
* @param array $targets Array of ['url' => ..., 'token' => ..., 'label' => ...]
|
||||
*
|
||||
* @return array Per-target results
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function syncAllTargets(array $targets): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($targets as $target)
|
||||
{
|
||||
$url = $target['url'] ?? '';
|
||||
$token = $target['token'] ?? '';
|
||||
$label = $target['label'] ?? $url;
|
||||
|
||||
if (empty($url) || empty($token))
|
||||
{
|
||||
$results[] = [
|
||||
'status' => 'skipped',
|
||||
'target' => $label,
|
||||
'message' => 'Missing URL or token',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$result = $this->pushToTarget($url, $token);
|
||||
$result['label'] = $label;
|
||||
$results[] = $result;
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'target' => $label,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Log::add(
|
||||
sprintf('Content sync pushed to %d target(s)', count($targets)),
|
||||
Log::INFO,
|
||||
'mokowaas'
|
||||
);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => sprintf('Sync completed for %d target(s)', count($results)),
|
||||
'targets' => $results,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build category ID → alias path map.
|
||||
*
|
||||
* @return array [id => 'parent-alias/child-alias']
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function buildCategoryPathMap(): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true)
|
||||
->select([$db->quoteName('id'), $db->quoteName('alias'), $db->quoteName('parent_id')])
|
||||
->from($db->quoteName('#__categories'))
|
||||
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
|
||||
->where($db->quoteName('published') . ' != -2')
|
||||
->where($db->quoteName('id') . ' > 1');
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList('id');
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($rows as $id => $row)
|
||||
{
|
||||
$map[$id] = $this->resolvePathFromRows($id, $rows);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively build alias path for a category ID.
|
||||
*
|
||||
* @param int $id Category ID
|
||||
* @param array $rows All category rows keyed by ID
|
||||
*
|
||||
* @return string Slash-delimited alias path
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function resolvePathFromRows(int $id, array $rows): string
|
||||
{
|
||||
if (!isset($rows[$id]))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$row = $rows[$id];
|
||||
$parentId = (int) $row['parent_id'];
|
||||
|
||||
if ($parentId <= 1 || !isset($rows[$parentId]))
|
||||
{
|
||||
return $row['alias'];
|
||||
}
|
||||
|
||||
return $this->resolvePathFromRows($parentId, $rows) . '/' . $row['alias'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build category payload.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function buildCategoryPayload(): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('id'),
|
||||
$db->quoteName('title'),
|
||||
$db->quoteName('alias'),
|
||||
$db->quoteName('description'),
|
||||
$db->quoteName('published'),
|
||||
$db->quoteName('access'),
|
||||
$db->quoteName('language'),
|
||||
$db->quoteName('params'),
|
||||
$db->quoteName('metadata'),
|
||||
])
|
||||
->from($db->quoteName('#__categories'))
|
||||
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
|
||||
->where($db->quoteName('published') . ' != -2')
|
||||
->where($db->quoteName('id') . ' > 1')
|
||||
->order($db->quoteName('lft') . ' ASC')
|
||||
->setLimit(self::MAX_ITEMS);
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
$categories = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$categories[] = [
|
||||
'title' => $row['title'],
|
||||
'alias' => $row['alias'],
|
||||
'path' => $this->catPathMap[(int) $row['id']] ?? $row['alias'],
|
||||
'description' => $row['description'] ?? '',
|
||||
'published' => (int) $row['published'],
|
||||
'access' => (int) $row['access'],
|
||||
'language' => $row['language'],
|
||||
'params' => json_decode($row['params'] ?: '{}', true),
|
||||
'metadata' => json_decode($row['metadata'] ?: '{}', true),
|
||||
];
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build article payload.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function buildArticlePayload(): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('title'),
|
||||
$db->quoteName('alias'),
|
||||
$db->quoteName('introtext'),
|
||||
$db->quoteName('fulltext'),
|
||||
$db->quoteName('state'),
|
||||
$db->quoteName('catid'),
|
||||
$db->quoteName('access'),
|
||||
$db->quoteName('language'),
|
||||
$db->quoteName('featured'),
|
||||
$db->quoteName('publish_up'),
|
||||
$db->quoteName('publish_down'),
|
||||
$db->quoteName('metadata'),
|
||||
$db->quoteName('attribs'),
|
||||
$db->quoteName('images'),
|
||||
$db->quoteName('urls'),
|
||||
])
|
||||
->from($db->quoteName('#__content'))
|
||||
->where($db->quoteName('state') . ' != -2')
|
||||
->order($db->quoteName('id') . ' ASC')
|
||||
->setLimit(self::MAX_ITEMS);
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
$articles = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$articles[] = [
|
||||
'title' => $row['title'],
|
||||
'alias' => $row['alias'],
|
||||
'introtext' => $row['introtext'],
|
||||
'fulltext' => $row['fulltext'],
|
||||
'state' => (int) $row['state'],
|
||||
'catid_alias_path' => $this->catPathMap[(int) $row['catid']] ?? 'uncategorised',
|
||||
'access' => (int) $row['access'],
|
||||
'language' => $row['language'],
|
||||
'featured' => (int) $row['featured'],
|
||||
'publish_up' => $row['publish_up'],
|
||||
'publish_down' => $row['publish_down'],
|
||||
'metadata' => json_decode($row['metadata'] ?: '{}', true),
|
||||
'attribs' => json_decode($row['attribs'] ?: '{}', true),
|
||||
'images' => json_decode($row['images'] ?: '{}', true),
|
||||
'urls' => json_decode($row['urls'] ?: '{}', true),
|
||||
];
|
||||
}
|
||||
|
||||
return $articles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build menu type payload.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function buildMenuTypePayload(): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('title'),
|
||||
$db->quoteName('menutype'),
|
||||
$db->quoteName('description'),
|
||||
])
|
||||
->from($db->quoteName('#__menu_types'));
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadAssocList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build menu item payload with {catid:path} tokens in links.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function buildMenuItemPayload(): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('a.title'),
|
||||
$db->quoteName('a.alias'),
|
||||
$db->quoteName('a.menutype'),
|
||||
$db->quoteName('a.parent_id'),
|
||||
$db->quoteName('a.link'),
|
||||
$db->quoteName('a.type'),
|
||||
$db->quoteName('a.published'),
|
||||
$db->quoteName('a.access'),
|
||||
$db->quoteName('a.language'),
|
||||
$db->quoteName('a.params'),
|
||||
$db->quoteName('a.home'),
|
||||
$db->quoteName('a.component_id'),
|
||||
$db->quoteName('b.alias', 'parent_alias'),
|
||||
])
|
||||
->from($db->quoteName('#__menu', 'a'))
|
||||
->leftJoin(
|
||||
$db->quoteName('#__menu', 'b') . ' ON '
|
||||
. $db->quoteName('a.parent_id') . ' = ' . $db->quoteName('b.id')
|
||||
)
|
||||
->where($db->quoteName('a.published') . ' != -2')
|
||||
->where($db->quoteName('a.client_id') . ' = 0')
|
||||
->where($db->quoteName('a.level') . ' >= 1')
|
||||
->order($db->quoteName('a.lft') . ' ASC')
|
||||
->setLimit(self::MAX_ITEMS);
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
// Get component name map
|
||||
$compQuery = $db->getQuery(true)
|
||||
->select([$db->quoteName('extension_id'), $db->quoteName('element')])
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'));
|
||||
$db->setQuery($compQuery);
|
||||
$components = $db->loadAssocList('extension_id') ?: [];
|
||||
|
||||
$items = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$link = $row['link'];
|
||||
|
||||
// Encode category IDs in com_content links as {catid:path} tokens
|
||||
if (preg_match('/option=com_content/', $link) && preg_match('/&id=(\d+)/', $link, $m))
|
||||
{
|
||||
$catId = (int) $m[1];
|
||||
|
||||
if (isset($this->catPathMap[$catId]))
|
||||
{
|
||||
$link = preg_replace('/&id=\d+/', '&id={catid:' . $this->catPathMap[$catId] . '}', $link);
|
||||
}
|
||||
}
|
||||
|
||||
$compName = '';
|
||||
|
||||
if (!empty($row['component_id']) && isset($components[$row['component_id']]))
|
||||
{
|
||||
$compName = $components[$row['component_id']]['element'];
|
||||
}
|
||||
|
||||
// Root-level items have parent_id=1 (Joomla's root menu item)
|
||||
$parentAlias = ((int) $row['parent_id'] <= 1) ? '' : ($row['parent_alias'] ?? '');
|
||||
|
||||
$items[] = [
|
||||
'title' => $row['title'],
|
||||
'alias' => $row['alias'],
|
||||
'menutype' => $row['menutype'],
|
||||
'parent_alias' => $parentAlias,
|
||||
'link' => $link,
|
||||
'type' => $row['type'],
|
||||
'component_name' => $compName,
|
||||
'published' => (int) $row['published'],
|
||||
'access' => (int) $row['access'],
|
||||
'language' => $row['language'],
|
||||
'params' => json_decode($row['params'] ?: '{}', true),
|
||||
'home' => (int) $row['home'],
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build module payload with menu assignments.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function buildModulePayload(): array
|
||||
{
|
||||
$db = $this->db;
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('id'),
|
||||
$db->quoteName('title'),
|
||||
$db->quoteName('module'),
|
||||
$db->quoteName('position'),
|
||||
$db->quoteName('content'),
|
||||
$db->quoteName('published'),
|
||||
$db->quoteName('access'),
|
||||
$db->quoteName('language'),
|
||||
$db->quoteName('params'),
|
||||
$db->quoteName('client_id'),
|
||||
])
|
||||
->from($db->quoteName('#__modules'))
|
||||
->where($db->quoteName('client_id') . ' = 0')
|
||||
->where($db->quoteName('published') . ' != -2')
|
||||
->order($db->quoteName('ordering') . ' ASC')
|
||||
->setLimit(self::MAX_ITEMS);
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
// Get all module-menu assignments
|
||||
$mmQuery = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('mm.moduleid'),
|
||||
$db->quoteName('mm.menuid'),
|
||||
$db->quoteName('m.alias', 'menu_alias'),
|
||||
$db->quoteName('m.menutype'),
|
||||
])
|
||||
->from($db->quoteName('#__modules_menu', 'mm'))
|
||||
->leftJoin(
|
||||
$db->quoteName('#__menu', 'm') . ' ON '
|
||||
. $db->quoteName('mm.menuid') . ' = ' . $db->quoteName('m.id')
|
||||
);
|
||||
$db->setQuery($mmQuery);
|
||||
$allAssignments = $db->loadAssocList();
|
||||
|
||||
// Group assignments by module ID
|
||||
$assignmentsByModule = [];
|
||||
|
||||
foreach ($allAssignments as $a)
|
||||
{
|
||||
$assignmentsByModule[(int) $a['moduleid']][] = $a;
|
||||
}
|
||||
|
||||
$modules = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$moduleId = (int) $row['id'];
|
||||
$assignments = $assignmentsByModule[$moduleId] ?? [];
|
||||
|
||||
// Determine assignment type: 0 = all pages, positive = selected, negative = excluded
|
||||
$menuAliases = [];
|
||||
$assignType = 0;
|
||||
|
||||
if (!empty($assignments))
|
||||
{
|
||||
$firstMenuId = (int) $assignments[0]['menuid'];
|
||||
|
||||
if ($firstMenuId === 0)
|
||||
{
|
||||
$assignType = 0; // All pages
|
||||
}
|
||||
elseif ($firstMenuId < 0)
|
||||
{
|
||||
$assignType = -1; // All except selected
|
||||
foreach ($assignments as $a)
|
||||
{
|
||||
if (!empty($a['menu_alias']))
|
||||
{
|
||||
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$assignType = 1; // Selected only
|
||||
foreach ($assignments as $a)
|
||||
{
|
||||
if (!empty($a['menu_alias']))
|
||||
{
|
||||
$menuAliases[] = $a['menutype'] . ':' . $a['menu_alias'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$modules[] = [
|
||||
'title' => $row['title'],
|
||||
'module' => $row['module'],
|
||||
'position' => $row['position'],
|
||||
'content' => $row['content'],
|
||||
'published' => (int) $row['published'],
|
||||
'access' => (int) $row['access'],
|
||||
'language' => $row['language'],
|
||||
'params' => json_decode($row['params'] ?: '{}', true),
|
||||
'client_id' => (int) $row['client_id'],
|
||||
'menu_assignment' => [
|
||||
'assignment' => $assignType,
|
||||
'menu_item_aliases' => $menuAliases,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $modules;
|
||||
}
|
||||
}
|
||||
@@ -1,606 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_system_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
|
||||
* VERSION: 02.33.01
|
||||
* BRIEF: Content-only snapshot/restore for demo site reset
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Service;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
/**
|
||||
* Demo Reset Service — content-only snapshot and restore.
|
||||
*
|
||||
* Only touches safe content tables (articles, categories, menus, modules,
|
||||
* users, tags, fields). Never touches extensions, assets, sessions,
|
||||
* schemas, update sites, or any system tables.
|
||||
*
|
||||
* @since 02.31.00
|
||||
*/
|
||||
class DemoResetService
|
||||
{
|
||||
private const MAX_NAME_LENGTH = 64;
|
||||
private const BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* Safe content tables to snapshot/restore.
|
||||
* These can be wiped and restored without breaking the Joomla installation.
|
||||
*/
|
||||
private const SAFE_TABLES = [
|
||||
// Content
|
||||
'#__content',
|
||||
'#__content_frontpage',
|
||||
'#__categories',
|
||||
'#__fields',
|
||||
'#__fields_values',
|
||||
'#__fields_groups',
|
||||
'#__tags',
|
||||
'#__contentitem_tag_map',
|
||||
'#__ucm_content',
|
||||
|
||||
// Menus
|
||||
'#__menu',
|
||||
'#__menu_types',
|
||||
|
||||
// Modules
|
||||
'#__modules',
|
||||
'#__modules_menu',
|
||||
|
||||
// Users
|
||||
'#__users',
|
||||
'#__user_usergroup_map',
|
||||
'#__user_profiles',
|
||||
|
||||
// Contact
|
||||
'#__contact_details',
|
||||
|
||||
// Banners
|
||||
'#__banners',
|
||||
'#__banner_clients',
|
||||
'#__banner_tracks',
|
||||
|
||||
// Community Builder
|
||||
'#__comprofiler',
|
||||
'#__comprofiler_fields',
|
||||
'#__comprofiler_field_values',
|
||||
'#__comprofiler_tabs',
|
||||
'#__comprofiler_members',
|
||||
'#__comprofiler_lists',
|
||||
'#__comprofiler_plugin',
|
||||
'#__comprofiler_userreports',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private string $snapshotDir;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private bool $includeMedia;
|
||||
|
||||
/**
|
||||
* @param bool $includeMedia Include /images/ directory
|
||||
* @param string $baseDir Override snapshot root
|
||||
*/
|
||||
public function __construct(bool $includeMedia = true, string $baseDir = '')
|
||||
{
|
||||
$this->includeMedia = $includeMedia;
|
||||
$this->snapshotDir = $baseDir ?: JPATH_ROOT . '/mokowaas-snapshots';
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available snapshots.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function listSnapshots(): array
|
||||
{
|
||||
$snapshots = [];
|
||||
|
||||
if (!is_dir($this->snapshotDir))
|
||||
{
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
foreach (glob($this->snapshotDir . '/*/manifest.json') as $path)
|
||||
{
|
||||
$data = json_decode(file_get_contents($path), true);
|
||||
|
||||
if ($data && isset($data['name']))
|
||||
{
|
||||
$snapshots[$data['name']] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a content snapshot.
|
||||
*
|
||||
* @param string $name Snapshot name
|
||||
*
|
||||
* @return array Result
|
||||
*/
|
||||
public function createSnapshot(string $name): array
|
||||
{
|
||||
$this->validateSnapshotName($name);
|
||||
$this->ensureSnapshotDir();
|
||||
|
||||
$path = $this->getSnapshotPath($name);
|
||||
|
||||
if (is_dir($path))
|
||||
{
|
||||
$this->removeDirectory($path);
|
||||
}
|
||||
|
||||
mkdir($path, 0755, true);
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$prefix = $db->getPrefix();
|
||||
$allTables = $db->getTableList();
|
||||
$dumped = 0;
|
||||
|
||||
foreach (self::SAFE_TABLES as $logicalName)
|
||||
{
|
||||
$realName = str_replace('#__', $prefix, $logicalName);
|
||||
|
||||
if (!in_array($realName, $allTables))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->dumpTable($logicalName, $realName, $path, $db);
|
||||
$dumped++;
|
||||
}
|
||||
|
||||
// Media
|
||||
$hasMedia = false;
|
||||
|
||||
if ($this->includeMedia && is_dir(JPATH_ROOT . '/images'))
|
||||
{
|
||||
$hasMedia = $this->zipDirectory(JPATH_ROOT . '/images', $path . '/media.zip');
|
||||
}
|
||||
|
||||
$manifest = [
|
||||
'name' => $name,
|
||||
'created_at' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'type' => 'content-only',
|
||||
'tables' => $dumped,
|
||||
'has_media' => $hasMedia,
|
||||
'joomla_version' => JVERSION,
|
||||
];
|
||||
|
||||
file_put_contents($path . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
Log::add(sprintf('Demo snapshot "%s" created (%d tables, media=%s)', $name, $dumped, $hasMedia ? 'yes' : 'no'), Log::INFO, 'mokowaas');
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'Snapshot created',
|
||||
'name' => $name,
|
||||
'tables' => $dumped,
|
||||
'has_media' => $hasMedia,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from a snapshot.
|
||||
*
|
||||
* @param string $name Snapshot name
|
||||
*
|
||||
* @return array Result
|
||||
*/
|
||||
public function restoreSnapshot(string $name): array
|
||||
{
|
||||
$this->validateSnapshotName($name);
|
||||
|
||||
$path = $this->getSnapshotPath($name);
|
||||
$manifest = $path . '/manifest.json';
|
||||
|
||||
if (!file_exists($manifest))
|
||||
{
|
||||
throw new \RuntimeException('Snapshot not found: ' . $name);
|
||||
}
|
||||
|
||||
$manifestData = json_decode(file_get_contents($manifest), true);
|
||||
|
||||
// Clear cache
|
||||
try { Factory::getCache('')->clean(''); } catch (\Throwable $e) {}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$prefix = $db->getPrefix();
|
||||
$restored = 0;
|
||||
$sqlFiles = glob($path . '/*.sql');
|
||||
|
||||
foreach ($sqlFiles as $sqlFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->restoreTable($sqlFile, $db, $prefix);
|
||||
$restored++;
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Demo reset: failed to restore ' . basename($sqlFile) . ': ' . $e->getMessage(), Log::ERROR, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
// Restore /images/
|
||||
$mediaRestored = false;
|
||||
|
||||
if (($manifestData['has_media'] ?? false) && file_exists($path . '/media.zip'))
|
||||
{
|
||||
$this->clearDirectory(JPATH_ROOT . '/images');
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($path . '/media.zip') === true)
|
||||
{
|
||||
$zip->extractTo(JPATH_ROOT . '/images');
|
||||
$zip->close();
|
||||
$mediaRestored = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild assets table to fix ACL after content restore
|
||||
$this->rebuildAssets();
|
||||
|
||||
Log::add(sprintf('Demo site reset (%d tables, media=%s)', $restored, $mediaRestored ? 'yes' : 'no'), Log::WARNING, 'mokowaas');
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'Site content restored',
|
||||
'baseline' => $name,
|
||||
'restored_tables' => $restored,
|
||||
'media_restored' => $mediaRestored,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a snapshot.
|
||||
*/
|
||||
public function deleteSnapshot(string $name): bool
|
||||
{
|
||||
$this->validateSnapshotName($name);
|
||||
$path = $this->getSnapshotPath($name);
|
||||
|
||||
if (!is_dir($path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->removeDirectory($path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the assets table after content restore.
|
||||
*
|
||||
* Deletes content-related asset entries (which now have stale IDs)
|
||||
* and rebuilds them using Joomla's Table classes. Extension and
|
||||
* component-level assets are left untouched.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function rebuildAssets(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Delete content-level assets (articles, categories, modules, etc.)
|
||||
// Keep component-level and root assets intact
|
||||
$contentAssetPrefixes = [
|
||||
'com_content.article.%',
|
||||
'com_content.category.%',
|
||||
'com_contact.contact.%',
|
||||
'com_banners.banner.%',
|
||||
'com_banners.category.%',
|
||||
'com_modules.module.%',
|
||||
'com_menus.menu.%',
|
||||
'com_users.user.%',
|
||||
];
|
||||
|
||||
foreach ($contentAssetPrefixes as $prefix)
|
||||
{
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__assets'))
|
||||
->where($db->quoteName('name') . ' LIKE ' . $db->quote($prefix))
|
||||
);
|
||||
$db->execute();
|
||||
}
|
||||
|
||||
// Rebuild category tree (also fixes category assets)
|
||||
$catTable = \Joomla\CMS\Table\Table::getInstance('Category');
|
||||
|
||||
if ($catTable)
|
||||
{
|
||||
$catTable->rebuild();
|
||||
}
|
||||
|
||||
// Rebuild menu tree
|
||||
$menuTable = \Joomla\CMS\Table\Table::getInstance('Menu');
|
||||
|
||||
if ($menuTable)
|
||||
{
|
||||
$menuTable->rebuild();
|
||||
}
|
||||
|
||||
// Rebuild asset tree
|
||||
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
|
||||
|
||||
if ($assetTable)
|
||||
{
|
||||
$assetTable->rebuild();
|
||||
}
|
||||
|
||||
// Re-create assets for content items that lost theirs
|
||||
$this->fixContentAssets($db);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Asset rebuild warning: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-create missing asset entries for content items.
|
||||
*
|
||||
* After deleting stale assets and restoring content, some items
|
||||
* may reference asset_id=0. This creates new asset rows for them.
|
||||
*
|
||||
* @param \Joomla\Database\DatabaseInterface $db
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function fixContentAssets($db): void
|
||||
{
|
||||
// Fix articles with missing assets
|
||||
$query = $db->getQuery(true)
|
||||
->select([$db->quoteName('id'), $db->quoteName('title'), $db->quoteName('alias')])
|
||||
->from($db->quoteName('#__content'))
|
||||
->where($db->quoteName('asset_id') . ' = 0');
|
||||
|
||||
$db->setQuery($query);
|
||||
$articles = $db->loadAssocList() ?: [];
|
||||
|
||||
// Find the com_content component asset as parent
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__assets'))
|
||||
->where($db->quoteName('name') . ' = ' . $db->quote('com_content'))
|
||||
);
|
||||
$contentAssetId = (int) $db->loadResult();
|
||||
|
||||
foreach ($articles as $article)
|
||||
{
|
||||
$assetName = 'com_content.article.' . (int) $article['id'];
|
||||
|
||||
$asset = (object) [
|
||||
'parent_id' => $contentAssetId ?: 1,
|
||||
'lft' => 0,
|
||||
'rgt' => 0,
|
||||
'level' => 0,
|
||||
'name' => $assetName,
|
||||
'title' => $article['title'],
|
||||
'rules' => '{}',
|
||||
];
|
||||
|
||||
$db->insertObject('#__assets', $asset, 'id');
|
||||
|
||||
// Update content row with new asset_id
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__content'))
|
||||
->set($db->quoteName('asset_id') . ' = ' . (int) $asset->id)
|
||||
->where($db->quoteName('id') . ' = ' . (int) $article['id'])
|
||||
);
|
||||
$db->execute();
|
||||
}
|
||||
|
||||
// Rebuild asset tree again after inserts
|
||||
try
|
||||
{
|
||||
$assetTable = \Joomla\CMS\Table\Table::getInstance('Asset');
|
||||
|
||||
if ($assetTable)
|
||||
{
|
||||
$assetTable->rebuild();
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Best effort
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private function dumpTable(string $logicalName, string $realName, string $dir, $db): void
|
||||
{
|
||||
$safeFileName = str_replace('#__', 'jml__', $logicalName);
|
||||
$fp = fopen($dir . '/' . $safeFileName . '.sql', 'w');
|
||||
|
||||
$columns = $db->getTableColumns($realName, false);
|
||||
$colNames = array_keys($columns);
|
||||
$quotedCols = array_map([$db, 'quoteName'], $colNames);
|
||||
$colList = implode(', ', $quotedCols);
|
||||
$offset = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName($realName))
|
||||
->setLimit(self::BATCH_SIZE, $offset);
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
if (empty($rows))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$values = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$vals = [];
|
||||
|
||||
foreach ($colNames as $col)
|
||||
{
|
||||
$vals[] = $row[$col] === null ? 'NULL' : $db->quote($row[$col]);
|
||||
}
|
||||
|
||||
$values[] = '(' . implode(', ', $vals) . ')';
|
||||
}
|
||||
|
||||
fwrite($fp, 'INSERT INTO ' . $db->quoteName($realName) . ' (' . $colList . ') VALUES ' . "\n" . implode(",\n", $values) . ";\n\n");
|
||||
|
||||
$offset += self::BATCH_SIZE;
|
||||
|
||||
if (count($rows) < self::BATCH_SIZE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
private function restoreTable(string $sqlFile, $db, string $prefix): void
|
||||
{
|
||||
$baseName = basename($sqlFile, '.sql');
|
||||
$realTable = str_replace('jml__', $prefix, $baseName);
|
||||
|
||||
$db->setQuery('TRUNCATE TABLE ' . $db->quoteName($realTable));
|
||||
$db->execute();
|
||||
|
||||
$sql = file_get_contents($sqlFile);
|
||||
|
||||
if (empty(trim($sql)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$statements = array_filter(
|
||||
array_map('trim', explode(";\n", $sql)),
|
||||
function ($s) { return !empty($s) && $s !== ';'; }
|
||||
);
|
||||
|
||||
foreach ($statements as $statement)
|
||||
{
|
||||
$statement = rtrim($statement, ';');
|
||||
|
||||
if (empty($statement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$db->setQuery($statement);
|
||||
$db->execute();
|
||||
}
|
||||
}
|
||||
|
||||
private function zipDirectory(string $sourceDir, string $zipPath): bool
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($sourceDir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $item)
|
||||
{
|
||||
$rel = str_replace('\\', '/', substr($item->getPathname(), strlen($sourceDir) + 1));
|
||||
$item->isDir() ? $zip->addEmptyDir($rel) : $zip->addFile($item->getPathname(), $rel);
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function ensureSnapshotDir(): void
|
||||
{
|
||||
if (!is_dir($this->snapshotDir))
|
||||
{
|
||||
mkdir($this->snapshotDir, 0755, true);
|
||||
}
|
||||
|
||||
if (!file_exists($this->snapshotDir . '/.htaccess'))
|
||||
{
|
||||
file_put_contents($this->snapshotDir . '/.htaccess', "Deny from all\n");
|
||||
}
|
||||
}
|
||||
|
||||
private function getSnapshotPath(string $name): string
|
||||
{
|
||||
return $this->snapshotDir . '/' . $name;
|
||||
}
|
||||
|
||||
private function validateSnapshotName(string $name): void
|
||||
{
|
||||
if ($name === '' || strlen($name) > self::MAX_NAME_LENGTH || !preg_match('/^[a-zA-Z0-9_-]+$/', $name))
|
||||
{
|
||||
throw new \InvalidArgumentException('Invalid snapshot name');
|
||||
}
|
||||
}
|
||||
|
||||
private function removeDirectory(string $dir): void
|
||||
{
|
||||
$items = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($items as $item)
|
||||
{
|
||||
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
|
||||
}
|
||||
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
private function clearDirectory(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir)) return;
|
||||
|
||||
$items = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($items as $item)
|
||||
{
|
||||
$item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
+2
-2
@@ -15,5 +15,5 @@
|
||||
; Variables: (none)
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
|
||||
|
||||
+2
-2
@@ -15,5 +15,5 @@
|
||||
; Variables: (none)
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS="System - Moko WaaS"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
|
||||
|
||||
+58
-60
@@ -10,111 +10,109 @@
|
||||
; Version: 02.01.08
|
||||
; File: en-GB.override.ini
|
||||
; Path: administrator/language/overrides/en-GB.override.ini
|
||||
; Brief: Admin override TEMPLATE — placeholders resolved at runtime/install.
|
||||
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
|
||||
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
|
||||
; Brief: Admin language overrides — values are hardcoded.
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
; ===== Footer & template branding =====
|
||||
TPL_ATUM_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
TPL_ATUM_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
; ===== Control panel greetings =====
|
||||
COM_CPANEL_WELCOME_TITLE="Welcome to {{BRAND_NAME}}!"
|
||||
COM_CPANEL_MSG_WELCOME="Welcome to {{BRAND_NAME}}!"
|
||||
COM_CPANEL_WELCOME_TITLE="Welcome to MokoWaaS!"
|
||||
COM_CPANEL_MSG_WELCOME="Welcome to MokoWaaS!"
|
||||
|
||||
; ===== Help/Docs phrasing =====
|
||||
COM_ADMIN_HELP_SITE="{{BRAND_NAME}} Help"
|
||||
COM_ADMIN_HELPSITE_FIELD_LABEL="{{BRAND_NAME}} Help"
|
||||
COM_ADMIN_HELP_SITE="MokoWaaS Help"
|
||||
COM_ADMIN_HELPSITE_FIELD_LABEL="MokoWaaS Help"
|
||||
|
||||
; ===== Generic replacements =====
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
|
||||
COM_INSTALLER_TYPE_JOOMLA="{{BRAND_NAME}} Package"
|
||||
LIB_JOOMLA="{{BRAND_NAME}} Library"
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
|
||||
COM_INSTALLER_TYPE_JOOMLA="MokoWaaS Package"
|
||||
LIB_JOOMLA="MokoWaaS Library"
|
||||
|
||||
; ===== System messages =====
|
||||
JERROR_JOOMLA="{{BRAND_NAME}} Error"
|
||||
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
|
||||
JERROR_JOOMLA="MokoWaaS Error"
|
||||
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
|
||||
|
||||
; ===== AdminLogin Support =====
|
||||
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
|
||||
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit {{COMPANY_NAME}}:"
|
||||
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to {{COMPANY_NAME}} support on the login screen."
|
||||
TPL_ATUM_BACKEND_LOGIN="{{BRAND_NAME}} Administrator Login"
|
||||
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
|
||||
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit Moko Consulting:"
|
||||
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to Moko Consulting support on the login screen."
|
||||
TPL_ATUM_BACKEND_LOGIN="MokoWaaS Administrator Login"
|
||||
|
||||
; ===== Error messages =====
|
||||
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
|
||||
|
||||
; ===== Admin-specific branding =====
|
||||
COM_ADMIN_VIEW_HOME_TITLE="{{BRAND_NAME}} Control Panel"
|
||||
JLIB_APPLICATION_ERROR_SAVE_FAILED="{{BRAND_NAME}} Error: Save failed"
|
||||
COM_ADMIN_VIEW_HOME_TITLE="MokoWaaS Control Panel"
|
||||
JLIB_APPLICATION_ERROR_SAVE_FAILED="MokoWaaS Error: Save failed"
|
||||
|
||||
; ===== Module list workaround (RegularLabs) =====
|
||||
COM_MODULES_HEADING_POSITION="Position"
|
||||
|
||||
; ===== Extensions =====
|
||||
COM_INSTALLER_TYPE_TYPE_JOOMLA="{{BRAND_NAME}}"
|
||||
COM_INSTALLER_TYPE_TYPE_JOOMLA="MokoWaaS"
|
||||
COM_INSTALLER_MSG_UPDATE_SUCCESS="Update installed successfully"
|
||||
|
||||
; ===== Dashboard =====
|
||||
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to {{BRAND_NAME}}!"
|
||||
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Documentation</a></li><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Support</a></li></ul>"
|
||||
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in {{BRAND_NAME}}"
|
||||
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to MokoWaaS!"
|
||||
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Documentation</a></li><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Support</a></li></ul>"
|
||||
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in MokoWaaS"
|
||||
|
||||
; ===== Quick Icons =====
|
||||
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking {{BRAND_NAME}}…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown {{BRAND_NAME}}…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="{{BRAND_NAME}} is up to date."
|
||||
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking MokoWaaS…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown MokoWaaS…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="MokoWaaS is up to date."
|
||||
|
||||
; ===== System Info =====
|
||||
COM_ADMIN_JOOMLA_VERSION="{{BRAND_NAME}} Version"
|
||||
COM_ADMIN_HELP="{{BRAND_NAME}} Help"
|
||||
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="{{BRAND_NAME}} Backward Compatibility Plugin"
|
||||
COM_ADMIN_JOOMLA_VERSION="MokoWaaS Version"
|
||||
COM_ADMIN_HELP="MokoWaaS Help"
|
||||
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="MokoWaaS Backward Compatibility Plugin"
|
||||
|
||||
; ===== Installer =====
|
||||
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install {{BRAND_NAME}} Extension"
|
||||
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The {{BRAND_NAME}} package cannot be installed through the Extension Manager. Please use the {{BRAND_NAME}} Update component to update."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The {{BRAND_NAME}} temporary folder is not set."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The {{BRAND_NAME}} temporary folder is not writable or does not exist."
|
||||
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your {{BRAND_NAME}} installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
|
||||
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install MokoWaaS Extension"
|
||||
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The MokoWaaS package cannot be installed through the Extension Manager. Please use the MokoWaaS Update component to update."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The MokoWaaS temporary folder is not set."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The MokoWaaS temporary folder is not writable or does not exist."
|
||||
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your MokoWaaS installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
|
||||
|
||||
; ===== Global Configuration =====
|
||||
COM_CONFIG_FIELD_METAVERSION_LABEL="{{BRAND_NAME}} Version"
|
||||
COM_CONFIG_FIELD_METAVERSION_LABEL="MokoWaaS Version"
|
||||
|
||||
; ===== Update component =====
|
||||
COM_JOOMLAUPDATE_CONFIGURATION="{{BRAND_NAME}} Update: Options"
|
||||
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="{{BRAND_NAME}} Next"
|
||||
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where {{BRAND_NAME}} gets its update information from."
|
||||
COM_JOOMLAUPDATE_CONFIGURATION="MokoWaaS Update: Options"
|
||||
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="MokoWaaS Next"
|
||||
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where MokoWaaS gets its update information from."
|
||||
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="{{BRAND_NAME}} Update"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="{{BRAND_NAME}} Update Component"
|
||||
COM_JOOMLAUPDATE_NOCHANGE="{{BRAND_NAME}} is up to date."
|
||||
COM_JOOMLAUPDATE_PREUPDATE_CHECK="{{BRAND_NAME}} Pre-Update Check"
|
||||
COM_JOOMLAUPDATE_UPDATE_HEADER="{{BRAND_NAME}} Update"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="MokoWaaS Update"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="MokoWaaS Update Component"
|
||||
COM_JOOMLAUPDATE_NOCHANGE="MokoWaaS is up to date."
|
||||
COM_JOOMLAUPDATE_PREUPDATE_CHECK="MokoWaaS Pre-Update Check"
|
||||
COM_JOOMLAUPDATE_UPDATE_HEADER="MokoWaaS Update"
|
||||
COM_JOOMLAUPDATE_LIVEUPDATE="Live Update"
|
||||
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for {{BRAND_NAME}} updates."
|
||||
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for MokoWaaS updates."
|
||||
|
||||
; ===== Privacy =====
|
||||
COM_PRIVACY_HEADING_CORE_CAPABILITIES="{{BRAND_NAME}} Core Capabilities"
|
||||
COM_PRIVACY_HEADING_CORE_CAPABILITIES="MokoWaaS Core Capabilities"
|
||||
|
||||
; ===== Database & Library errors =====
|
||||
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum {{BRAND_NAME}} version requirement of J%s"
|
||||
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find {{BRAND_NAME}} XML setup file."
|
||||
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum MokoWaaS version requirement of J%s"
|
||||
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find MokoWaaS XML setup file."
|
||||
|
||||
; ===== Version and About =====
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
COM_ADMIN_HELP_DOCUMENTATION="{{BRAND_NAME}} Documentation"
|
||||
COM_ADMIN_HELP_SUPPORT="{{BRAND_NAME}} Support"
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
COM_ADMIN_HELP_DOCUMENTATION="MokoWaaS Documentation"
|
||||
COM_ADMIN_HELP_SUPPORT="MokoWaaS Support"
|
||||
|
||||
; ===== Akeeba Ticket System (ATS) =====
|
||||
COM_ATS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
|
||||
COM_ATS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
|
||||
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
|
||||
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
|
||||
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
+58
-60
@@ -10,111 +10,109 @@
|
||||
; Version: 02.01.08
|
||||
; File: en-US.override.ini
|
||||
; Path: administrator/language/overrides/en-US.override.ini
|
||||
; Brief: Admin override TEMPLATE — placeholders resolved at runtime/install.
|
||||
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
|
||||
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
|
||||
; Brief: Admin language overrides — values are hardcoded.
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
; ===== Footer & template branding =====
|
||||
TPL_ATUM_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
TPL_ATUM_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
; ===== Control panel greetings =====
|
||||
COM_CPANEL_WELCOME_TITLE="Welcome to {{BRAND_NAME}}!"
|
||||
COM_CPANEL_MSG_WELCOME="Welcome to {{BRAND_NAME}}!"
|
||||
COM_CPANEL_WELCOME_TITLE="Welcome to MokoWaaS!"
|
||||
COM_CPANEL_MSG_WELCOME="Welcome to MokoWaaS!"
|
||||
|
||||
; ===== Help/Docs phrasing =====
|
||||
COM_ADMIN_HELP_SITE="{{BRAND_NAME}} Help"
|
||||
COM_ADMIN_HELPSITE_FIELD_LABEL="{{BRAND_NAME}} Help"
|
||||
COM_ADMIN_HELP_SITE="MokoWaaS Help"
|
||||
COM_ADMIN_HELPSITE_FIELD_LABEL="MokoWaaS Help"
|
||||
|
||||
; ===== Generic replacements =====
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
|
||||
COM_INSTALLER_TYPE_JOOMLA="{{BRAND_NAME}} Package"
|
||||
LIB_JOOMLA="{{BRAND_NAME}} Library"
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
|
||||
COM_INSTALLER_TYPE_JOOMLA="MokoWaaS Package"
|
||||
LIB_JOOMLA="MokoWaaS Library"
|
||||
|
||||
; ===== System messages =====
|
||||
JERROR_JOOMLA="{{BRAND_NAME}} Error"
|
||||
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
|
||||
JERROR_JOOMLA="MokoWaaS Error"
|
||||
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
|
||||
|
||||
; ===== AdminLogin Support =====
|
||||
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
|
||||
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit {{COMPANY_NAME}}:"
|
||||
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to {{COMPANY_NAME}} support on the login screen."
|
||||
TPL_ATUM_BACKEND_LOGIN="{{BRAND_NAME}} Administrator Login"
|
||||
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
|
||||
MOD_LOGINSUPPORT_HEADLINE="Need help? Visit Moko Consulting:"
|
||||
MOD_LOGINSUPPORT_XML_DESCRIPTION="This module displays useful links to Moko Consulting support on the login screen."
|
||||
TPL_ATUM_BACKEND_LOGIN="MokoWaaS Administrator Login"
|
||||
|
||||
; ===== Error messages =====
|
||||
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
|
||||
|
||||
; ===== Admin-specific branding =====
|
||||
COM_ADMIN_VIEW_HOME_TITLE="{{BRAND_NAME}} Control Panel"
|
||||
JLIB_APPLICATION_ERROR_SAVE_FAILED="{{BRAND_NAME}} Error: Save failed"
|
||||
COM_ADMIN_VIEW_HOME_TITLE="MokoWaaS Control Panel"
|
||||
JLIB_APPLICATION_ERROR_SAVE_FAILED="MokoWaaS Error: Save failed"
|
||||
|
||||
; ===== Module list workaround (RegularLabs) =====
|
||||
COM_MODULES_HEADING_POSITION="Position"
|
||||
|
||||
; ===== Extensions =====
|
||||
COM_INSTALLER_TYPE_TYPE_JOOMLA="{{BRAND_NAME}}"
|
||||
COM_INSTALLER_TYPE_TYPE_JOOMLA="MokoWaaS"
|
||||
COM_INSTALLER_MSG_UPDATE_SUCCESS="Update installed successfully"
|
||||
|
||||
; ===== Dashboard =====
|
||||
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to {{BRAND_NAME}}!"
|
||||
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Documentation</a></li><li><a href=\"{{SUPPORT_URL}}\" target=\"_blank\" rel=\"noopener noreferrer\">{{BRAND_NAME}} Support</a></li></ul>"
|
||||
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in {{BRAND_NAME}}"
|
||||
COM_CPANEL_WELCOME_BEGINNERS_TITLE="Welcome to MokoWaaS!"
|
||||
COM_CPANEL_WELCOME_BEGINNERS_MESSAGE="<p>Community resources are available for new users.</p><ul><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Documentation</a></li><li><a href=\"https://mokoconsulting.tech/support\" target=\"_blank\" rel=\"noopener noreferrer\">MokoWaaS Support</a></li></ul>"
|
||||
COM_CPANEL_MSG_STATS_COLLECTION_TITLE="Stats Collection in MokoWaaS"
|
||||
|
||||
; ===== Quick Icons =====
|
||||
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking {{BRAND_NAME}}…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown {{BRAND_NAME}}…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="{{BRAND_NAME}} is up to date."
|
||||
PLG_QUICKICON_JOOMLAUPDATE_CHECKING="Checking MokoWaaS…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_ERROR="Unknown MokoWaaS…"
|
||||
PLG_QUICKICON_JOOMLAUPDATE_UPTODATE="MokoWaaS is up to date."
|
||||
|
||||
; ===== System Info =====
|
||||
COM_ADMIN_JOOMLA_VERSION="{{BRAND_NAME}} Version"
|
||||
COM_ADMIN_HELP="{{BRAND_NAME}} Help"
|
||||
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="{{BRAND_NAME}} Backward Compatibility Plugin"
|
||||
COM_ADMIN_JOOMLA_VERSION="MokoWaaS Version"
|
||||
COM_ADMIN_HELP="MokoWaaS Help"
|
||||
COM_ADMIN_JOOMLA_COMPAT_PLUGIN="MokoWaaS Backward Compatibility Plugin"
|
||||
|
||||
; ===== Installer =====
|
||||
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install {{BRAND_NAME}} Extension"
|
||||
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The {{BRAND_NAME}} package cannot be installed through the Extension Manager. Please use the {{BRAND_NAME}} Update component to update."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The {{BRAND_NAME}} temporary folder is not set."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The {{BRAND_NAME}} temporary folder is not writable or does not exist."
|
||||
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your {{BRAND_NAME}} installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
|
||||
COM_INSTALLER_UPLOAD_INSTALL_JOOMLA_EXTENSION="Upload & Install MokoWaaS Extension"
|
||||
COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE="The MokoWaaS package cannot be installed through the Extension Manager. Please use the MokoWaaS Update component to update."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET="The MokoWaaS temporary folder is not set."
|
||||
COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE="The MokoWaaS temporary folder is not writable or does not exist."
|
||||
COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE="Before updating ensure that the update is compatible with your MokoWaaS installation. <br>You are strongly advised to make a <strong>backup</strong> of your site's files and database before you start updating."
|
||||
|
||||
; ===== Global Configuration =====
|
||||
COM_CONFIG_FIELD_METAVERSION_LABEL="{{BRAND_NAME}} Version"
|
||||
COM_CONFIG_FIELD_METAVERSION_LABEL="MokoWaaS Version"
|
||||
|
||||
; ===== Update component =====
|
||||
COM_JOOMLAUPDATE_CONFIGURATION="{{BRAND_NAME}} Update: Options"
|
||||
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="{{BRAND_NAME}} Next"
|
||||
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where {{BRAND_NAME}} gets its update information from."
|
||||
COM_JOOMLAUPDATE_CONFIGURATION="MokoWaaS Update: Options"
|
||||
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT="MokoWaaS Next"
|
||||
COM_JOOMLAUPDATE_CONFIG_SOURCES_DESC="Configure where MokoWaaS gets its update information from."
|
||||
COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_LABEL="Update Channel"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="{{BRAND_NAME}} Update"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="{{BRAND_NAME}} Update Component"
|
||||
COM_JOOMLAUPDATE_NOCHANGE="{{BRAND_NAME}} is up to date."
|
||||
COM_JOOMLAUPDATE_PREUPDATE_CHECK="{{BRAND_NAME}} Pre-Update Check"
|
||||
COM_JOOMLAUPDATE_UPDATE_HEADER="{{BRAND_NAME}} Update"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_TITLE="MokoWaaS Update"
|
||||
COM_JOOMLAUPDATE_VIEW_DEFAULT_DESCRIPTION="MokoWaaS Update Component"
|
||||
COM_JOOMLAUPDATE_NOCHANGE="MokoWaaS is up to date."
|
||||
COM_JOOMLAUPDATE_PREUPDATE_CHECK="MokoWaaS Pre-Update Check"
|
||||
COM_JOOMLAUPDATE_UPDATE_HEADER="MokoWaaS Update"
|
||||
COM_JOOMLAUPDATE_LIVEUPDATE="Live Update"
|
||||
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for {{BRAND_NAME}} updates."
|
||||
COM_JOOMLAUPDATE_CHECKEDFOR_UPDATES="Checked for MokoWaaS updates."
|
||||
|
||||
; ===== Privacy =====
|
||||
COM_PRIVACY_HEADING_CORE_CAPABILITIES="{{BRAND_NAME}} Core Capabilities"
|
||||
COM_PRIVACY_HEADING_CORE_CAPABILITIES="MokoWaaS Core Capabilities"
|
||||
|
||||
; ===== Database & Library errors =====
|
||||
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum {{BRAND_NAME}} version requirement of J%s"
|
||||
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find {{BRAND_NAME}} XML setup file."
|
||||
JLIB_INSTALLER_MINIMUM_JOOMLA="You don't have the minimum MokoWaaS version requirement of J%s"
|
||||
JLIB_INSTALLER_ERROR_NOTFINDJOOMLAXMLSETUPFILE="Installer: Can't find MokoWaaS XML setup file."
|
||||
|
||||
; ===== Version and About =====
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
COM_ADMIN_HELP_DOCUMENTATION="{{BRAND_NAME}} Documentation"
|
||||
COM_ADMIN_HELP_SUPPORT="{{BRAND_NAME}} Support"
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
COM_ADMIN_HELP_DOCUMENTATION="MokoWaaS Documentation"
|
||||
COM_ADMIN_HELP_SUPPORT="MokoWaaS Support"
|
||||
|
||||
; ===== Akeeba Ticket System (ATS) =====
|
||||
COM_ATS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
|
||||
COM_ATS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
|
||||
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
|
||||
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
|
||||
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form>
|
||||
<field name="url" type="url"
|
||||
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC"
|
||||
required="true" hint="https://client.example.com" />
|
||||
<field name="token" type="text"
|
||||
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC"
|
||||
required="true" hint="health_api_token from target site" />
|
||||
<field name="label" type="text"
|
||||
label="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC"
|
||||
hint="e.g. Client A" />
|
||||
</form>
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form>
|
||||
<field
|
||||
name="ip"
|
||||
type="text"
|
||||
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC"
|
||||
required="true"
|
||||
hint="e.g. 192.168.1.100 or 10.0.0.0/24"
|
||||
/>
|
||||
<field
|
||||
name="label"
|
||||
type="text"
|
||||
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC"
|
||||
hint="e.g. Office network"
|
||||
/>
|
||||
<field
|
||||
name="enabled"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL"
|
||||
default="1"
|
||||
class="btn-group btn-group-yesno"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</form>
|
||||
@@ -7,180 +7,20 @@
|
||||
; FILE INFORMATION
|
||||
; Defgroup: Joomla Language
|
||||
; Ingroup: MokoWaaS
|
||||
; Version: 02.01.08
|
||||
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
|
||||
; File: plg_system_mokowaas.ini
|
||||
; Path: /src/language/en-GB/plg_system_mokowaas.ini
|
||||
; Brief: English language strings for MokoWaaS system plugin
|
||||
; Notes: Contains translatable strings for plugin functionality
|
||||
; Variables: (none)
|
||||
; Brief: English language strings for MokoWaaS core system plugin
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL="Enable Branding"
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_DESC="Enable or disable the branding overrides across the system."
|
||||
; ===== Core fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_LABEL="Brand Name"
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_DESC="The brand name that replaces 'Joomla' throughout the interface. Used in all language overrides."
|
||||
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
|
||||
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
|
||||
|
||||
; ===== WaaS Access fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_SUCCESS="Emergency access LOGIN by {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_BLOCKED_IP="Emergency access BLOCKED (unauthorized IP) — {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_WRONG_PASSWORD="Emergency access FAILED (wrong password) — {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_VERIFY_FILE_CREATED="Emergency access verification file created — {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_PENDING_FILE_DELETE="Emergency access pending file deletion — {username} from {ip}"
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access requires an IP whitelist. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Emergency access is BLOCKED if no IPs are configured."
|
||||
|
||||
; ===== Maintenance fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_LABEL="Maintenance"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_DESC="One-time maintenance actions. Set to Yes and save to execute. Resets to No automatically after execution."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL="Development Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC="Disables all Joomla caching at runtime. Useful during development and testing. Does not modify configuration.php."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_RESET_HITS_LABEL="Reset All Hits"
|
||||
PLG_SYSTEM_MOKOWAAS_RESET_HITS_DESC="Set all article hit counters to zero across the site. This action executes on save and resets to No."
|
||||
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_LABEL="Delete All Versions"
|
||||
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_DESC="Purge all content version history from the database. This action executes on save and resets to No."
|
||||
|
||||
; ===== Visual Branding fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_LABEL="Visual Branding"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_DESC="Admin color scheme and CSS injection. Logos and favicon are shipped in the plugin media folder."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_LABEL="Logos & Favicon"
|
||||
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_DESC="Logos and favicon are automatically applied from the plugin media folder (<code>/media/plg_system_mokowaas/</code>). Replace <code>logo.png</code>, <code>favicon.ico</code>, and <code>favicon_256.png</code> to change them."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_LABEL="Primary Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_DESC="Main accent color used in the admin template header and buttons."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_LABEL="Sidebar Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_DESC="Background color for the admin sidebar navigation."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_LABEL="Header Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_DESC="Background color for the admin top header bar."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_LABEL="Link Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_DESC="Color for hyperlinks in the admin interface."
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_LABEL="Brand Icon (FontAwesome)"
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_DESC="FontAwesome unicode codepoint for the brand icon that replaces the Joomla logo icon. Enter the hex code only (e.g. f6d5 for fa-hat-cowboy). Find codes at fontawesome.com/icons."
|
||||
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_LABEL="Custom CSS"
|
||||
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_DESC="Additional CSS injected into admin pages. Use for fine-tuning visual presentation."
|
||||
|
||||
; ===== Tenant Restrictions fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL="Tenant Restrictions"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master users. Master user always has full access."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
|
||||
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
|
||||
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_DESC="Block non-master users from changing Global Configuration. Component config is still accessible."
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_LABEL="Restrict Template Code Editing"
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_DESC="Block non-master users from editing template source code. Template styles remain accessible."
|
||||
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_LABEL="Disable Install from URL"
|
||||
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from URL for ALL users (including master) as a safety measure."
|
||||
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
|
||||
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
|
||||
|
||||
; ===== Content Sync fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The health_api_token from the remote site's MokoWaaS plugin settings."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
|
||||
|
||||
; ===== Diagnostics fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_LABEL="Enable Health Endpoint"
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_DESC="Expose a JSON health check endpoint at <code>/?mokowaas=health</code>. Requires a valid API token. A random token is generated automatically when enabled."
|
||||
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Health API Token"
|
||||
; ===== Diagnostics =====
|
||||
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
|
||||
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer <token></code> header or <code>&token=<value></code> query parameter."
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_LABEL="Grafana URL"
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_DESC="Base URL of your Grafana instance (e.g. <code>https://grafana.example.com</code>). When provided along with an API key, the plugin will auto-provision a datasource and dashboard in Grafana when the health endpoint is enabled."
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_LABEL="Grafana API Key"
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_DESC="Service account token or API key with Editor role in Grafana. Required for auto-provisioning the MokoWaaS datasource and dashboard."
|
||||
|
||||
; ===== Security fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL="Security Hardening"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC="HTTPS enforcement, session timeouts, password policy, and upload restrictions."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
|
||||
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
|
||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
|
||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_NUMBER_LABEL="Require Number"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_SPECIAL_LABEL="Require Special Character"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_LABEL="Allowed Upload Types"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file extensions for media uploads."
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
|
||||
|
||||
; ===== Demo Mode fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
|
||||
|
||||
; ===== Site Aliases fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
; Variables: (none)
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
|
||||
|
||||
@@ -7,180 +7,20 @@
|
||||
; FILE INFORMATION
|
||||
; Defgroup: Joomla Language
|
||||
; Ingroup: MokoWaaS
|
||||
; Version: 02.01.08
|
||||
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
|
||||
; File: plg_system_mokowaas.ini
|
||||
; Path: /src/language/en-GB/plg_system_mokowaas.ini
|
||||
; Brief: English language strings for MokoWaaS system plugin
|
||||
; Notes: Contains translatable strings for plugin functionality
|
||||
; Variables: (none)
|
||||
; Brief: English language strings for MokoWaaS core system plugin
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_LABEL="Enable Branding"
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_BRANDING_DESC="Enable or disable the branding overrides across the system."
|
||||
; ===== Core fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_LABEL="Core"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_CORE_DESC="Heartbeat token for health monitoring and Grafana integration."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_LABEL="Brand Name"
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_NAME_DESC="The brand name that replaces 'Joomla' throughout the interface. Used in all language overrides."
|
||||
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
|
||||
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
|
||||
|
||||
; ===== WaaS Access fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_SUCCESS="Emergency access LOGIN by {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_BLOCKED_IP="Emergency access BLOCKED (unauthorized IP) — {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_WRONG_PASSWORD="Emergency access FAILED (wrong password) — {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_VERIFY_FILE_CREATED="Emergency access verification file created — {username} from {ip}"
|
||||
PLG_SYSTEM_MOKOWAAS_ACTION_EMERGENCY_PENDING_FILE_DELETE="Emergency access pending file deletion — {username} from {ip}"
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access requires an IP whitelist. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Emergency access is BLOCKED if no IPs are configured."
|
||||
|
||||
; ===== Maintenance fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_LABEL="Maintenance"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_MAINTENANCE_DESC="One-time maintenance actions. Set to Yes and save to execute. Resets to No automatically after execution."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_DEV_MODE_LABEL="Development Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_DEV_MODE_DESC="Disables all Joomla caching at runtime. Useful during development and testing. Does not modify configuration.php."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_RESET_HITS_LABEL="Reset All Hits"
|
||||
PLG_SYSTEM_MOKOWAAS_RESET_HITS_DESC="Set all article hit counters to zero across the site. This action executes on save and resets to No."
|
||||
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_LABEL="Delete All Versions"
|
||||
PLG_SYSTEM_MOKOWAAS_DELETE_VERSIONS_DESC="Purge all content version history from the database. This action executes on save and resets to No."
|
||||
|
||||
; ===== Visual Branding fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_LABEL="Visual Branding"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_VISUAL_DESC="Admin color scheme and CSS injection. Logos and favicon are shipped in the plugin media folder."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_LABEL="Logos & Favicon"
|
||||
PLG_SYSTEM_MOKOWAAS_BRANDING_NOTE_DESC="Logos and favicon are automatically applied from the plugin media folder (<code>/media/plg_system_mokowaas/</code>). Replace <code>logo.png</code>, <code>favicon.ico</code>, and <code>favicon_256.png</code> to change them."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_LABEL="Primary Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_PRIMARY_DESC="Main accent color used in the admin template header and buttons."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_LABEL="Sidebar Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_SIDEBAR_DESC="Background color for the admin sidebar navigation."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_LABEL="Header Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_HEADER_DESC="Background color for the admin top header bar."
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_LABEL="Link Color"
|
||||
PLG_SYSTEM_MOKOWAAS_COLOR_LINK_DESC="Color for hyperlinks in the admin interface."
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_LABEL="Brand Icon (FontAwesome)"
|
||||
PLG_SYSTEM_MOKOWAAS_BRAND_ICON_DESC="FontAwesome unicode codepoint for the brand icon that replaces the Joomla logo icon. Enter the hex code only (e.g. f6d5 for fa-hat-cowboy). Find codes at fontawesome.com/icons."
|
||||
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_LABEL="Custom CSS"
|
||||
PLG_SYSTEM_MOKOWAAS_CUSTOM_CSS_DESC="Additional CSS injected into admin pages. Use for fine-tuning visual presentation."
|
||||
|
||||
; ===== Tenant Restrictions fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_LABEL="Tenant Restrictions"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_TENANT_DESC="Restrict admin features for non-master users. Master user always has full access."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_LABEL="Restrict Extension Installer"
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_INSTALLER_DESC="Block non-master users from installing or removing extensions."
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_LABEL="Allow Extension Updates"
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOW_UPDATES_DESC="When the installer is restricted, still allow non-master users to update extensions."
|
||||
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_LABEL="Hide System Information"
|
||||
PLG_SYSTEM_MOKOWAAS_HIDE_SYSINFO_DESC="Block non-master users from viewing PHP, database, and server information."
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_LABEL="Restrict Global Configuration"
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_CONFIG_DESC="Block non-master users from changing Global Configuration. Component config is still accessible."
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_LABEL="Restrict Template Code Editing"
|
||||
PLG_SYSTEM_MOKOWAAS_RESTRICT_TEMPLATE_DESC="Block non-master users from editing template source code. Template styles remain accessible."
|
||||
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_LABEL="Disable Install from URL"
|
||||
PLG_SYSTEM_MOKOWAAS_DISABLE_INSTALL_URL_DESC="Block installing extensions from URL for ALL users (including master) as a safety measure."
|
||||
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_LABEL="Hidden Menu Items"
|
||||
PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC="Components to hide from admin menu for non-master users. One per line (e.g., com_installer)."
|
||||
|
||||
; ===== Content Sync fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_LABEL="Content Sync"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SYNC_DESC="One-way content push to remote MokoWaaS sites. Syncs articles, categories, menus, and modules by alias."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_LABEL="Sync Targets"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGETS_DESC="Remote sites to push content to. Each target requires the site URL and that site's health API token."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_LABEL="Push Content Now"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_PUSH_NOW_DESC="Set to Yes and save to immediately push all content to all configured targets. Resets to No automatically."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_LABEL="Site URL"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_URL_DESC="Full URL of the remote Joomla site (e.g. https://client.example.com)."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_LABEL="API Token"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_TOKEN_DESC="The health_api_token from the remote site's MokoWaaS plugin settings."
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_LABEL="Label"
|
||||
PLG_SYSTEM_MOKOWAAS_SYNC_TARGET_LABEL_DESC="Friendly name for this target (for identification only)."
|
||||
|
||||
; ===== Diagnostics fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_LABEL="Diagnostics & Monitoring"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DIAGNOSTICS_DESC="Health check endpoint for external monitoring systems (e.g. Grafana). Exposes system status via a token-authenticated JSON API."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_LABEL="Enable Health Endpoint"
|
||||
PLG_SYSTEM_MOKOWAAS_ENABLE_HEALTH_DESC="Expose a JSON health check endpoint at <code>/?mokowaas=health</code>. Requires a valid API token. A random token is generated automatically when enabled."
|
||||
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Health API Token"
|
||||
; ===== Diagnostics =====
|
||||
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_LABEL="Heartbeat Token"
|
||||
PLG_SYSTEM_MOKOWAAS_HEALTH_TOKEN_DESC="Auto-generated bearer token for the health endpoint. Use this token in your Grafana datasource configuration. Send as <code>Authorization: Bearer <token></code> header or <code>&token=<value></code> query parameter."
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_LABEL="Grafana URL"
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_URL_DESC="Base URL of your Grafana instance (e.g. <code>https://grafana.example.com</code>). When provided along with an API key, the plugin will auto-provision a datasource and dashboard in Grafana when the health endpoint is enabled."
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_LABEL="Grafana API Key"
|
||||
PLG_SYSTEM_MOKOWAAS_GRAFANA_KEY_DESC="Service account token or API key with Editor role in Grafana. Required for auto-provisioning the MokoWaaS datasource and dashboard."
|
||||
|
||||
; ===== Security fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL="Security Hardening"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC="HTTPS enforcement, session timeouts, password policy, and upload restrictions."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_LABEL="Force HTTPS"
|
||||
PLG_SYSTEM_MOKOWAAS_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Supports reverse proxy setups."
|
||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_LABEL="Admin Session Timeout"
|
||||
PLG_SYSTEM_MOKOWAAS_SESSION_TIMEOUT_DESC="Minutes of idle time before admin sessions expire. 0 uses the Joomla default."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_LABEL="Trusted IPs (No Session Timeout)"
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IPS_DESC="Sessions from these IP addresses or ranges will never time out. Supports exact IPs, CIDR notation (e.g. 10.0.0.0/24), and wildcards (e.g. 192.168.1.*)."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_LABEL="IP / CIDR"
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ADDR_DESC="An IP address, CIDR range, or wildcard pattern."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_LABEL="Label"
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_LABEL_DESC="A descriptive label for this entry (e.g. Office, VPN)."
|
||||
PLG_SYSTEM_MOKOWAAS_TRUSTED_IP_ENABLED_LABEL="Enabled"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_LABEL="Minimum Password Length"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_LENGTH_DESC="Minimum number of characters required for user passwords."
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_UPPER_LABEL="Require Uppercase"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_NUMBER_LABEL="Require Number"
|
||||
PLG_SYSTEM_MOKOWAAS_PASSWORD_SPECIAL_LABEL="Require Special Character"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_LABEL="Allowed Upload Types"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file extensions for media uploads."
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
|
||||
|
||||
; ===== Demo Mode fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_LABEL="Reset Schedule"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_SCHEDULE_DESC="How often the demo site resets. Select a preset or choose Custom to enter a crontab expression."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_LABEL="Custom Crontab"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_CRON_DESC="Crontab expression for the reset schedule. Format: minute hour day month weekday (e.g. 0 */6 * * * for every 6 hours)."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_LABEL="Next Scheduled Reset"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_NEXT_RESET_DESC="Calculated automatically from the reset schedule. The banner countdown uses this timestamp."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Directories"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Select which directories to include in the snapshot. Images contains uploaded media, Media contains extension assets."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
|
||||
|
||||
; ===== Site Aliases fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
; Variables: (none)
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform."
|
||||
PLG_SYSTEM_MOKOWAAS="System - MokoWaaS Core"
|
||||
PLG_SYSTEM_MOKOWAAS_XML_DESCRIPTION="MokoWaaS core system plugin — coordinates feature plugins, master user management, event routing, and admin customizations."
|
||||
|
||||
@@ -10,38 +10,36 @@
|
||||
; Version: 02.01.08
|
||||
; File: en-GB.override.ini
|
||||
; Path: language/overrides/en-GB.override.ini
|
||||
; Brief: Site/frontend override TEMPLATE — placeholders resolved at runtime/install.
|
||||
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
|
||||
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
|
||||
; Brief: Site/frontend language overrides — values are hardcoded.
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
; ===== Footer & template branding =====
|
||||
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
; ===== Generic replacements =====
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
|
||||
LIB_JOOMLA="{{BRAND_NAME}} Library"
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
|
||||
LIB_JOOMLA="MokoWaaS Library"
|
||||
|
||||
; ===== System messages =====
|
||||
JERROR_JOOMLA="{{BRAND_NAME}} Error"
|
||||
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
|
||||
JERROR_JOOMLA="MokoWaaS Error"
|
||||
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
|
||||
|
||||
; ===== Error messages =====
|
||||
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
|
||||
|
||||
; ===== Installer / Sample data =====
|
||||
INSTL_SITE_NAME_LABEL="{{BRAND_NAME}} Site Name"
|
||||
INSTL_SAMPLE_BLOG_SET="{{BRAND_NAME}} Sample Data - Blog"
|
||||
INSTL_SAMPLE_BROCHURE_SET="{{BRAND_NAME}} Sample Data - Brochure Site"
|
||||
INSTL_SAMPLE_DATA_SET="{{BRAND_NAME}} Sample Data - Default"
|
||||
INSTL_SAMPLE_LEARN_SET="{{BRAND_NAME}} Sample Data - Learn"
|
||||
INSTL_SAMPLE_TESTING_SET="{{BRAND_NAME}} Sample Data - Testing"
|
||||
INSTL_SITE_NAME_LABEL="MokoWaaS Site Name"
|
||||
INSTL_SAMPLE_BLOG_SET="MokoWaaS Sample Data - Blog"
|
||||
INSTL_SAMPLE_BROCHURE_SET="MokoWaaS Sample Data - Brochure Site"
|
||||
INSTL_SAMPLE_DATA_SET="MokoWaaS Sample Data - Default"
|
||||
INSTL_SAMPLE_LEARN_SET="MokoWaaS Sample Data - Learn"
|
||||
INSTL_SAMPLE_TESTING_SET="MokoWaaS Sample Data - Testing"
|
||||
|
||||
; ===== Login support =====
|
||||
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
|
||||
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
|
||||
|
||||
; ===== Site offline =====
|
||||
JOFFLINE_MESSAGE="This site is down for maintenance.<br>Please check back again soon."
|
||||
@@ -52,15 +50,15 @@ JERROR_AN_ERROR_HAS_OCCURRED="An error has occurred."
|
||||
JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND="Component not found."
|
||||
|
||||
; ===== Version and About =====
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
; ===== Akeeba Ticket System (ATS) =====
|
||||
COM_ATS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
|
||||
COM_ATS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
|
||||
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
|
||||
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
|
||||
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
@@ -10,38 +10,36 @@
|
||||
; Version: 02.01.08
|
||||
; File: en-US.override.ini
|
||||
; Path: language/overrides/en-US.override.ini
|
||||
; Brief: Site/frontend override TEMPLATE — placeholders resolved at runtime/install.
|
||||
; Notes: Use {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} placeholders.
|
||||
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}}
|
||||
; Brief: Site/frontend language overrides — values are hardcoded.
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
; ===== Footer & template branding =====
|
||||
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
TPL_CASSIOPEIA_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
MOD_FOOTER_LINE2="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
; ===== Generic replacements =====
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="{{BRAND_NAME}} Defaults"
|
||||
LIB_JOOMLA="{{BRAND_NAME}} Library"
|
||||
JGLOBAL_FIELDSET_JOOMLA_DEFAULTS="MokoWaaS Defaults"
|
||||
LIB_JOOMLA="MokoWaaS Library"
|
||||
|
||||
; ===== System messages =====
|
||||
JERROR_JOOMLA="{{BRAND_NAME}} Error"
|
||||
JFIELD_JOOMLA_LABEL="{{BRAND_NAME}} Field"
|
||||
JERROR_JOOMLA="MokoWaaS Error"
|
||||
JFIELD_JOOMLA_LABEL="MokoWaaS Field"
|
||||
|
||||
; ===== Error messages =====
|
||||
JERROR_LAYOUT_ERROR_HAS_OCCURRED="ERROR OCCURRED"
|
||||
|
||||
; ===== Installer / Sample data =====
|
||||
INSTL_SITE_NAME_LABEL="{{BRAND_NAME}} Site Name"
|
||||
INSTL_SAMPLE_BLOG_SET="{{BRAND_NAME}} Sample Data - Blog"
|
||||
INSTL_SAMPLE_BROCHURE_SET="{{BRAND_NAME}} Sample Data - Brochure Site"
|
||||
INSTL_SAMPLE_DATA_SET="{{BRAND_NAME}} Sample Data - Default"
|
||||
INSTL_SAMPLE_LEARN_SET="{{BRAND_NAME}} Sample Data - Learn"
|
||||
INSTL_SAMPLE_TESTING_SET="{{BRAND_NAME}} Sample Data - Testing"
|
||||
INSTL_SITE_NAME_LABEL="MokoWaaS Site Name"
|
||||
INSTL_SAMPLE_BLOG_SET="MokoWaaS Sample Data - Blog"
|
||||
INSTL_SAMPLE_BROCHURE_SET="MokoWaaS Sample Data - Brochure Site"
|
||||
INSTL_SAMPLE_DATA_SET="MokoWaaS Sample Data - Default"
|
||||
INSTL_SAMPLE_LEARN_SET="MokoWaaS Sample Data - Learn"
|
||||
INSTL_SAMPLE_TESTING_SET="MokoWaaS Sample Data - Testing"
|
||||
|
||||
; ===== Login support =====
|
||||
MOD_LOGINSUPPORT_FORUM="{{COMPANY_NAME}} Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="{{BRAND_NAME}} Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="{{COMPANY_NAME}} News"
|
||||
MOD_LOGINSUPPORT_FORUM="Moko Consulting Support"
|
||||
MOD_LOGINSUPPORT_DOCUMENTATION="MokoWaaS Documentation"
|
||||
MOD_LOGINSUPPORT_NEWS="Moko Consulting News"
|
||||
|
||||
; ===== Site offline =====
|
||||
JOFFLINE_MESSAGE="This site is down for maintenance.<br>Please check back again soon."
|
||||
@@ -52,15 +50,15 @@ JERROR_AN_ERROR_HAS_OCCURRED="An error has occurred."
|
||||
JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND="Component not found."
|
||||
|
||||
; ===== Version and About =====
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
JLIB_HTML_POWERED_BY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
; ===== Akeeba Ticket System (ATS) =====
|
||||
COM_ATS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKETS="{{BRAND_NAME}} Tickets"
|
||||
COM_ATS_TITLE_TICKET="{{BRAND_NAME}} Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New {{BRAND_NAME}} Ticket"
|
||||
COM_ATS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKETS="MokoWaaS Tickets"
|
||||
COM_ATS_TITLE_TICKET="MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_NEWTICKET="New MokoWaaS Ticket"
|
||||
COM_ATS_TITLE_CATEGORIES="Ticket Categories"
|
||||
COM_ATS_MSG_TICKET_SAVED="Your {{BRAND_NAME}} ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your {{BRAND_NAME}} ticket has been closed."
|
||||
COM_ATS_MSG_TICKET_SAVED="Your MokoWaaS ticket has been saved."
|
||||
COM_ATS_MSG_TICKET_CLOSED="Your MokoWaaS ticket has been closed."
|
||||
COM_ATS_MSG_REPLY_SAVED="Your reply has been saved."
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='{{SUPPORT_URL}}'>{{BRAND_NAME}}</a>"
|
||||
COM_ATS_LBL_POWEREDBY="Powered by <a href='https://mokoconsulting.tech/support'>MokoWaaS</a>"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 137 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
@@ -16,13 +16,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.32.04
|
||||
VERSION: 02.34.10
|
||||
PATH: /src/mokowaas.xml
|
||||
BRIEF: Plugin manifest for MokoWaaS system plugin
|
||||
NOTE: Defines installation metadata, files, and configuration for Joomla
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoWaaS</name>
|
||||
<name>System - MokoWaaS Core</name>
|
||||
<element>mokowaas</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-05-22</creationDate>
|
||||
@@ -30,8 +30,8 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
|
||||
<version>02.34.10</version>
|
||||
<description>MokoWaaS core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
|
||||
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
<folder>Extension</folder>
|
||||
<folder>Field</folder>
|
||||
<folder>Helper</folder>
|
||||
<folder>Service</folder>
|
||||
<folder>forms</folder>
|
||||
<folder>payload</folder>
|
||||
<folder>services</folder>
|
||||
@@ -48,14 +47,6 @@
|
||||
<folder>administrator</folder>
|
||||
</files>
|
||||
|
||||
<media destination="plg_system_mokowaas" folder="media">
|
||||
<filename>index.html</filename>
|
||||
<filename>favicon.ico</filename>
|
||||
<filename>favicon.svg</filename>
|
||||
<filename>favicon_256.png</filename>
|
||||
<filename>logo.png</filename>
|
||||
</media>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_system_mokowaas.ini</language>
|
||||
<language tag="en-US">en-US/plg_system_mokowaas.ini</language>
|
||||
@@ -89,42 +80,6 @@
|
||||
readonly="true"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset name="demo_mode"
|
||||
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC"
|
||||
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
|
||||
>
|
||||
<field name="demo_scheduled_task" type="DemoTaskInfo"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_TASK_INFO_LABEL"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset name="security"
|
||||
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_SECURITY_DESC"
|
||||
addfieldprefix="Moko\Plugin\System\MokoWaaS\Field"
|
||||
>
|
||||
<field
|
||||
name="emergency_access"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC"
|
||||
default="1"
|
||||
class="btn-group btn-group-yesno"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field
|
||||
name="allowed_ips_display"
|
||||
type="AllowedIps"
|
||||
label=""
|
||||
/>
|
||||
<field
|
||||
name="current_ip_display"
|
||||
type="CurrentIp"
|
||||
label=""
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
* VERSION: 02.33.01
|
||||
* VERSION: 02.34.10
|
||||
* PATH: /src/script.php
|
||||
* BRIEF: Installation script for MokoWaaS plugin
|
||||
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
|
||||
@@ -127,7 +127,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
|
||||
$this->ensureMokoCassiopeia();
|
||||
$this->installLanguageOverrides();
|
||||
$this->updateLoginSupportUrls();
|
||||
$this->updateAtumBranding();
|
||||
$this->registerActionLogExtension();
|
||||
$this->provisionHealthEndpoint();
|
||||
$this->sendInstallNotification($type);
|
||||
@@ -552,7 +551,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
|
||||
$params = $this->getPluginParams();
|
||||
|
||||
return [
|
||||
'{{BRAND_NAME}}' => $params->get('brand_name', 'MokoWaaS'),
|
||||
'{{COMPANY_NAME}}' => $params->get('company_name', 'Moko Consulting'),
|
||||
'{{SUPPORT_URL}}' => $params->get('support_url', 'https://mokoconsulting.tech/support'),
|
||||
];
|
||||
@@ -727,75 +725,6 @@ class plgSystemMokoWaaSInstallerScript implements InstallerScriptInterface
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Atum admin template branding params at install time.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.01.08
|
||||
*/
|
||||
private function updateAtumBranding()
|
||||
{
|
||||
$mediaBase = 'media/plg_system_mokowaas/';
|
||||
|
||||
$expected = [
|
||||
'logoBrandLarge' => $mediaBase . 'logo.png',
|
||||
'logoBrandSmall' => $mediaBase . 'favicon_256.png',
|
||||
'loginLogo' => $mediaBase . 'logo.png',
|
||||
'logoBrandLargeAlt' => '',
|
||||
'logoBrandSmallAlt' => '',
|
||||
'loginLogoAlt' => '',
|
||||
'emptyLogoBrandLargeAlt' => '1',
|
||||
'emptyLogoBrandSmallAlt' => '1',
|
||||
'emptyLoginLogoAlt' => '1',
|
||||
'hue' => 'hsl(219, 44%, 18%)',
|
||||
'special-color' => '#1a2744',
|
||||
'link-color' => '#0051ad',
|
||||
];
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select([$db->quoteName('id'), $db->quoteName('params')])
|
||||
->from($db->quoteName('#__template_styles'))
|
||||
->where($db->quoteName('template') . ' = '
|
||||
. $db->quote('atum'))
|
||||
->where($db->quoteName('client_id') . ' = 1');
|
||||
|
||||
$db->setQuery($query);
|
||||
$styles = $db->loadObjectList();
|
||||
|
||||
if (empty($styles))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($styles as $style)
|
||||
{
|
||||
$params = new \Joomla\Registry\Registry(
|
||||
$style->params ?: '{}'
|
||||
);
|
||||
|
||||
foreach ($expected as $key => $value)
|
||||
{
|
||||
$params->set($key, $value);
|
||||
}
|
||||
|
||||
$update = $db->getQuery(true)
|
||||
->update($db->quoteName('#__template_styles'))
|
||||
->set($db->quoteName('params') . ' = '
|
||||
. $db->quote($params->toString()))
|
||||
->where($db->quoteName('id') . ' = '
|
||||
. (int) $style->id);
|
||||
|
||||
$db->setQuery($update);
|
||||
$db->execute();
|
||||
}
|
||||
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'Updated Atum template branding.', 'message'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the plugin in #__action_logs_extensions so it appears
|
||||
* as a filterable extension in System > Action Logs.
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
* VERSION: 02.33.01
|
||||
* VERSION: 02.34.10
|
||||
* PATH: /src/services/provider.php
|
||||
* BRIEF: Service provider for dependency injection in Joomla 5.x
|
||||
* NOTE: Registers the plugin with Joomla's DI container
|
||||
|
||||
+2
@@ -13,3 +13,5 @@ PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_LABEL="Reset All Hits"
|
||||
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_HITS_DESC="One-shot: reset article hit counters on save. Automatically turns off after execution."
|
||||
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_LABEL="Delete All Versions"
|
||||
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DELETE_VERSIONS_DESC="One-shot: delete all content version history on save. Automatically turns off after execution."
|
||||
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_LABEL="Reset Download Keys"
|
||||
PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_DESC="One-shot: clear all download keys (dlid) from update sites on save. Automatically turns off after execution."
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>PLG_SYSTEM_MOKOWAAS_DEVTOOLS_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoWaaSDevTools</namespace>
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
|
||||
<field name="reset_download_keys" type="radio" default="0"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEVTOOLS_RESET_DLKEYS_DESC"
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
|
||||
@@ -111,6 +111,13 @@ class DevTools extends CMSPlugin implements SubscriberInterface
|
||||
$params->set('delete_versions', 0);
|
||||
}
|
||||
|
||||
// Reset download keys on save if toggled on
|
||||
if ($params->get('reset_download_keys', 0))
|
||||
{
|
||||
$this->resetDownloadKeys();
|
||||
$params->set('reset_download_keys', 0);
|
||||
}
|
||||
|
||||
// Reset the one-shot toggles
|
||||
if ($table->params !== $params->toString())
|
||||
{
|
||||
@@ -152,4 +159,41 @@ class DevTools extends CMSPlugin implements SubscriberInterface
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function resetDownloadKeys(): int
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Find update sites that have a dlid in extra_query
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
|
||||
->from($db->quoteName('#__update_sites'))
|
||||
->where($db->quoteName('extra_query') . ' LIKE ' . $db->quote('%dlid=%'))
|
||||
);
|
||||
|
||||
$sites = $db->loadObjectList();
|
||||
$count = 0;
|
||||
|
||||
foreach ($sites as $site)
|
||||
{
|
||||
// Parse the query string, remove dlid, rebuild
|
||||
parse_str($site->extra_query, $parsed);
|
||||
unset($parsed['dlid']);
|
||||
$newQuery = http_build_query($parsed);
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites'))
|
||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote($newQuery))
|
||||
->where($db->quoteName('update_site_id') . ' = ' . (int) $site->update_site_id)
|
||||
)->execute();
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
$this->getApplication()->enqueueMessage(\sprintf('Cleared download keys from %d update sites.', $count), 'message');
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace>
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
|
||||
{
|
||||
return [
|
||||
'onAfterInitialise' => 'onAfterInitialise',
|
||||
'onAfterRoute' => 'onAfterRoute',
|
||||
'onUserBeforeSave' => 'onUserBeforeSave',
|
||||
];
|
||||
}
|
||||
@@ -792,4 +793,177 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi
|
||||
$config->set('upload_maxsize', $maxMb);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// Extension Protection (#155)
|
||||
// ==================================================================
|
||||
|
||||
/**
|
||||
* Protect MokoWaaS extensions after routing.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.35.00
|
||||
*/
|
||||
public function onAfterRoute(): void
|
||||
{
|
||||
$app = $this->getApplication();
|
||||
|
||||
if (!$app->isClient('administrator'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->protectPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Protect the plugin from being disabled or uninstalled by non-master users.
|
||||
* Does NOT self-heal (no lock) -- master users can still disable if needed.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.03.04
|
||||
*/
|
||||
private function protectPlugin(): void
|
||||
{
|
||||
// Ensure protected flag is set (self-healing -- runs once per session)
|
||||
static $flagChecked = false;
|
||||
|
||||
if (!$flagChecked)
|
||||
{
|
||||
$flagChecked = true;
|
||||
$this->ensureProtectedFlag();
|
||||
}
|
||||
|
||||
if (MokoWaaSHelper::isMasterUser())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$app = $this->getApplication();
|
||||
$option = $app->input->get('option', '');
|
||||
$task = $app->input->get('task', '');
|
||||
|
||||
// Block non-master from uninstalling MokoWaaS
|
||||
if ($option === 'com_installer' && strpos($task, 'manage.remove') !== false)
|
||||
{
|
||||
$cid = $app->input->get('cid', [], 'array');
|
||||
|
||||
if ($this->isOurExtension($cid))
|
||||
{
|
||||
$app->enqueueMessage('MokoWaaS cannot be uninstalled.', 'error');
|
||||
$app->redirect('index.php?option=com_installer&view=manage');
|
||||
}
|
||||
}
|
||||
|
||||
// Block non-master from disabling via list toggle
|
||||
if ($option === 'com_plugins' && strpos($task, 'plugins.publish') !== false)
|
||||
{
|
||||
$cid = $app->input->get('cid', [], 'array');
|
||||
|
||||
if ($this->isOurExtension($cid))
|
||||
{
|
||||
$app->enqueueMessage('MokoWaaS cannot be disabled.', 'error');
|
||||
$app->redirect('index.php?option=com_plugins');
|
||||
}
|
||||
}
|
||||
|
||||
// Block non-master from viewing or editing MokoWaaS plugin settings
|
||||
if ($option === 'com_plugins')
|
||||
{
|
||||
$view = $app->input->get('view', '');
|
||||
$layout = $app->input->get('layout', '');
|
||||
$extensionId = (int) $app->input->get('extension_id', 0);
|
||||
|
||||
if (($view === 'plugin' || $layout === 'edit') && $extensionId > 0)
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('extension_id') . ' = ' . $extensionId)
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'));
|
||||
|
||||
if ((int) $db->setQuery($query)->loadResult() > 0)
|
||||
{
|
||||
$app->enqueueMessage('MokoWaaS settings are restricted to the master user.', 'warning');
|
||||
$app->redirect('index.php?option=com_plugins');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the protected flag is set on MokoWaaS extensions in the DB.
|
||||
*
|
||||
* Sets protected=1, locked=0 so the extension can't be disabled or
|
||||
* uninstalled but can still receive updates and config changes.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.03.10
|
||||
*/
|
||||
private function ensureProtectedFlag(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Set protected=1, locked=0 on MokoWaaS extensions
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('protected') . ' = 1')
|
||||
->set($db->quoteName('locked') . ' = 0')
|
||||
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
|
||||
. ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')')
|
||||
->where($db->quoteName('protected') . ' = 0');
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Ensure update site stays enabled (protected extensions get their update site disabled by Joomla)
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__update_sites') . ' AS us')
|
||||
->join('INNER', $db->quoteName('#__update_sites_extensions') . ' AS use2 ON us.update_site_id = use2.update_site_id')
|
||||
->join('INNER', $db->quoteName('#__extensions') . ' AS e ON use2.extension_id = e.extension_id')
|
||||
->set('us.enabled = 1')
|
||||
->where('us.enabled = 0')
|
||||
->where('(' . $db->quoteName('e.element') . ' = ' . $db->quote('mokowaas')
|
||||
. ' OR ' . $db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Non-critical
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any of the given extension IDs belong to MokoWaaS.
|
||||
*
|
||||
* @param array $ids Extension IDs to check
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 02.03.04
|
||||
*/
|
||||
private function isOurExtension(array $ids): bool
|
||||
{
|
||||
if (empty($ids))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('extension_id') . ' IN (' . implode(',', array_map('intval', $ids)) . ')')
|
||||
->where('(' . $db->quoteName('element') . ' = ' . $db->quote('mokowaas')
|
||||
. ' OR ' . $db->quoteName('element') . ' = ' . $db->quote('pkg_mokowaas') . ')');
|
||||
|
||||
return (int) $db->setQuery($query)->loadResult() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>PLG_SYSTEM_MOKOWAAS_MONITOR_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoWaaSMonitor</namespace>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>PLG_SYSTEM_MOKOWAAS_OFFLINE_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoWaaSOffline</namespace>
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ final class Tos extends CMSPlugin implements SubscriberInterface
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'onAfterRoute' => 'onAfterRoute',
|
||||
'onAfterInitialise' => 'onAfterInitialise',
|
||||
];
|
||||
}
|
||||
|
||||
public function onAfterRoute(): void
|
||||
public function onAfterInitialise(): void
|
||||
{
|
||||
$app = $this->getApplication();
|
||||
|
||||
@@ -61,9 +61,17 @@ final class Tos extends CMSPlugin implements SubscriberInterface
|
||||
$slugs = (array) $slugs;
|
||||
}
|
||||
|
||||
// Default bypassed pages when none configured
|
||||
if (empty($slugs))
|
||||
{
|
||||
return;
|
||||
$slugs = [
|
||||
'legal/terms-of-service',
|
||||
'legal/privacy-policy',
|
||||
'legal/community-guidelines',
|
||||
'support',
|
||||
'support/tickets',
|
||||
'support/submit-a-ticket',
|
||||
];
|
||||
}
|
||||
|
||||
$includeChildren = (int) $this->params->get('include_children', 1);
|
||||
@@ -167,6 +175,5 @@ final class Tos extends CMSPlugin implements SubscriberInterface
|
||||
private function bypassOffline($config, $app): void
|
||||
{
|
||||
$config->set('offline', 0);
|
||||
$app->getInput()->set('tmpl', 'component');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>PLG_SYSTEM_MOKOWAAS_TENANT_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\System\MokoWaaSTenant</namespace>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoWaaSTickets</namespace>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>PLG_TASK_MOKOWAASDEMO_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>PLG_TASK_MOKOWAASSYNC_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoWaaSSync</namespace>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>Joomla Web Services API routes for MokoWaaS site management — health checks, cache, updates, backups, and site info.</description>
|
||||
<namespace path="src">Moko\Plugin\WebServices\MokoWaaS</namespace>
|
||||
<files>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<description>Joomla Web Services API routes for Perfect Publisher (com_autotweet) — channels, posts, requests, rules, and feeds.</description>
|
||||
<namespace path="src">Moko\Plugin\WebServices\PerfectPublisher</namespace>
|
||||
<files>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
|
||||
* VERSION: 02.33.01
|
||||
* VERSION: 02.34.10
|
||||
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
|
||||
*/
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
|
||||
* VERSION: 02.33.01
|
||||
* VERSION: 02.34.10
|
||||
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
|
||||
*/
|
||||
|
||||
|
||||
Submodule src/packages/tpl_mokoonyx deleted from f3897495ad
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoWaaS</name>
|
||||
<packagename>mokowaas</packagename>
|
||||
<version>02.33.01-dev</version>
|
||||
<version>02.34.10</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -10,6 +10,8 @@
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<description>MokoWaaS site management suite — admin dashboard, security firewall, tenant restrictions, health monitoring, developer tools, and REST API.</description>
|
||||
<dlid prefix="dlid=" suffix=""/>
|
||||
<blockChildUninstall>true</blockChildUninstall>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
|
||||
<files folder="packages">
|
||||
|
||||
+209
-5
@@ -89,6 +89,9 @@ class Pkg_MokowaasInstallerScript
|
||||
// Set menu_icon params on submenu items (Joomla only renders img on level 1)
|
||||
$this->fixMenuIcons();
|
||||
|
||||
// Set up MokoWaaS guided tours and unpublish Joomla defaults
|
||||
$this->setupGuidedTours();
|
||||
|
||||
// Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level)
|
||||
$this->protectExtensions();
|
||||
|
||||
@@ -98,6 +101,9 @@ class Pkg_MokowaasInstallerScript
|
||||
// Clean up stale/duplicate update sites
|
||||
$this->cleanupStaleUpdateSites();
|
||||
|
||||
// Fix orphaned update records (extension_id=0)
|
||||
$this->fixUpdateRecords();
|
||||
|
||||
// Trigger heartbeat registration
|
||||
$this->sendHeartbeat();
|
||||
|
||||
@@ -552,6 +558,29 @@ class Pkg_MokowaasInstallerScript
|
||||
*
|
||||
* @since 02.31.00
|
||||
*/
|
||||
private function fixUpdateRecords(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Link orphaned #__updates records to the installed extension
|
||||
$db->setQuery(
|
||||
"UPDATE " . $db->quoteName('#__updates') . " u"
|
||||
. " JOIN " . $db->quoteName('#__extensions') . " e"
|
||||
. " ON u.element = e.element AND u.type = e.type"
|
||||
. " SET u.extension_id = e.extension_id"
|
||||
. " WHERE u.extension_id = 0"
|
||||
. " AND u.element LIKE " . $db->quote('%mokowaas%')
|
||||
);
|
||||
$db->execute();
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Non-critical
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupStaleUpdateSites(): void
|
||||
{
|
||||
try
|
||||
@@ -951,8 +980,8 @@ class Pkg_MokowaasInstallerScript
|
||||
$iconMap = [
|
||||
'class:cogs' => 'icon-cogs',
|
||||
'class:puzzle-piece' => 'icon-puzzle-piece',
|
||||
'class:headphones' => 'icon-headphones',
|
||||
'class:file-code' => 'icon-file-code',
|
||||
'class:headphones' => 'fa-solid fa-handshake-angle',
|
||||
'class:file-code' => 'fa-solid fa-file-code',
|
||||
'class:lock' => 'icon-lock',
|
||||
'class:shield-alt' => 'icon-shield-alt',
|
||||
'class:database' => 'icon-database',
|
||||
@@ -963,10 +992,15 @@ class Pkg_MokowaasInstallerScript
|
||||
'class:bolt' => 'icon-bolt',
|
||||
];
|
||||
|
||||
// Find all MokoWaaS component submenu items (including those linking to other components)
|
||||
$db->setQuery(
|
||||
"SELECT id, img, params FROM #__menu"
|
||||
. " WHERE client_id = 1 AND level >= 2"
|
||||
. " AND link LIKE '%com_mokowaas%'"
|
||||
$db->getQuery(true)
|
||||
->select(['m.id', 'm.img', 'm.params'])
|
||||
->from($db->quoteName('#__menu', 'm'))
|
||||
->where('m.client_id = 1')
|
||||
->where('m.level >= 2')
|
||||
->where('m.parent_id IN (SELECT id FROM ' . $db->quoteName('#__menu')
|
||||
. ' WHERE client_id = 1 AND level = 1 AND link LIKE ' . $db->quote('%com_mokowaas%') . ')')
|
||||
);
|
||||
|
||||
foreach ($db->loadObjectList() as $item)
|
||||
@@ -1001,6 +1035,176 @@ class Pkg_MokowaasInstallerScript
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpublish default Joomla guided tours and create MokoWaaS tours.
|
||||
* Re-enables the guided tours plugin if disabled.
|
||||
*/
|
||||
private function setupGuidedTours(): void
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Re-enable guided tours plugin (may have been disabled)
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('guidedtours'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
)->execute();
|
||||
|
||||
// Re-enable the guided tours module (shows our tours, not Joomla's)
|
||||
$db->setQuery(
|
||||
"UPDATE " . $db->quoteName('#__modules')
|
||||
. " SET published = 1, title = 'MokoWaaS Tours'"
|
||||
. " WHERE module = 'mod_guidedtours'"
|
||||
);
|
||||
$db->execute();
|
||||
|
||||
// Override the guided tours module language string
|
||||
$overridePath = JPATH_ADMINISTRATOR . '/language/overrides/en-GB.override.ini';
|
||||
$overrides = file_exists($overridePath) ? parse_ini_file($overridePath) : [];
|
||||
|
||||
if (empty($overrides['MOD_GUIDEDTOURS']))
|
||||
{
|
||||
$overrides['MOD_GUIDEDTOURS'] = 'MokoWaaS Tours';
|
||||
$overrides['MOD_GUIDEDTOURS_TITLE'] = 'MokoWaaS Tours';
|
||||
|
||||
$lines = [];
|
||||
foreach ($overrides as $k => $v)
|
||||
{
|
||||
$lines[] = $k . '="' . str_replace('"', '\"', $v) . '"';
|
||||
}
|
||||
file_put_contents($overridePath, implode("\n", $lines) . "\n");
|
||||
}
|
||||
|
||||
// Unpublish all default Joomla tours
|
||||
$db->setQuery(
|
||||
"UPDATE " . $db->quoteName('#__guidedtours')
|
||||
. " SET published = 0"
|
||||
. " WHERE " . $db->quoteName('uid') . " LIKE 'joomla-%'"
|
||||
);
|
||||
$db->execute();
|
||||
|
||||
// Define MokoWaaS tours
|
||||
$tours = [
|
||||
[
|
||||
'uid' => 'mokowaas-welcome',
|
||||
'title' => 'Welcome to MokoWaaS',
|
||||
'desc' => 'Get started with the MokoWaaS Admin Tools Suite. This tour shows you the key areas of your admin dashboard.',
|
||||
'url' => 'administrator/index.php?option=com_mokowaas',
|
||||
'steps' => [
|
||||
['title' => 'MokoWaaS Dashboard', 'desc' => 'This is your MokoWaaS control center. You can see site info, feature plugins, WAF activity, and quick actions all in one place.', 'target' => '#mokowaas-dashboard', 'type' => 0],
|
||||
['title' => 'Site Information', 'desc' => 'The info bar shows your Joomla version, PHP version, database type, and debug/offline status at a glance.', 'target' => '.mokowaas-info-bar', 'type' => 0],
|
||||
['title' => 'Quick Actions', 'desc' => 'Use these buttons to clear cache, check updates, manage extensions, and perform common admin tasks with one click.', 'target' => '#mokowaas-btn-cache', 'type' => 0],
|
||||
['title' => 'Feature Plugins', 'desc' => 'MokoWaaS features are split into toggleable plugins. Enable or disable security, tenant restrictions, developer tools, and more from here.', 'target' => '.mokowaas-plugin-grid', 'type' => 0],
|
||||
['title' => 'MokoWaaS Menu', 'desc' => 'The MokoWaaS sidebar menu gives you quick access to all admin tools — Helpdesk, Extensions, WAF Log, Database Tools, and more.', 'target' => '.mokowaas-admin-menu, [class*="mokowaas"]', 'type' => 0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'uid' => 'mokowaas-firewall',
|
||||
'title' => 'MokoWaaS Firewall Setup',
|
||||
'desc' => 'Configure the Web Application Firewall to protect your site from common attacks.',
|
||||
'url' => 'administrator/index.php?option=com_plugins&task=plugin.edit&filter[search]=mokowaas_firewall',
|
||||
'steps' => [
|
||||
['title' => 'Firewall Plugin', 'desc' => 'The MokoWaaS Firewall provides 10 security shields including SQL injection, XSS, and malicious user agent detection.', 'target' => '', 'type' => 0],
|
||||
['title' => 'WAF Shields', 'desc' => 'Enable or disable individual WAF shields. Each shield protects against a specific attack vector. All shields are enabled by default.', 'target' => '', 'type' => 0],
|
||||
['title' => 'Security Headers', 'desc' => 'Configure HTTP security headers like X-Frame-Options, Content-Security-Policy, and HSTS to harden your site against browser-based attacks.', 'target' => '', 'type' => 0],
|
||||
['title' => 'IP Blocklist', 'desc' => 'Block specific IP addresses, CIDR ranges, or wildcard patterns. The auto-ban feature automatically blocks IPs that trigger too many WAF alerts.', 'target' => '', 'type' => 0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'uid' => 'mokowaas-helpdesk',
|
||||
'title' => 'MokoWaaS Helpdesk',
|
||||
'desc' => 'Learn how to manage support tickets, categories, and automation rules.',
|
||||
'url' => 'administrator/index.php?option=com_mokowaas&view=tickets',
|
||||
'steps' => [
|
||||
['title' => 'Ticket List', 'desc' => 'View all support tickets with status, priority, SLA tracking, and assignment. Filter by status or search to find specific tickets.', 'target' => '', 'type' => 0],
|
||||
['title' => 'Create a Ticket', 'desc' => 'Click the New button to create a support ticket. Assign a category, priority, and optional SLA deadline.', 'target' => '', 'type' => 0],
|
||||
['title' => 'Ticket Automation', 'desc' => 'Set up automation rules that trigger on ticket events (new ticket, status change) or Joomla events (user login, registration). Automate assignment, notifications, and status changes.', 'target' => '', 'type' => 0],
|
||||
],
|
||||
],
|
||||
[
|
||||
'uid' => 'mokowaas-extensions',
|
||||
'title' => 'Moko Extensions Manager',
|
||||
'desc' => 'Browse and install Moko Consulting extensions from the built-in catalog.',
|
||||
'url' => 'administrator/index.php?option=com_mokowaas&view=extensions',
|
||||
'steps' => [
|
||||
['title' => 'Extension Catalog', 'desc' => 'Browse all available Moko Consulting extensions. Each card shows the extension name, description, install status, and current version.', 'target' => '', 'type' => 0],
|
||||
['title' => 'Install Extensions', 'desc' => 'Click Install to add an extension from the Moko Consulting repository. Updates are handled through Joomla\'s standard update system.', 'target' => '', 'type' => 0],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($tours as $tourDef)
|
||||
{
|
||||
// Check if tour already exists
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__guidedtours'))
|
||||
->where($db->quoteName('uid') . ' = ' . $db->quote($tourDef['uid']))
|
||||
);
|
||||
|
||||
if ($db->loadResult())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$tour = (object) [
|
||||
'title' => $tourDef['title'],
|
||||
'uid' => $tourDef['uid'],
|
||||
'description' => $tourDef['desc'],
|
||||
'extensions' => '',
|
||||
'url' => $tourDef['url'],
|
||||
'created' => date('Y-m-d H:i:s'),
|
||||
'created_by' => 0,
|
||||
'modified' => date('Y-m-d H:i:s'),
|
||||
'modified_by' => 0,
|
||||
'published' => 1,
|
||||
'language' => '*',
|
||||
'note' => 'MokoWaaS',
|
||||
'access' => 3,
|
||||
'ordering' => 0,
|
||||
'autostart' => 0,
|
||||
];
|
||||
|
||||
$db->insertObject('#__guidedtours', $tour, 'id');
|
||||
$tourId = (int) $tour->id;
|
||||
|
||||
foreach ($tourDef['steps'] as $i => $stepDef)
|
||||
{
|
||||
$step = (object) [
|
||||
'tour_id' => $tourId,
|
||||
'title' => $stepDef['title'],
|
||||
'description' => $stepDef['desc'],
|
||||
'target' => $stepDef['target'],
|
||||
'type' => $stepDef['type'],
|
||||
'interactive_type' => 1,
|
||||
'url' => '',
|
||||
'position' => 'bottom',
|
||||
'ordering' => $i + 1,
|
||||
'published' => 1,
|
||||
'created' => date('Y-m-d H:i:s'),
|
||||
'created_by' => 0,
|
||||
'modified' => date('Y-m-d H:i:s'),
|
||||
'modified_by' => 0,
|
||||
'language' => '*',
|
||||
'note' => '',
|
||||
'params' => '{}',
|
||||
];
|
||||
|
||||
$db->insertObject('#__guidedtour_steps', $step, 'id');
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add('Guided tours setup error: ' . $e->getMessage(), Log::WARNING, 'mokowaas');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a "Support" menu item on the frontend main menu.
|
||||
*/
|
||||
|
||||
+4
-5
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
VERSION: 02.33.01-dev
|
||||
VERSION: 02.34.10-dev
|
||||
-->
|
||||
|
||||
<updates>
|
||||
@@ -11,13 +11,12 @@
|
||||
<element>pkg_mokowaas</element>
|
||||
<type>package</type>
|
||||
<client>site</client>
|
||||
<version>02.33.01-dev</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<version>02.34.10-dev</version>
|
||||
<creationDate>2026-06-06</creationDate>
|
||||
<infourl title='Package - MokoWaaS'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development</infourl>
|
||||
<downloads>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.33.01-dev.zip</downloadurl>
|
||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/pkg_mokowaas-02.34.10-dev.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>be79af0f02a31ec83f15f3319af00cbf084590e72ef2cf13693a23028206539a</sha256>
|
||||
<tags><tag>dev</tag></tags>
|
||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
|
||||
Reference in New Issue
Block a user