2 Commits

Author SHA1 Message Date
Jonathan Miller 5caf97afdc fix(manifest): add Gitea update server URL to updateservers block
Points to updates.xml on the main branch of the Gitea repository
so Joomla can check for new versions automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 08:53:01 -05:00
Jonathan Miller 08b8dc3b06 fix(plugin): harden installation script and resolve production issues
Fixes hardcoded category/component IDs, missing language key,
unqualified exception catches, and removes legacy plugin directory.

- Query Uncategorised category ID dynamically instead of assuming ID 2
- Remove fragile component_id fallback (fail gracefully with log)
- Use installing admin's user ID for article ownership
- Add missing PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS key
- Qualify all Exception catches with backslash for namespace safety
- Standardize help text URL across all locale files
- Remove obsolete src/plugins/ legacy directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 08:53:00 -05:00
36 changed files with 4287 additions and 2736 deletions
+20
View File
@@ -0,0 +1,20 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: MokoStandards.Templates.Config
# INGROUP: MokoStandards.Templates
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/configs/moko-standards.yml
# VERSION: 04.01.00
# BRIEF: Governance attachment template — synced to .moko-standards in every governed repository
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoJoomTOS, waas-component, 04.00.04
#
# This file is managed automatically by MokoStandards bulk sync.
# Do not edit manually — changes will be overwritten on the next sync.
# To update governance settings, open a PR in MokoStandards instead:
# https://github.com/mokoconsulting-tech/MokoStandards
standards_source: "https://github.com/mokoconsulting-tech/MokoStandards"
standards_version: "04.00.04"
platform: "waas-component"
governed_repo: "mokoconsulting-tech/MokoJoomTOS"
@@ -1,77 +0,0 @@
---
name: WaaS Client Site Issue
about: Report an issue with a WaaS client site (branding, deployment, media sync)
title: '[WAAS] '
labels: 'waas, client-site'
assignees: ''
---
## Site Issue Type
- [ ] Branding / CSS not applying
- [ ] Deployment failure
- [ ] Media sync issue
- [ ] Template override not working
- [ ] Module positioning issue
- [ ] Mobile / responsive layout
- [ ] Performance issue
## Client Site
- **Client Org**: [e.g., ClarksvilleFurs]
- **Repo**: [e.g., client-waas-clarksvillefurs]
- **Environment**: [Dev / Production]
- **Site URL**: [dev or production URL — omit if private]
## Issue Description
Describe the issue clearly.
## Steps to Reproduce
1. Visit [page URL]
2. Look at [element]
3. See error
## Expected Behavior
What the site should look like or how it should behave.
## Actual Behavior
What is happening instead.
## Screenshots
Attach screenshots showing the issue (desktop and mobile if relevant).
## Deployment Status
- **Last deploy**: [date or "unknown"]
- **Deploy workflow**: [succeeded / failed / not run]
- **Branch**: [dev / main]
## Media Sync
- [ ] Images missing after sync
- [ ] Sync direction: [dev-to-prod / prod-to-dev / bidirectional]
- [ ] Last sync: [date]
## Template Details
- **Joomla Version**: [e.g., 5.x]
- **Template Name**: [e.g., clienttemplate]
- **MokoWaaS Plugin**: [Active / Inactive]
- **MokoOnyx Admin**: [Active / Inactive]
## CSS Custom Properties
If branding issue, list the relevant CSS variables:
```css
:root {
--client-primary: #...;
--client-secondary: #...;
}
```
## Browser / Device
- **Browser**: [e.g., Chrome 120, Safari 17]
- **Device**: [Desktop / Tablet / Mobile]
- **Screen Width**: [e.g., 1920px, 768px, 375px]
## Checklist
- [ ] I have cleared Joomla cache
- [ ] I have hard-refreshed the browser (Ctrl+Shift+R)
- [ ] I have checked the deploy workflow completed
- [ ] I have verified the change is on the correct branch
- [ ] No credentials or PII are included in this issue
+6 -6
View File
@@ -1,25 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Moko Platform Repository Manifest
MokoStandards Repository Manifest
Auto-generated by cleanup script.
See: https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home
-->
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity>
<name>MokoJoomTOS</name>
<display-name>Plugin - MokoJoomTOS</display-name>
<org>MokoConsulting</org>
<description>Joomla system plugin to keep legal pages accessible during offline mode</description>
<version>04.02.01</version>
<description>A component to present a sites Term of Service and privacy policy even through offline.</description>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
<platform>joomla</platform>
<standards-version>05.00.00</standards-version>
<standards-version>04.07.00</standards-version>
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/moko-platform</standards-source>
<last-synced>2026-05-10T19:51:05+00:00</last-synced>
</governance>
<build>
<language>PHP</language>
<package-type>joomla-extension</package-type>
<package-type>joomla</package-type>
<entry-point>src/</entry-point>
</build>
</moko-platform>
File diff suppressed because it is too large Load Diff
+213
View File
@@ -0,0 +1,213 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main
#
# +========================================================================+
# | CASCADE MAIN → ALL BRANCHES |
# +========================================================================+
# | |
# | Triggers on every push to main (PR merges, bot commits, etc.) |
# | |
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
# | 2. For each: create PR (main → branch), auto-merge if clean |
# | 3. On conflict: leave PR open for manual resolution |
# | |
# +========================================================================+
name: "Universal: Cascade Main → Dev"
on:
push:
branches:
- main
workflow_dispatch:
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
pull-requests: write
jobs:
cascade:
name: Cascade main → branches
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip cascade]')
steps:
- name: Discover target branches
id: branches
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Fetch all branches (paginated)
PAGE=1
ALL_BRANCHES=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
PAGE=$((PAGE + 1))
done
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
TARGETS=""
for BRANCH in $ALL_BRANCHES; do
case "$BRANCH" in
dev|dev/*|rc/*|beta/*|alpha/*)
TARGETS="$TARGETS $BRANCH"
;;
esac
done
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
if [ -z "$TARGETS" ]; then
echo "targets=" >> "$GITHUB_OUTPUT"
echo "️ No cascade target branches found"
else
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$TARGETS" | wc -w)
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
fi
- name: Cascade to all target branches
if: steps.branches.outputs.targets != ''
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHORT_SHA="${GITHUB_SHA:0:7}"
TARGETS="${{ steps.branches.outputs.targets }}"
SUCCESS=0
CONFLICTS=0
SKIPPED=0
FAILED=0
for BRANCH in $TARGETS; do
echo ""
echo "═══ main → ${BRANCH} ═══"
# Check if branch is already up to date
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
RESPONSE=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/compare/${ENCODED_BRANCH}...main")
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
if [ "$AHEAD" -eq 0 ]; then
echo " ✅ Already up to date"
SKIPPED=$((SKIPPED + 1))
continue
fi
echo " ️ main is ${AHEAD} commit(s) ahead"
# Check for existing cascade PR
EXISTING=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
PR_NUMBER=""
if [ "$EXISTING_COUNT" -gt 0 ]; then
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
echo " ️ Reusing existing PR #${PR_NUMBER}"
else
# Create cascade PR
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
\"head\": \"main\",
\"base\": \"${BRANCH}\"
}" \
"${API}/pulls")
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
BODY=$(echo "$PR_RESPONSE" | sed '$d')
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
FAILED=$((FAILED + 1))
continue
fi
echo " ✅ Created PR #${PR_NUMBER}"
fi
# Try auto-merge
PR_DATA=$(curl -sS \
-H "Authorization: token ${GA_TOKEN}" \
"${API}/pulls/${PR_NUMBER}")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
if [ "$MERGEABLE" != "true" ]; then
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
continue
fi
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"Do\": \"merge\",
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
\"delete_branch_after_merge\": false
}" \
"${API}/pulls/${PR_NUMBER}/merge")
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
echo " ✅ Merged — ${BRANCH} is in sync"
SUCCESS=$((SUCCESS + 1))
else
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
fi
done
# Summary
echo ""
echo "════════════════════════════════════════"
echo " ✅ Merged: ${SUCCESS}"
echo " ⚠️ Conflicts: ${CONFLICTS}"
echo " ⏭️ Up to date: ${SKIPPED}"
echo " ❌ Failed: ${FAILED}"
echo "════════════════════════════════════════"
if [ "$FAILED" -gt 0 ]; then
exit 1
fi
+6 -23
View File
@@ -43,9 +43,9 @@ jobs:
- name: Clone MokoStandards
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
@@ -53,7 +53,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -346,7 +346,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install \
@@ -391,7 +391,7 @@ jobs:
- name: Install dependencies
env:
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
if [ -f "composer.json" ]; then
composer install --no-interaction --prefer-dist --optimize-autoloader
@@ -448,20 +448,3 @@ jobs:
echo '```' >> $GITHUB_STEP_SUMMARY
fi
exit $EXIT
pre-release:
name: Build RC Pre-Release
runs-on: ubuntu-latest
needs: [lint-and-validate, test]
if: github.event_name == 'pull_request'
steps:
- name: Trigger pre-release build
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
run: |
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
+9 -9
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Maintenance
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/cleanup.yml
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
+126
View File
@@ -0,0 +1,126 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+2 -2
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/gitleaks.yml.template
# VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
+3 -2
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Notifications
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/notify.yml
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
@@ -18,6 +18,7 @@ on:
- "Joomla Build & Release"
- "Joomla Extension CI"
- "Deploy"
- "Cascade Main → Dev"
types:
- completed
+196 -277
View File
@@ -1,277 +1,196 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- 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 in source files"
echo "## Conflict Markers Found" >> $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: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ──────────────────────────────────────────────────────
report-issues:
name: Report Issues
runs-on: ubuntu-latest
needs: [branch-policy, validate]
if: >-
always() &&
needs.validate.result == 'failure'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: "File issue for PR validation failure"
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
./automation/ci-issue-reporter.sh \
--gate "PR Validation" \
--workflow "PR Check" \
--severity error \
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
+384
View File
@@ -0,0 +1,384 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability }})"
runs-on: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GA_TOKEN }}
- name: Setup PHP
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1
fi
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
- name: Resolve metadata
id: meta
run: |
STABILITY="${{ inputs.stability }}"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Read and bump patch version (with rollover)
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
[ -z "$CURRENT" ] && CURRENT="00.00.00"
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
# Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major
NEW_PATCH=$((10#$PATCH + 1))
NEW_MINOR=$((10#$MINOR))
NEW_MAJOR=$((10#$MAJOR))
if [ $NEW_PATCH -gt 99 ]; then
NEW_PATCH=0
NEW_MINOR=$((NEW_MINOR + 1))
fi
if [ $NEW_MINOR -gt 99 ]; then
NEW_MINOR=0
NEW_MAJOR=$((NEW_MAJOR + 1))
fi
VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH)
TODAY=$(date +%Y-%m-%d)
echo "Bumping: ${CURRENT} → ${VERSION} (patch)"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
# Update platform-specific manifest
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE"
fi
;;
*) ;;
esac
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element (platform-aware)
case "$PLATFORM" in
joomla)
MANIFEST="${{ steps.platform.outputs.manifest }}"
EXT_ELEMENT=""
if [ -n "$MANIFEST" ]; then
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
;;
dolibarr)
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
if [ -n "$MOD_FILE" ]; then
MOD_BASENAME=$(basename "$MOD_FILE" .class.php)
EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]')
else
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
fi
;;
*)
EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
;;
esac
ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Build package
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::error::No src/ or htdocs/ directory"
exit 1
fi
mkdir -p build/package
rsync -a \
--exclude='sftp-config*' \
--exclude='.ftpignore' \
--exclude='*.ppk' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='.env*' \
--exclude='*.local' \
--exclude='.build-trigger' \
"${SOURCE_DIR}/" build/package/
- name: Create ZIP
id: zip
run: |
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
cd build/package
zip -r "../${ZIP_NAME}" .
cd ..
SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1)
echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT"
echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)"
- name: Create or replace Gitea release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
BRANCH=$(git branch --show-current)
BODY="## ${VERSION} ($(date +%Y-%m-%d))
**Channel:** ${STABILITY}
**SHA-256:** \`${SHA256}\`"
# Delete existing release
EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null)
if [ -n "$EXISTING_ID" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/releases/${EXISTING_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API}/tags/${TAG}" 2>/dev/null || true
fi
# Create release
RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API}/releases" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg target "$BRANCH" \
--arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \
--arg body "$BODY" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}'
)" | jq -r '.id')
echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"
# Upload ZIP
curl -sS -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
--data-binary "@build/${ZIP_NAME}"
echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})"
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag }}"
DATE=$(date +%Y-%m-%d)
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
python3 << 'PYEOF'
import re, os
stability = os.environ["PY_STABILITY"]
version = os.environ["PY_VERSION"]
sha256 = os.environ["PY_SHA256"]
zip_name = os.environ["PY_ZIP_NAME"]
tag = os.environ["PY_TAG"]
date = os.environ["PY_DATE"]
gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"]
download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
with open("updates.xml", "r") as f:
content = f.read()
# Map stability to XML tag name
tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"}
xml_tag = tag_map.get(stability, stability)
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
updated = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", updated)
if "<sha256>" in updated:
updated = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", updated)
else:
updated = updated.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
updated = re.sub(r"(<downloadurl[^>]*>)[^<]*(</downloadurl>)", rf"\g<1>{download_url}\g<2>", updated)
content = content.replace(block, updated)
print(f"Updated {xml_tag} channel: version={version}")
else:
print(f"WARNING: No <tag>{xml_tag}</tag> block in updates.xml")
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit and push to current branch
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]"
# Sync updates.xml to main and dev (whichever isn't current)
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}" -- . 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
STABILITY="${{ steps.meta.outputs.stability }}"
# Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing
case "$STABILITY" in
release-candidate) TAGS_TO_DELETE="beta alpha development" ;;
beta) TAGS_TO_DELETE="alpha development" ;;
alpha) TAGS_TO_DELETE="development" ;;
*) TAGS_TO_DELETE="" ;;
esac
[ -z "$TAGS_TO_DELETE" ] && exit 0
for TAG in $TAGS_TO_DELETE; do
RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true
curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/tags/${TAG}" 2>/dev/null || true
echo "Deleted: ${TAG} (id: ${RELEASE_ID})"
fi
done
File diff suppressed because it is too large Load Diff
+2 -18
View File
@@ -4,8 +4,8 @@
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# INGROUP: MokoStandards.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.gitea/workflows/security-audit.yml
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
@@ -80,19 +80,3 @@ jobs:
-H "Priority: high" \
-d "Security audit found vulnerabilities. Review dependency updates." \
"${NTFY_URL}/${NTFY_TOPIC}" || true
- name: Joomla version audit
if: always()
run: |
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
echo "$JOOMLA_SITES" > /tmp/sites.json
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
rm -f /tmp/sites.json
else
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
fi
env:
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
+464
View File
@@ -0,0 +1,464 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
#
# Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev or dev/**
#
# Joomla filters by user's "Minimum Stability" setting.
name: "Joomla: Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update updates.xml
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup MokoStandards tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_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
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Generate updates.xml entry
id: update
run: |
BRANCH="${{ github.ref_name }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on all branches (dev, alpha, beta, rc)
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
git push 2>/dev/null || true
fi
# Determine stability from branch or input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then
STABILITY="development"
else
STABILITY="stable"
fi
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
# Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "No Joomla manifest found — skipping"
exit 0
fi
# Extract fields using sed (works on all runners)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
EXT_TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_ELEMENT=$(sed -n 's/.*<element>\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=$(sed -n 's/.*<extension[^>]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_FOLDER=$(sed -n 's/.*<extension[^>]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_VERSION=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
TARGET_PLATFORM=$(sed -n 's/.*\(<targetplatform[^/]*\/>\).*/\1/p' "$MANIFEST" | head -1)
PHP_MINIMUM=$(sed -n 's/.*<php_minimum>\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1)
# Fallbacks
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest: try XML filename, then repo name
if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]')
case "$EXT_ELEMENT" in
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi
# Use manifest version if README version is empty
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/")
CLIENT_TAG=""
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
[ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="<client>site</client>"
FOLDER_TAG=""
[ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
PHP_TAG=""
[ -n "$PHP_MINIMUM" ] && PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
# Version suffix for non-stable
DISPLAY_VERSION="$VERSION"
case "$STABILITY" in
development) DISPLAY_VERSION="${VERSION}-dev" ;;
alpha) DISPLAY_VERSION="${VERSION}-alpha" ;;
beta) DISPLAY_VERSION="${VERSION}-beta" ;;
rc) DISPLAY_VERSION="${VERSION}-rc" ;;
esac
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
# Each stability level has its own release tag
case "$STABILITY" in
development) RELEASE_TAG="development" ;;
alpha) RELEASE_TAG="alpha" ;;
beta) RELEASE_TAG="beta" ;;
rc) RELEASE_TAG="release-candidate" ;;
*) RELEASE_TAG="v${MAJOR}" ;;
esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}"
# -- Build install packages (ZIP + tar.gz) --------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then
EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*"
TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES
cd ..
tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \
--exclude='.ftpignore' --exclude='sftp-config*' \
--exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' .
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# Ensure release exists on Gitea
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
# Create release
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_BASE}/releases" \
-d "$(python3 -c "import json; print(json.dumps({
'tag_name': '${RELEASE_TAG}',
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
'body': '${STABILITY} release',
'prerelease': True,
'target_commitish': 'main'
}))")" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
fi
if [ -n "$RELEASE_ID" ]; then
# Delete existing assets with same name before uploading
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
ASSET_ID=$(echo "$ASSETS" | python3 -c "
import sys,json
assets = json.load(sys.stdin)
for a in assets:
if a['name'] == '${ASSET_FILE}':
print(a['id']); break
" 2>/dev/null || true)
if [ -n "$ASSET_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
fi
done
# Upload both formats
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${PACKAGE_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${TAR_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
fi
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
else
SHA256=""
fi
# -- Build the new entry (canonical format matching release.yml) --
NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
NEW_ENTRY="${NEW_ENTRY} <description>${EXT_NAME} ${STABILITY} build.</description>\n"
NEW_ENTRY="${NEW_ENTRY} <element>${EXT_ELEMENT}</element>\n"
NEW_ENTRY="${NEW_ENTRY} <type>${EXT_TYPE}</type>\n"
[ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n"
[ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <version>${VERSION}</version>\n"
NEW_ENTRY="${NEW_ENTRY} <creationDate>$(date +%Y-%m-%d)</creationDate>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title='${EXT_NAME}'>https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type='full' format='zip'>${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} <tags><tag>${STABILITY}</tag></tags>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} <targetplatform name='joomla' version='(5|6).*'/>\n"
[ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} <php_minimum>${PHP_MINIMUM}</php_minimum>\n"
NEW_ENTRY="${NEW_ENTRY} </update>"
# -- Write new entry to temp file --------------------------------
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# -- Merge into updates.xml ----------------------------------------
# Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
TARGETS=""
for entry in $CASCADE_MAP; do
key="${entry%%:*}"
vals="${entry#*:}"
if [ "$key" = "${STABILITY}" ]; then
TARGETS="$vals"
break
fi
done
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
echo "Cascade: ${STABILITY} → ${TARGETS}"
# Create updates.xml if missing
if [ ! -f "updates.xml" ]; then
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting -->" >> updates.xml
printf '%s\n' "<updates>" >> updates.xml
printf '%s\n' "</updates>" >> updates.xml
fi
# Update existing blocks or create missing ones
export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)"
python3 << 'PYEOF'
import re, os
targets = os.environ["PY_TARGETS"].split(",")
version = os.environ["PY_VERSION"]
date = os.environ["PY_DATE"]
with open("updates.xml") as f:
content = f.read()
with open("/tmp/new_entry.xml") as f:
new_entry_template = f.read()
for tag in targets:
tag = tag.strip()
# Build entry with this tag's name
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
# Try to find existing block (handles both single-line and multi-line <tags>)
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL)
if match:
# Update in place — replace entire block
content = content.replace(match.group(1), new_entry.strip())
print(f" UPDATED: <tag>{tag}</tag> → {version}")
else:
# Create — insert before </updates>
content = content.replace("</updates>", "\n" + new_entry.strip() + "\n\n</updates>")
print(f" CREATED: <tag>{tag}</tag> → {version}")
# Clean up excessive blank lines
content = re.sub(r"\n{3,}", "\n\n", content)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
# Commit
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
# -- Sync updates.xml to main (for non-main branches) ----------------------
- name: Sync updates.xml to main
if: github.ref_name != 'main'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GA_TOKEN="${{ secrets.GA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/contents/updates.xml" \
-d "$(python3 -c "import json; print(json.dumps({
'content': '${CONTENT}',
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
'branch': 'main'
}))")" > /dev/null 2>&1 \
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
else
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# -- Permission check: admin or maintain role required --------
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY
+162 -4
View File
@@ -1,11 +1,169 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
(./LICENSE).
# FILE INFORMATION
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
VERSION: 03.08.04
PATH: ./CHANGELOG.md
BRIEF: Version history and release notes
-->
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Planned
- GitHub Actions CI/CD workflows
- Pre-commit hooks for code quality
- Docker development environment
### Fixed
- Legacy `mokojoomtos.php` now uses `onAfterRoute` event instead of `onAfterInitialise`
- Legacy plugin entry point now sets `tmpl=component` for component-only view during offline mode
- Installation script language keys corrected to match actual INI file definitions
- Installation script `$extension` property corrected from `plg_system_mokojoomtos_offline` to `plg_system_mokojoomtos`
- README.md event reference corrected from `onAfterInitialise` to `onAfterRoute`
### Changed
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
- Template source paths updated: `templates/gitea/` to `templates/mokogitea/`
- HCL definition files removed -- Template repos are now the canonical source
- Updated all markdown FILE INFORMATION headers to reference MokoJoomTOS repository
- Updated CONTRIBUTING.md title, clone URLs, and links to reference MokoJoomTOS
- Updated CHANGELOG.md links to reference MokoJoomTOS repository
- Updated CLAUDE.md and copilot-instructions.md for accuracy
## [03.08.04] - 2026-02-28
### Changed
- Updated version number to 03.08.04 across all files
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
### Fixed
- Template chrome (header, footer, modules) no longer loads when accessing TOS page in offline mode
- Component-only view now properly applied when site is offline
## [1.0.0] - 2026-01-16
### Added
- `branch-cleanup.yml`: auto-delete merged feature branches after PR merge
- Initial template repository structure
- Comprehensive Makefile with 30+ build targets
- Complete README.md with usage documentation
- CONTRIBUTING.md with contribution guidelines
- SECURITY.md with security policy and best practices
- CHANGELOG.md for version tracking
- LICENSE file (GPL-3.0-or-later)
- EditorConfig for consistent code formatting
- Documentation structure in `docs/` directory
- Placeholders for component directories (`admin/`, `site/`, `media/`)
- MokoStandards compliance:
- File header standards with copyright and SPDX identifiers
- Joomla coding standards configuration
- PHPStan static analysis setup
- Dependency security auditing
- Makefile targets for:
- Dependency management (install-deps, update-deps)
- Code validation (lint, phpcs, phpstan)
- Testing (test, test-coverage)
- Building (build, build-assets)
- Development (dev-install, watch-assets)
- Release management (release, bump-version)
- Utility commands (clean, version, help)
- Documentation index files with MokoStandards metadata
### Documentation
- Comprehensive README with:
- Quick start guide
- Prerequisites and installation
- Usage examples
- Project structure overview
- Makefile command reference
- Standards compliance information
- Detailed CONTRIBUTING guide with:
- Commit message conventions
- DCO sign-off requirements
- Development setup instructions
- Code review process
- Security policy with:
- Vulnerability reporting procedures
- Security best practices
- Code examples for secure development
- Template structure for Joomla 4.x and 5.x compatibility
## Version Guidelines
### Version Numbering
This template follows [Semantic Versioning](https://semver.org/):
- **MAJOR** version for incompatible changes (e.g., 2.0.0)
- **MINOR** version for new features (e.g., 1.1.0)
- **PATCH** version for bug fixes (e.g., 1.0.1)
### Change Categories
- **Added**: New features
- **Changed**: Changes to existing functionality
- **Deprecated**: Soon-to-be removed features
- **Removed**: Removed features
- **Fixed**: Bug fixes
- **Security**: Security fixes
## Release Notes
### [1.0.0] Release Notes
This is the initial release of the MokoJoomTOS plugin. It provides a production-ready Joomla system plugin for offline TOS access with:
- **Complete build system** via comprehensive Makefile
- **MokoStandards compliance** out of the box
- **Developer-friendly** workflow with automation
- **Security-focused** with built-in best practices
- **Well-documented** with clear usage instructions
This template is ready for use in creating new Joomla components that follow organizational coding standards and best practices.
### Migration Guide
**For New Projects**: Simply use this template to create your new component repository.
**For Existing Projects**: Review the Makefile, documentation structure, and standards compliance files to gradually adopt features that benefit your project.
## Links
- [Repository](https://github.com/mokoconsulting-tech/MokoJoomTOS)
- [Issues](https://github.com/mokoconsulting-tech/MokoJoomTOS/issues)
- [Pull Requests](https://github.com/mokoconsulting-tech/MokoJoomTOS/pulls)
- [MokoStandards](https://github.com/mokoconsulting-tech/MokoCodingDefaults)
[Unreleased]: https://github.com/mokoconsulting-tech/MokoJoomTOS/compare/v03.08.04...HEAD
[03.08.04]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v03.08.04
[1.0.0]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v1.0.0
+6 -6
View File
@@ -6,7 +6,7 @@ MokoJoomTOS is a lightweight Joomla 4.x/5.x system plugin that allows Terms of S
```
/
├── .mokogitea/ # GitHub workflows, issue templates, copilot-instructions.md
├── .github/ # GitHub workflows, issue templates, copilot-instructions.md
├── docs/ # Detailed documentation (currently minimal with index.md)
├── scripts/ # Build and utility scripts (validate/, package scripts)
├── src/ # Plugin source code at root level (NOT nested under plugins/)
@@ -75,7 +75,7 @@ XML files use the MokoStandard header format:
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
PATH: src/mokojoomtos.xml
VERSION: 04.00.00
VERSION: 03.08.04
BRIEF: [Brief description of file purpose]
=========================================================================
-->
@@ -88,7 +88,7 @@ Markdown files use an HTML comment format with the same structure.
- **DEFGROUP**: Top-level group (always `MokoJoomTOS` for this repo)
- **INGROUP**: Subgroup/component (always `plg_system_mokojoomtos`)
- **PATH**: Relative path from repository root (e.g., `src/mokojoomtos.xml`)
- **VERSION**: Current plugin version (currently `04.00.00`)
- **VERSION**: Current plugin version (currently `03.08.04`)
- **BRIEF**: One-line description of file's purpose
### Exempt Files
@@ -327,7 +327,7 @@ No automated test infrastructure exists in this repository. Manual testing requi
```bash
# Manual packaging (build scripts being migrated to scripts/ directory)
cd src/
zip -r ../plg_system_mokojoomtos-04.00.00.zip .
zip -r ../plg_system_mokojoomtos-03.08.04.zip .
```
Package should contain: `mokojoomtos.php`, `mokojoomtos.xml`, `script.php`, `src/`, `language/`, `administrator/`
@@ -397,7 +397,7 @@ Before opening a pull request, ensure:
- [ ] CHANGELOG.md updated with changes under correct version
- [ ] README.md updated if user-facing changes
- [ ] All markdown file headers include VERSION: 04.00.00
- [ ] All markdown file headers include VERSION: 03.08.04
- [ ] XML file headers include complete FILE INFORMATION block
## Version Management
@@ -469,6 +469,6 @@ This repository follows minimal documentation structure with essential docs in r
2. **SECURITY.md** - Security policy and vulnerability reporting procedures
3. **CODE_OF_CONDUCT.md** - Community standards and behavior expectations
4. **CHANGELOG.md** - Version history following Keep a Changelog format
5. **.mokogitea/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns)
5. **.github/copilot-instructions.md** - Comprehensive guidance for GitHub Copilot (includes all Joomla patterns)
Currently no `docs/policy/` directory exists - all policy is in root-level markdown files.
+108 -141
View File
@@ -1,161 +1,128 @@
# Contributing to Moko Consulting Projects
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
This file is part of a Moko Consulting project.
## Branching Workflow
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License (./LICENSE).
# FILE INFORMATION
DEFGROUP: {{DEFGROUP}}
INGROUP: Project.Documentation
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
VERSION: 04.04.00
PATH: ./CONTRIBUTING.md
BRIEF: How to contribute; branch strategy, commit conventions, PR workflow, and release pipeline
-->
# Contributing
Thank you for your interest in contributing to **MokoJoomTOS**!
This repository is governed by **[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)** — the authoritative source of coding standards, workflows, and policies for all Moko Consulting repositories.
## Branch Strategy
| Branch | Purpose | Deploys To |
|--------|---------|------------|
| `main` | Bleeding edge — all development merges here | CI only |
| `dev/XX.YY.ZZ` | Feature development | Dev server (version: "development") |
| `version/XX.YY` | Stable frozen snapshot | Demo + RS servers |
### Development Workflow
```
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
1. Create branch: git checkout -b dev/XX.YY.ZZ/my-feature
2. Develop + test (dev server auto-deploys on push)
3. Open PR → main (squash merge only)
4. Auto-release (version branch + tag + GitHub Release created automatically)
```
### Step by step
### Branch Naming
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
| Prefix | Use |
|--------|-----|
| `dev/XX.YY.ZZ` | Feature development (e.g., `dev/02.00.00/add-extrafields`) |
| `version/XX.YY` | Stable release (auto-created, never manually pushed) |
| `chore/` | Automated sync branches (managed by MokoStandards) |
2. **Work and commit** on your feature branch. Push to origin.
> **Never use** `feature/`, `hotfix/`, or `release/` prefixes — they are not part of the MokoStandards branch strategy.
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
## Commit Conventions
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
Use [conventional commits](https://www.conventionalcommits.org/):
```
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
feat(scope): add new extrafield for invoice tracking
fix(sql): correct column type in llx_mytable
docs(readme): update installation instructions
chore(deps): bump enterprise library to 04.02.30
```
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
**Valid types:** `feat` | `fix` | `docs` | `chore` | `ci` | `refactor` | `style` | `test` | `perf` | `revert` | `build`
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
## Pull Request Workflow
## Reporting Issues
1. **Branch** from `main` using `dev/XX.YY.ZZ/description` format
2. **Bump** the patch version in `README.md` before opening the PR
3. **Title** must be a valid conventional commit subject line
4. **Target** `main` — squash merge only (merge commits are disabled)
5. **CI checks** must pass before merge
Use the repository's issue tracker with the appropriate template.
### What Happens on Merge
When your PR is merged to `main`, these workflows run automatically:
1. **sync-version-on-merge** — auto-bumps patch version, propagates to all file headers
2. **auto-release** — creates `version/XX.YY` branch, git tag, and GitHub Release
3. **deploy-demo / deploy-rs** — deploys to demo and RS servers (if `src/**` changed)
## Coding Standards
All contributions must follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards):
| Standard | Reference |
|----------|-----------|
| Coding Style | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) |
| File Headers | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) |
| Branching | [branch-release-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branch-release-strategy.md) |
| Merge Strategy | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) |
| Scripting | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) |
| Build & Release | [build-release.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/build-release.md) |
## PR Checklist
- [ ] Branch named `dev/XX.YY.ZZ/description`
- [ ] Patch version bumped in `README.md`
- [ ] Conventional commit format for PR title
- [ ] All new files have FILE INFORMATION headers
- [ ] `declare(strict_types=1)` in all PHP files
- [ ] PHPDoc on all public methods
- [ ] Tests pass
- [ ] CHANGELOG.md updated
- [ ] No secrets, tokens, or credentials committed
## Custom Workflows
Place repo-specific workflows in `.github/workflows/custom/` — they are **never overwritten or deleted** by MokoStandards sync:
```
.github/workflows/
├── deploy-dev.yml ← Synced from MokoStandards
├── auto-release.yml ← Synced from MokoStandards
└── custom/ ← Your custom workflows (safe)
└── my-custom-ci.yml
```
## License
By contributing, you agree that your contributions will be licensed under the [GPL-3.0-or-later](LICENSE) license.
---
*Moko Consulting <hello@mokoconsulting.tech>*
*This file is synced from [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Do not edit directly — changes will be overwritten on the next sync.*
+2 -2
View File
@@ -2,14 +2,14 @@
A Joomla system plugin that keeps your Terms of Service, Privacy Policy, or any legal page accessible to visitors -- even when the site is in offline (maintenance) mode.
![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue?style=flat-square&logo=joomla&logoColor=white) ![PHP](https://img.shields.io/badge/PHP-%E2%89%A58.1-777BB4?style=flat-square&logo=php&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Version](https://img.shields.io/badge/version-04.02.00-orange?style=flat-square) ![Type](https://img.shields.io/badge/type-system%20plugin-blueviolet?style=flat-square)
![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-blue?style=flat-square&logo=joomla&logoColor=white) ![PHP](https://img.shields.io/badge/PHP-%E2%89%A58.1-777BB4?style=flat-square&logo=php&logoColor=white) ![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Version](https://img.shields.io/badge/version-03.08.04-orange?style=flat-square) ![Type](https://img.shields.io/badge/type-system%20plugin-blueviolet?style=flat-square)
| Field | Value |
|---|---|
| **Author** | [Moko Consulting](https://mokoconsulting.tech) |
| **License** | GPL-3.0-or-later |
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS) |
| **Version** | 04.02.00 |
| **Version** | 03.08.04 |
---
-237
View File
@@ -1,237 +0,0 @@
#!/usr/bin/env bash
# ============================================================================
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Automation.CI
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
# ============================================================================
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
GATE=""
DETAILS=""
SEVERITY="error"
WORKFLOW=""
# ── Parse arguments ─────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
Required:
--gate CI gate name (e.g. "Code Quality", "Self-Health")
--details Human-readable failure description
Optional:
--severity "error" (default) or "warning"
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gate) GATE="$2"; shift 2 ;;
--details) DETAILS="$2"; shift 2 ;;
--severity) SEVERITY="$2"; shift 2 ;;
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
else
TITLE="[CI] ${GATE} failed"
fi
# ── Ensure label exists ─────────────────────────────────────────────────────
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
> /dev/null 2>&1 || true
fi
fi
}
# ── Search for existing open issue ──────────────────────────────────────────
find_existing_issue() {
# URL-encode the gate name for the query
local query
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
# Extract the first matching issue number
echo "$response" \
| grep -oP '"number":\s*\K[0-9]+' \
| head -1
}
# ── Build issue body ────────────────────────────────────────────────────────
build_body() {
local severity_badge
if [[ "$SEVERITY" == "error" ]]; then
severity_badge="**Severity:** Error"
else
severity_badge="**Severity:** Warning"
fi
cat <<BODY
## CI Gate Failure: ${GATE}
${severity_badge}
**Workflow:** ${WORKFLOW:-unknown}
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
### Details
${DETAILS}
### Resolution
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
# ── Build comment body (for existing issues) ────────────────────────────────
build_comment() {
cat <<COMMENT
### CI failure recurrence
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
${DETAILS}
COMMENT
}
# ── Main ────────────────────────────────────────────────────────────────────
ensure_label
EXISTING=$(find_existing_issue)
if [[ -n "$EXISTING" ]]; then
# Append comment to existing issue
COMMENT_BODY=$(build_comment)
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
if [[ "$HTTP" == "201" ]]; then
echo "Commented on existing issue #${EXISTING}"
else
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
fi
else
# Create new issue
ISSUE_BODY=$(build_body)
ISSUE_JSON=$(python3 -c "
import sys, json
body = sys.stdin.read()
print(json.dumps({
'title': sys.argv[1],
'body': body,
'labels': []
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
> /dev/null 2>&1 || true
fi
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
else
echo "WARNING: Failed to create issue"
echo "Response: ${RESPONSE}"
fi
fi
View File
+16
View File
@@ -0,0 +1,16 @@
# Docs Index: /templates/repos/joomla/component/docs
## Purpose
This index provides navigation to documentation within this folder.
## Metadata
- **Document Type:** index
- **Auto-generated:** This file is automatically generated by rebuild_indexes.py
## Revision History
| Change | Notes | Author |
| --- | --- | --- |
| Automated update | Generated by documentation index automation | rebuild_indexes.py |
+119
View File
@@ -0,0 +1,119 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: MokoJoomTOS.Documentation
INGROUP: MokoStandards.Templates
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
PATH: /docs/update-server.md
VERSION: 04.04.00
BRIEF: How this extension's Joomla update server file (update.xml) is managed
-->
# Joomla Update Server
[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.04.00-blue)](https://github.com/mokoconsulting-tech/MokoStandards)
This document explains how `update.xml` is automatically managed for this Joomla extension following the [Joomla Update Server specification](https://docs.joomla.org/Deploying_an_Update_Server).
## How It Works
Joomla checks for extension updates by fetching an XML file from the URL defined in the `<updateservers>` tag in the extension's XML manifest. MokoStandards generates this file automatically.
### Automatic Generation
| Event | Workflow | `<tag>` | `<version>` |
|-------|----------|---------|-------------|
| Merge to `main` | `auto-release.yml` | `stable` | `XX.YY.ZZ` |
| Push to `dev/**` | `deploy-dev.yml` | `development` | `development` |
| Push to `rc/**` | `deploy-dev.yml` | `rc` | `XX.YY.ZZ-rc` |
### Generated XML Structure
```xml
<?xml version="1.0" encoding="utf-8"?>
<updates>
<update>
<name>Extension Name</name>
<description>Extension Name update</description>
<element>com_extensionname</element>
<type>component</type>
<version>01.02.03</version>
<client>site</client>
<folder>system</folder> <!-- plugins only -->
<tags>
<tag>stable</tag>
</tags>
<infourl title="Extension Name">https://github.com/.../releases/tag/v01.02.03</infourl>
<downloads>
<downloadurl type="full" format="zip">https://github.com/.../releases/download/v01.02.03/com_ext-01.02.03.zip</downloadurl>
</downloads>
<targetplatform name="joomla" version="((5\.[0-9])|(6\.[0-9]))" />
<php_minimum>8.2</php_minimum> <!-- if present in manifest -->
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
</updates>
```
### Metadata Source
All metadata is extracted from the extension's XML manifest (`src/*.xml`) at build time:
| XML Element | Source | Notes |
|-------------|--------|-------|
| `<name>` | `<name>` in manifest | Extension display name |
| `<element>` | `<element>` in manifest | Must match installed extension identifier |
| `<type>` | `type` attribute on `<extension>` | `component`, `module`, `plugin`, `library`, `package`, `template` |
| `<client>` | `client` attribute on `<extension>` | `site` or `administrator`**required for plugins and modules** |
| `<folder>` | `group` attribute on `<extension>` | Plugin group (e.g., `system`, `content`) — **required for plugins** |
| `<targetplatform>` | `<targetplatform>` in manifest | Falls back to Joomla 5.x / 6.x if not specified |
| `<php_minimum>` | `<php_minimum>` in manifest | Included only if present |
### Extension Manifest Setup
Your XML manifest must include an `<updateservers>` tag pointing to the `update.xml` on the `main` branch:
```xml
<extension type="component" client="site" method="upgrade">
<name>My Extension</name>
<element>com_myextension</element>
<!-- ... -->
<updateservers>
<server type="extension" name="My Extension Updates">
https://raw.githubusercontent.com/mokoconsulting-tech/MokoJoomTOS/main/update.xml
</server>
</updateservers>
</extension>
```
### Branch Lifecycle
```
dev/XX.YY.ZZ → rc/XX.YY.ZZ → main → version/XX.YY
(development) (rc) (stable) (frozen snapshot)
```
1. **Development** (`dev/**`): `update.xml` with `<tag>development</tag>`, download points to branch archive
2. **Release Candidate** (`rc/**`): `update.xml` with `<tag>rc</tag>`, version set to `XX.YY.ZZ-rc`
3. **Stable Release** (merge to `main`): `update.xml` with `<tag>stable</tag>`, download points to GitHub Release asset
4. **Frozen Snapshot** (`version/XX.YY`): immutable, never force-pushed
### Health Checks
The `repo_health.yml` workflow verifies on every commit:
- `update.xml` exists in the repository root
- XML manifest exists with `<extension>` tag
- `<version>`, `<name>`, `<author>`, `<namespace>` tags present
- Extension `type` attribute is valid
- Language `.ini` files exist
- `index.html` directory listing protection in `src/`, `src/admin/`, `src/site/`
---
*Managed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). See [docs/workflows/update-server.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/update-server.md) for the full specification.*
@@ -3,23 +3,17 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create a Joomla article for your Terms of Service.<br/><strong>Step 2:</strong> Create a menu item pointing to that article.<br/><strong>Step 3:</strong> Set the menu item alias/slug (e.g., 'terms-of-service').<br/><strong>Step 4:</strong> Enter that same slug above.<br/><strong>Step 5:</strong> When your site goes offline, visitors can still access yoursite.com/terms-of-service"
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
@@ -3,5 +3,5 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when site is offline"
@@ -3,23 +3,17 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create a Joomla article for your Terms of Service.<br/><strong>Step 2:</strong> Create a menu item pointing to that article.<br/><strong>Step 3:</strong> Set the menu item alias/slug (e.g., 'terms-of-service').<br/><strong>Step 4:</strong> Enter that same slug above.<br/><strong>Step 5:</strong> When your site goes offline, visitors can still access yoursite.com/terms-of-service"
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
@@ -3,5 +3,5 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when site is offline"
+4 -10
View File
@@ -3,23 +3,17 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create a Joomla article for your Terms of Service.<br/><strong>Step 2:</strong> Create a menu item pointing to that article.<br/><strong>Step 3:</strong> Set the menu item alias/slug (e.g., 'terms-of-service').<br/><strong>Step 4:</strong> Enter that same slug above.<br/><strong>Step 5:</strong> When your site goes offline, visitors can still access yoursite.com/terms-of-service"
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
+4 -10
View File
@@ -3,23 +3,17 @@
; License GNU General Public License version 3 or later; see LICENSE
; Note: All ini files need to be saved as UTF-8
PLG_SYSTEM_MOKOJOOMTOS="System - Moko Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS="System - Offline Terms of Service"
PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION="Allows Terms of Service to be accessible via menu slug when the site is in offline mode. Simply configure the menu slug (e.g., 'terms-of-service') and that page will remain accessible even when the site is offline."
; Configuration
PLG_SYSTEM_MOKOJOOMTOS_FIELDSET_BASIC="Basic Settings"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Offline-Accessible Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Select one or more menu items that should remain accessible when the site is in offline mode. Hold Ctrl/Cmd to select multiple items."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL="Include Child Menu Items"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC="When enabled, child menu items under the selected items will also be accessible during offline mode. For example, selecting 'legal' will also allow access to 'legal/terms-of-service' and 'legal/privacy-policy'."
; Warnings
PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING="⚠ SEF URLs are disabled — path matching requires SEF. Itemid fallback is active."
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL="Terms of Service Menu Slug"
PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC="Enter the menu slug for your Terms of Service page (e.g., 'terms-of-service'). This page will be accessible even when the site is offline. The slug must match the menu item alias exactly."
; Help
PLG_SYSTEM_MOKOJOOMTOS_HELP_LABEL="How to Use This Plugin"
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create articles for your legal pages (Terms of Service, Privacy Policy, etc.).<br/><strong>Step 2:</strong> Create menu items pointing to those articles.<br/><strong>Step 3:</strong> Select the menu items above (hold Ctrl/Cmd to select multiple).<br/><strong>Step 4:</strong> When your site goes offline, visitors can still access the selected pages.<br/><br/><em>Tip:</em> The dropdown shows the full URL path for each menu item (e.g., /legal/terms-of-service)."
PLG_SYSTEM_MOKOJOOMTOS_HELP_DESC="<strong>Step 1:</strong> Create a Joomla article for your Terms of Service.<br/><strong>Step 2:</strong> Create a menu item pointing to that article.<br/><strong>Step 3:</strong> Set the menu item alias/slug (e.g., 'terms-of-service').<br/><strong>Step 4:</strong> Enter that same slug above.<br/><strong>Step 5:</strong> When your site goes offline, visitors can still access yoursite.com/terms-of-service"
; Errors
PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS="Error loading menu items: %s"
+85 -4
View File
@@ -8,17 +8,98 @@
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
/**
* MokoJoomTOS Legacy Entry Point
* MokoJoomTOS Offline Mode Bypass Plugin
*
* This file is required by Joomla's plugin loader (<filename plugin="mokojoomtos">)
* but is NOT executed under Joomla 5's DI container. The actual plugin logic lives
* in src/Extension/MokoJoomTOS.php, bootstrapped via services/provider.php.
* Allows Terms of Service menu to be accessible via slug when the site
* is in offline mode.
*
* @since 1.0.0
*/
class PlgSystemMokojoomtos extends CMSPlugin
{
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* After route event handler
*
* Checks if the current request is for the Terms of Service slug and if
* the site is in offline mode. If both conditions are met, temporarily
* disables offline mode and sets component-only view for this request.
*
* This event fires after routing but before template selection, making it
* the correct place to set tmpl=component to prevent template chrome loading.
*
* @return void
*
* @since 03.08.04
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->app->isClient('site'))
{
return;
}
// Get the global configuration
$config = $this->app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the configured Terms of Service slug
$tosSlug = trim($this->params->get('tos_slug', 'terms-of-service'));
if (empty($tosSlug))
{
return;
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
// Remove the base path if present
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Check if the path matches the Terms of Service slug
if ($path === $tosSlug || strpos($path, $tosSlug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
$input = $this->app->input;
$input->set('tmpl', 'component');
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
}
}
}
+8 -20
View File
@@ -25,20 +25,20 @@
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
PATH: src/mokojoomtos.xml
VERSION: 04.02.01
VERSION: 03.08.04
BRIEF: Plugin manifest XML file for MokoJoomTOS system plugin
=========================================================================
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - Moko Terms of Service</name>
<name>plg_system_mokojoomtos</name>
<author>Moko Consulting</author>
<creationDate>2026-05-16</creationDate>
<creationDate>2026-01-01</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GNU General Public License version 3 or later; see LICENSE</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>04.02.01</version>
<description>Allows Terms of Service to be accessible via menu slug when site is offline</description>
<version>03.08.04</version>
<description>PLG_SYSTEM_MOKOJOOMTOS_XML_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\MokoJoomTOS</namespace>
@@ -47,7 +47,6 @@
<files>
<filename plugin="mokojoomtos">mokojoomtos.php</filename>
<folder>src</folder>
<folder>services</folder>
<folder>language</folder>
<folder>administrator</folder>
</files>
@@ -69,20 +68,9 @@
type="menuslug"
label="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_LABEL"
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_TOS_SLUG_DESC"
multiple="true"
default="terms-of-service"
required="true"
/>
<field
name="include_children"
type="radio"
label="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_LABEL"
description="PLG_SYSTEM_MOKOJOOMTOS_FIELD_INCLUDE_CHILDREN_DESC"
default="1"
class="btn-group btn-group-yesno"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="help_spacer"
@@ -96,6 +84,6 @@
</config>
<updateservers>
<server type="extension" name="MokoJoomTOS Plugin">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/updates.xml</server>
<server type="extension" name="MokoJoomTOS Plugin">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/raw/branch/main/updates.xml</server>
</updateservers>
</extension>
+364 -440
View File
@@ -13,470 +13,394 @@ use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScript;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Table\Table;
/**
* Installation script for MokoJoomTOS Plugin
* Installation script for MokoJoomTOS Offline Plugin
*
* @since 1.0.0
*/
class PlgSystemMokojoomtosInstallerScript extends InstallerScript
class PlgSystemMokojoomtosOfflineInstallerScript extends InstallerScript
{
/**
* Minimum Joomla version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumJoomla = '4.0.0';
/**
* Minimum Joomla version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumJoomla = '4.0.0';
/**
* Minimum PHP version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumPhp = '7.4.0';
/**
* Minimum PHP version required to install the plugin
*
* @var string
* @since 1.0.0
*/
protected $minimumPhp = '7.4.0';
/**
* Extension type (used by parent class)
*
* @var string
* @since 1.0.0
*/
protected $extension = 'plg_system_mokojoomtos';
/**
* Extension type (used by parent class)
*
* @var string
* @since 1.0.0
*/
protected $extension = 'plg_system_mokojoomtos';
/**
* Function called before plugin installation/update/uninstall
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return boolean True on success
*
* @since 1.0.0
*/
public function preflight($type, $parent)
{
if (!parent::preflight($type, $parent)) {
return false;
}
/**
* Function called before plugin installation/update/uninstall
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return boolean True on success
*
* @since 1.0.0
*/
public function preflight($type, $parent)
{
// Check minimum requirements
if (!parent::preflight($type, $parent)) {
return false;
}
return true;
}
return true;
}
/**
* Function called after plugin installation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function install($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin installation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function install($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_INSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin update
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function update($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
}
/**
* Function called after plugin update
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function update($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UPDATE_SUCCESS') . '</p>';
}
/**
* Function called after plugin uninstallation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function uninstall($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
}
/**
* Function called after plugin uninstallation
*
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function uninstall($parent)
{
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_UNINSTALL_SUCCESS') . '</p>';
}
/**
* Function called after extension installation/update/discover_install
*
* Fixes #89: enablePlugin() is now called unconditionally for both
* install and upgrade paths.
* Fixes #92: enablePlugin() is called on upgrade to re-enable if disabled.
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function postflight($type, $parent)
{
// Always enable the plugin on install or upgrade
$this->enablePlugin();
/**
* Function called after extension installation/update/discover_install
*
* @param string $type Installation type (install, update, discover_install)
* @param InstallerAdapter $parent Parent installer adapter
*
* @return void
*
* @since 1.0.0
*/
public function postflight($type, $parent)
{
if ($type === 'install' || $type === 'discover_install') {
// Create Terms of Service article and menu item
$this->createTermsOfServiceSetup();
echo '<div class="alert alert-success">';
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
echo '</div>';
}
}
if ($type === 'install' || $type === 'discover_install') {
$this->createTermsOfServiceSetup();
$this->setDefaultSlugs();
/**
* Create Terms of Service article and menu item
*
* @return void
*
* @since 1.0.0
*/
private function createTermsOfServiceSetup()
{
try {
$db = Factory::getDbo();
// Check if Terms of Service article already exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'));
$db->setQuery($query);
$articleId = $db->loadResult();
// Create article if it doesn't exist
if (!$articleId) {
$articleId = $this->createTermsArticle();
}
if ($articleId) {
// Check if menu item already exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('published') . ' >= 0');
$db->setQuery($query);
$menuId = $db->loadResult();
// Create menu item if it doesn't exist
if (!$menuId) {
$this->createTermsMenuItem($articleId);
}
}
} catch (\Exception $e) {
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
echo '<div class="alert alert-success">';
echo '<h4>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_TITLE') . '</h4>';
echo '<p>' . Text::_('PLG_SYSTEM_MOKOJOOMTOS_POSTINSTALL_DESC') . '</p>';
echo '</div>';
}
}
/**
* Create Terms of Service article
*
* @return int|null Article ID or null on failure
*
* @since 1.0.0
*/
private function createTermsArticle()
{
try {
$db = Factory::getDbo();
/**
* Create Terms of Service article and menu item
*
* @return void
*
* @since 1.0.0
*/
private function createTermsOfServiceSetup()
{
try {
$db = Factory::getDbo();
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_content/tables');
$table = Table::getInstance('Content', 'Joomla\\Component\\Content\\Administrator\\Table\\');
// Check if Terms of Service article already exists (by alias, any category)
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('state') . ' >= 0');
$db->setQuery($query);
$articleId = $db->loadResult();
if (!$table) {
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
return null;
}
if (!$articleId) {
$articleId = $this->createTermsArticle();
}
// Get Uncategorised category ID dynamically
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
->where($db->quoteName('published') . ' = 1');
$db->setQuery($query);
$catId = (int) $db->loadResult();
if ($articleId) {
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote('terms-of-service'))
->where($db->quoteName('published') . ' >= 0');
$db->setQuery($query);
$menuId = $db->loadResult();
if (!$catId) {
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
return null;
}
if (!$menuId) {
$this->createTermsMenuItem($articleId);
}
}
} catch (\Throwable $e) {
Log::add('Error creating Terms of Service setup: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
// Get current user ID for article ownership
$createdBy = Factory::getApplication()->getIdentity()->id ?: 0;
/**
* Create Terms of Service article using Joomla 5 MVCFactory
*
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
* the removed Table::addIncludePath() / Table::getInstance().
* Fixes #94: Includes params, metadata, and attribs defaults.
*
* @return int|null Article ID or null on failure
*
* @since 1.0.0
*/
private function createTermsArticle()
{
try {
$db = Factory::getDbo();
$app = Factory::getApplication();
$data = [
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
'fulltext' => '',
'state' => 1,
'catid' => $catId,
'created' => Factory::getDate()->toSql(),
'created_by' => $createdBy,
'language' => '*',
'access' => 1, // Public
];
// Bind data to table object first
if (!$table->bind($data)) {
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
// Check data validity
if (!$table->check()) {
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
// Save the table
if (!$table->store()) {
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
echo '<p class="alert alert-info">✓ Created Terms of Service article</p>';
return $table->id;
} catch (\Exception $e) {
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
return null;
}
// Get content table via MVCFactory (Joomla 4/5 compatible)
$table = $app->bootComponent('com_content')
->getMVCFactory()
->createTable('Article', 'Administrator');
/**
* Create Terms of Service menu item
*
* @param int $articleId The article ID to link to
*
* @return void
*
* @since 1.0.0
*/
private function createTermsMenuItem($articleId)
{
try {
$db = Factory::getDbo();
// Check if "Legal" menu type exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
$db->setQuery($query);
$legalMenuExists = $db->loadResult();
// Create "Legal" menu type if it doesn't exist
if (!$legalMenuExists) {
$this->createLegalMenuType();
}
// Get com_content component ID dynamically
$query = $db->getQuery(true)
->select('extension_id')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$componentId = (int) $db->loadResult();
if (!$table) {
Log::add('Failed to get Content table instance', Log::WARNING, 'jerror');
return null;
}
if (!$componentId) {
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
return;
}
Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables');
$table = Table::getInstance('Menu', 'Joomla\\Component\\Menus\\Administrator\\Table\\');
if (!$table) {
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
return;
}
$data = [
'menutype' => 'legal',
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
'type' => 'component',
'published' => 1,
'parent_id' => 1,
'component_id' => $componentId,
'level' => 1,
'language' => '*',
'access' => 1, // Public
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
];
// Set the location in the menu tree
$table->setLocation($data['parent_id'], 'last-child');
// Bind data to table object
if (!$table->bind($data)) {
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
// Check data validity
if (!$table->check()) {
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
// Save the menu item
if (!$table->store()) {
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
echo '<p class="alert alert-info">✓ Created Terms of Service menu item in Legal menu with slug: terms-of-service</p>';
// Enable the plugin
$this->enablePlugin();
} catch (\Exception $e) {
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
// Get Uncategorised category ID dynamically
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__categories'))
->where($db->quoteName('extension') . ' = ' . $db->quote('com_content'))
->where($db->quoteName('alias') . ' = ' . $db->quote('uncategorised'))
->where($db->quoteName('published') . ' = 1');
$db->setQuery($query);
$catId = (int) $db->loadResult();
/**
* Create Legal menu type
*
* @return void
*
* @since 1.0.0
*/
private function createLegalMenuType()
{
try {
$db = Factory::getDbo();
// Insert the Legal menu type
$query = $db->getQuery(true)
->insert($db->quoteName('#__menu_types'))
->columns($db->quoteName(['menutype', 'title', 'description']))
->values(
$db->quote('legal') . ', ' .
$db->quote('Legal') . ', ' .
$db->quote('Legal documents and policies menu')
);
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-info">✓ Created Legal menu type</p>';
} catch (\Exception $e) {
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
if (!$catId) {
Log::add('Could not find Uncategorised category for com_content', Log::WARNING, 'jerror');
return null;
}
$createdBy = $app->getIdentity()->id ?: 0;
$data = [
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'introtext' => '<h2>Terms of Service</h2><p>Welcome to our Terms of Service page.</p><p>This page will remain accessible even when the site is in offline/maintenance mode.</p>',
'fulltext' => '',
'state' => 1,
'catid' => $catId,
'created' => Factory::getDate()->toSql(),
'created_by' => $createdBy,
'language' => '*',
'access' => 1,
'params' => '{}',
'metadata' => '{"robots":"","author":"","rights":""}',
'attribs' => '{}',
];
if (!$table->bind($data)) {
Log::add('Failed to bind data to Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
if (!$table->check()) {
Log::add('Content table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
if (!$table->store()) {
Log::add('Failed to store Content table: ' . $table->getError(), Log::WARNING, 'jerror');
return null;
}
echo '<p class="alert alert-info">Created Terms of Service article</p>';
return $table->id;
} catch (\Throwable $e) {
Log::add('Error creating Terms of Service article: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
return null;
}
/**
* Create Terms of Service menu item using Joomla 5 MVCFactory
*
* Fixes #90: Uses bootComponent()->getMVCFactory() instead of
* the removed Table::addIncludePath() / Table::getInstance().
*
* @param int $articleId The article ID to link to
*
* @return void
*
* @since 1.0.0
*/
private function createTermsMenuItem($articleId)
{
try {
$db = Factory::getDbo();
$app = Factory::getApplication();
// Check if "Legal" menu type exists
$query = $db->getQuery(true)
->select('id')
->from($db->quoteName('#__menu_types'))
->where($db->quoteName('menutype') . ' = ' . $db->quote('legal'));
$db->setQuery($query);
$legalMenuExists = $db->loadResult();
if (!$legalMenuExists) {
$this->createLegalMenuType();
}
// Get com_content component ID dynamically
$query = $db->getQuery(true)
->select('extension_id')
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
->where($db->quoteName('element') . ' = ' . $db->quote('com_content'));
$db->setQuery($query);
$componentId = (int) $db->loadResult();
if (!$componentId) {
Log::add('Could not determine com_content component ID', Log::WARNING, 'jerror');
return;
}
// Get menu table via MVCFactory (Joomla 4/5 compatible)
$table = $app->bootComponent('com_menus')
->getMVCFactory()
->createTable('Menu', 'Administrator');
if (!$table) {
Log::add('Failed to get Menu table instance', Log::WARNING, 'jerror');
return;
}
$data = [
'menutype' => 'legal',
'title' => 'Terms of Service',
'alias' => 'terms-of-service',
'link' => 'index.php?option=com_content&view=article&id=' . $articleId,
'type' => 'component',
'published' => 1,
'parent_id' => 1,
'component_id' => $componentId,
'level' => 1,
'language' => '*',
'access' => 1,
'params' => '{"show_title":"1","link_titles":"0","show_intro":"","info_block_position":"","show_category":"0","link_category":"0","show_parent_category":"0","link_parent_category":"0","show_author":"0","link_author":"0","show_create_date":"0","show_modify_date":"0","show_publish_date":"0","show_item_navigation":"0","show_icons":"0","show_print_icon":"0","show_email_icon":"0","show_hits":"0","show_noauth":"0","urls_position":"","menu-anchor_title":"","menu-anchor_css":"","menu_image":"","menu_text":1,"page_title":"","show_page_heading":0,"page_heading":"","pageclass_sfx":"","menu-meta_description":"","menu-meta_keywords":"","robots":"","secure":0}',
];
$table->setLocation($data['parent_id'], 'last-child');
if (!$table->bind($data)) {
Log::add('Failed to bind data to Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
if (!$table->check()) {
Log::add('Menu table check failed: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
if (!$table->store()) {
Log::add('Failed to store Menu table: ' . $table->getError(), Log::WARNING, 'jerror');
return;
}
echo '<p class="alert alert-info">Created Terms of Service menu item in Legal menu</p>';
} catch (\Throwable $e) {
Log::add('Error creating Terms of Service menu item: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Create Legal menu type
*
* @return void
*
* @since 1.0.0
*/
private function createLegalMenuType()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->insert($db->quoteName('#__menu_types'))
->columns($db->quoteName(['menutype', 'title', 'description']))
->values(
$db->quote('legal') . ', ' .
$db->quote('Legal') . ', ' .
$db->quote('Legal documents and policies menu')
);
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-info">Created Legal menu type</p>';
} catch (\Throwable $e) {
// Duplicate key is expected if race condition — safe to ignore
Log::add('Error creating Legal menu type: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Auto-select default menu slugs (terms-of-service, privacy-policy)
*
* Looks up menu items matching common legal page aliases and sets
* them as the default tos_slug parameter so the plugin works
* immediately after install with zero configuration.
*
* @return void
*
* @since 4.2.1
*/
private function setDefaultSlugs()
{
try {
$db = Factory::getDbo();
$defaultAliases = ['terms-of-service', 'privacy-policy'];
$slugs = [];
foreach ($defaultAliases as $alias) {
$query = $db->getQuery(true)
->select($db->quoteName('path'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('alias') . ' = ' . $db->quote($alias))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$path = $db->loadResult();
if ($path) {
$slugs[] = trim($path, '/');
}
}
if (empty($slugs)) {
return;
}
// Load current plugin params
$query = $db->getQuery(true)
->select($db->quoteName('params'))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$paramsJson = $db->loadResult();
$params = json_decode($paramsJson ?: '{}', true) ?: [];
// Only set defaults if no slugs are already configured
$existing = $params['tos_slug'] ?? [];
if (!empty($existing)) {
return;
}
$params['tos_slug'] = $slugs;
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
} catch (\Throwable $e) {
Log::add('Error setting default slugs: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Enable the plugin after installation
*
* @return void
*
* @since 1.0.0
*/
private function enablePlugin()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
} catch (\Throwable $e) {
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
/**
* Enable the plugin after installation
*
* @return void
*
* @since 1.0.0
*/
private function enablePlugin()
{
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomtos'));
$db->setQuery($query);
$db->execute();
echo '<p class="alert alert-success">✓ Plugin enabled automatically</p>';
} catch (\Exception $e) {
Log::add('Error enabling plugin: ' . $e->getMessage(), Log::WARNING, 'jerror');
}
}
}
-44
View File
@@ -1,44 +0,0 @@
<?php
/**
* @package MokoJoomTOS
* @subpackage plg_system_mokojoomtos
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\MokoJoomTOS\Extension\MokoJoomTOS;
return new class () implements ServiceProviderInterface {
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 04.00.00
*/
public function register(Container $container)
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new MokoJoomTOS(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'mokojoomtos')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
+89 -210
View File
@@ -18,227 +18,106 @@ use Joomla\Event\SubscriberInterface;
/**
* MokoJoomTOS Offline Mode Bypass Plugin
*
* Allows configured menu items to remain accessible when the site
* Allows Terms of Service menu to be accessible via slug when the site
* is in offline mode.
*
* @since 1.0.0
*/
final class MokoJoomTOS extends CMSPlugin implements SubscriberInterface
{
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Load the language file on instantiation.
*
* @var boolean
* @since 1.0.0
*/
protected $autoloadLanguage = true;
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 1.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
];
}
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* After route event handler
*
* Checks if the current request matches any configured menu slug and if
* the site is in offline mode. If both conditions are met, temporarily
* disables offline mode and sets component-only view for this request.
*
* @return void
*
* @since 1.0.0
*/
public function onAfterRoute()
{
$app = $this->getApplication();
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 1.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
];
}
// Only process for site application
if (!$app->isClient('site'))
{
return;
}
/**
* After route event handler
*
* Checks if the current request is for the Terms of Service slug and if
* the site is in offline mode. If both conditions are met, temporarily
* disables offline mode and sets component-only view for this request.
*
* This event fires after routing but before template selection, making it
* the correct place to set tmpl=component to prevent template chrome loading.
*
* @return void
*
* @since 1.0.0
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->app->isClient('site'))
{
return;
}
$config = $app->getConfig();
// Get the global configuration
$config = $this->app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the configured Terms of Service slug
$tosSlug = trim($this->params->get('tos_slug', 'terms-of-service'));
if (empty($tosSlug))
{
return;
}
// Get the configured slugs — cast to array to handle stdClass from Registry (#96)
$slugs = $this->params->get('tos_slug', []);
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
// Remove the base path if present
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Handle legacy single-value string format
if (is_string($slugs))
{
$slugs = array_filter([trim($slugs)]);
}
else
{
$slugs = (array) $slugs;
}
if (empty($slugs))
{
return;
}
$includeChildren = (int) $this->params->get('include_children', 1);
// Try SEF path matching first, then fall back to Itemid matching (#91)
if ($this->matchByPath($slugs, $config, $app, $includeChildren))
{
return;
}
$this->matchByItemId($slugs, $config, $app, $includeChildren);
}
/**
* Match the current request path against configured slugs (SEF mode)
*
* @param array $slugs Configured slug values
* @param object $config Joomla configuration object
* @param object $app Application instance
* @param integer $includeChildren Whether to include child menu items
*
* @return boolean True if a match was found and offline mode was bypassed
*
* @since 4.1.0
*/
private function matchByPath(array $slugs, $config, $app, int $includeChildren = 1): bool
{
$uri = Uri::getInstance();
$path = urldecode(trim($uri->getPath(), '/'));
// Remove the base path if present (subdirectory installs)
$base = trim(Uri::base(true), '/');
if (!empty($base) && strpos($path, $base) === 0)
{
$path = trim(substr($path, strlen($base)), '/');
}
// Skip if path is empty or just index.php (non-SEF)
if (empty($path) || $path === 'index.php')
{
return false;
}
foreach ($slugs as $slug)
{
$slug = trim((string) $slug);
if (empty($slug))
{
continue;
}
$isMatch = ($path === $slug)
|| ($includeChildren && strpos($path, $slug . '/') === 0);
if ($isMatch)
{
$this->bypassOffline($config, $app);
return true;
}
}
return false;
}
/**
* Match the current request Itemid against menu items for configured slugs (non-SEF fallback)
*
* When SEF URLs are disabled, the path is just index.php so we match by
* checking if the requested Itemid belongs to a menu item whose path
* matches a configured slug.
*
* @param array $slugs Configured slug values
* @param object $config Joomla configuration object
* @param object $app Application instance
* @param integer $includeChildren Whether to include child menu items
*
* @return boolean True if a match was found and offline mode was bypassed
*
* @since 4.1.0
*/
private function matchByItemId(array $slugs, $config, $app, int $includeChildren = 1): bool
{
$itemId = (int) $app->input->getInt('Itemid', 0);
if (!$itemId)
{
return false;
}
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('path'))
->from($db->quoteName('#__menu'))
->where($db->quoteName('id') . ' = ' . $itemId)
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0');
$db->setQuery($query);
$menuPath = $db->loadResult();
if (!$menuPath)
{
return false;
}
$menuPath = trim($menuPath, '/');
foreach ($slugs as $slug)
{
$slug = trim((string) $slug);
if (empty($slug))
{
continue;
}
$isMatch = ($menuPath === $slug)
|| ($includeChildren && strpos($menuPath, $slug . '/') === 0);
if ($isMatch)
{
$this->bypassOffline($config, $app);
return true;
}
}
}
catch (\Throwable $e)
{
// Silently fail — do not bypass offline mode on error
}
return false;
}
/**
* Bypass offline mode and set component-only view for this request
*
* @param object $config Joomla configuration object
* @param object $app Application instance
*
* @return void
*
* @since 4.1.0
*/
private function bypassOffline($config, $app): void
{
$config->set('offline', 0);
$app->input->set('tmpl', 'component');
}
// Check if the path matches the Terms of Service slug
if ($path === $tosSlug || strpos($path, $tosSlug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
// This ensures clean display without full site template
$input = $this->app->input;
$input->set('tmpl', 'component');
// Also set it in the GET superglobal to ensure it's recognized
$_GET['tmpl'] = 'component';
}
}
}
+68 -84
View File
@@ -17,99 +17,83 @@ use Joomla\CMS\Language\Text;
/**
* Menu Slug Field
*
* Provides a multi-select dropdown of menu items with their full route paths
* Provides a dropdown list of menu items with their aliases (slugs)
*
* @since 1.0.0
*/
class MenuslugField extends ListField
{
/**
* The form field type.
*
* @var string
* @since 1.0.0
*/
protected $type = 'Menuslug';
/**
* The form field type.
*
* @var string
* @since 1.0.0
*/
protected $type = 'Menuslug';
/**
* Method to get the field options.
*
* @return array The field option objects.
*
* @since 1.0.0
*/
protected function getOptions()
{
$options = parent::getOptions();
/**
* Method to get the field options.
*
* @return array The field option objects.
*
* @since 1.0.0
*/
protected function getOptions()
{
$options = parent::getOptions();
// Warn if SEF URLs are disabled (#97)
try
{
$sef = Factory::getApplication()->get('sef', true);
if (!$sef)
{
$options[] = (object) [
'value' => '',
'text' => Text::_('PLG_SYSTEM_MOKOJOOMTOS_FIELD_SEF_WARNING'),
'disabled' => true
];
}
}
catch (\Exception $e)
{
// Ignore — field still works without the warning
}
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['alias', 'title', 'menutype']))
->from($db->quoteName('#__menu'))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('alias') . ' != ' . $db->quote(''))
->order($db->quoteName('menutype') . ', ' . $db->quoteName('title'));
try
{
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['path', 'alias', 'title', 'menutype']))
->from($db->quoteName('#__menu'))
->where($db->quoteName('published') . ' = 1')
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('alias') . ' != ' . $db->quote(''))
->order($db->quoteName('menutype') . ', ' . $db->quoteName('title'));
$db->setQuery($query);
$menuItems = $db->loadObjectList();
$db->setQuery($query);
$menuItems = $db->loadObjectList();
if ($menuItems)
{
$lastMenuType = '';
foreach ($menuItems as $item)
{
// Add menu type separator for better organization
if ($item->menutype !== $lastMenuType)
{
if ($lastMenuType !== '')
{
// Add a separator between menu types
$options[] = (object) [
'value' => '',
'text' => '──────────────',
'disable' => true
];
}
$lastMenuType = $item->menutype;
}
if ($menuItems)
{
$lastMenuType = '';
$displayText = $item->title !== '' ? $item->title : ucwords(str_replace(['-', '_'], ' ', $item->alias));
$options[] = (object) [
'value' => $item->alias,
'text' => $displayText . ' (' . $item->alias . ')'
];
}
}
}
catch (\Exception $e)
{
// Log error but don't break the form
Factory::getApplication()->enqueueMessage(
Text::sprintf('PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS', $e->getMessage()),
'warning'
);
}
foreach ($menuItems as $item)
{
// Add menu type separator for better organization
if ($item->menutype !== $lastMenuType)
{
if ($lastMenuType !== '')
{
$options[] = (object) [
'value' => '',
'text' => '──────────────',
'disabled' => true
];
}
$lastMenuType = $item->menutype;
}
$displayText = $item->title !== '' ? $item->title : ucwords(str_replace(['-', '_'], ' ', $item->alias));
$options[] = (object) [
'value' => $item->path,
'text' => $displayText . ' (/' . $item->path . ')'
];
}
}
}
catch (\Exception $e)
{
Factory::getApplication()->enqueueMessage(
Text::sprintf('PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS', $e->getMessage()),
'warning'
);
}
return $options;
}
return $options;
}
}
+38 -92
View File
@@ -1,98 +1,44 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 04.03.00
-->
VERSION: 03.08.04
Joomla Extension Update Server XML
See: https://docs.joomla.org/Deploying_an_Update_Server
This file is the update server manifest for MokoJoomTOS.
The Joomla installer polls this URL to check for new versions.
The manifest.xml in this repository must reference BOTH update servers:
<updateservers>
<server type="extension" priority="1" name="MokoJoomTOS Update Server (Gitea)">
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/raw/branch/main/updates.xml
</server>
<server type="extension" priority="2" name="MokoJoomTOS Update Server (GitHub)">
https://raw.githubusercontent.com/mokoconsulting-tech/MokoJoomTOS/main/updates.xml
</server>
</updateservers>
When a new release is made, run `make release` or the release workflow to
prepend a new <update> entry to this file automatically.
-->
<updates>
<update>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.03.00-dev</version>
<client>site</client>
<folder>system</folder>
<tags><tag>development</tag></tags>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/development</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
</downloads>
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.03.00-alpha</version>
<client>site</client>
<folder>system</folder>
<tags><tag>alpha</tag></tags>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/alpha</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
</downloads>
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.03.00-beta</version>
<client>site</client>
<folder>system</folder>
<tags><tag>beta</tag></tags>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/beta</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
</downloads>
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.03.00-rc</version>
<client>site</client>
<folder>system</folder>
<tags><tag>rc</tag></tags>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/rc</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
</downloads>
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - Moko Terms of Service</name>
<description>System - Moko Terms of Service update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.03.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>stable</tag></tags>
<infourl title="System - Moko Terms of Service">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/v04/plg_system_mokojoomtos-04.03.00.zip</downloadurl>
</downloads>
<sha256>7668df3f07eef38c8172dcde129a29078736f0a511b51446ec1cc51bc2bd2e4c</sha256>
<targetplatform name="joomla" version="(5|6)\..*" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>MokoJoomTOS — Moko Consulting Joomla plugin</description>
<element>plg_system_mokojoomtos</element>
<type>plugin</type>
<folder>system</folder>
<version>03.08.04</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/v03/plg_system_mokojoomtos-03.08.04.zip
</downloadurl>
<downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/download/v03/plg_system_mokojoomtos-03.08.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="[56].*"/>
<php_minimum>8.1</php_minimum>
</update>
</updates>