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
54 changed files with 593 additions and 649 deletions
@@ -8,18 +8,18 @@
<identity>
<name>MokoJoomTOS</name>
<org>MokoConsulting</org>
<description>Joomla system plugin to keep legal pages accessible during offline mode</description>
<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-16T17:30:00+00:00</last-synced>
<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>
@@ -58,7 +58,7 @@ jobs:
token: ${{ secrets.GA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform
- name: Setup MokoStandards tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
@@ -70,74 +70,31 @@ jobs:
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 ]; then
cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MokoStandards tools: $([ -d /tmp/mokostandards-api ] && echo 'available' || echo 'unavailable (using fallback)')"
/tmp/mokostandards-api
cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet
# -- PLATFORM DETECTION ---------------------------------------------------
- name: Detect platform
id: platform
run: |
# 1. Try explicit platform from manifest (.mokogitea takes precedence)
if [ -f ".mokogitea/manifest.xml" ]; then
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
elif [ -f ".gitea/manifest.xml" ]; then
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1)
fi
# 2. Auto-sense from file structure if no manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
CB_MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<cbplugin' {} \; 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)
if [ -z "$PLATFORM" ]; then
if [ -n "$CB_MANIFEST" ]; then
PLATFORM="joomla"
MANIFEST="$CB_MANIFEST"
elif [ -n "$MANIFEST" ]; then
PLATFORM="joomla"
elif [ -n "$MOD_FILE" ]; then
PLATFORM="dolibarr"
else
PLATFORM="generic"
fi
fi
# 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"
echo "Platform detected: ${PLATFORM}"
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"
# Detect update file methodology
if [ "$PLATFORM" = "joomla" ]; then
echo "update_method=updates.xml" >> "$GITHUB_OUTPUT"
echo "Update method: Joomla updates.xml"
elif [ "$PLATFORM" = "dolibarr" ]; then
echo "update_method=update.txt" >> "$GITHUB_OUTPUT"
echo "Update method: Dolibarr update.txt"
else
echo "update_method=none" >> "$GITHUB_OUTPUT"
echo "Update method: none (generic)"
fi
# -- STEP 1: Read version -----------------------------------------------
- name: "Step 1: Read version from README.md"
id: version
run: |
# Try MokoStandards PHP tool first, fall back to direct parsing
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || true)
if [ -z "$VERSION" ]; then
# Fallback: read from README table format "| **Version** | XX.YY.ZZ |"
VERSION=$(sed -n 's/.*\*\*Version\*\*[[:space:]]*|[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
fi
if [ -z "$VERSION" ]; then
# Fallback: read VERSION: XX.YY.ZZ pattern from any file
VERSION=$(grep -rh "VERSION:[[:space:]]*[0-9]" README.md CHANGELOG.md 2>/dev/null | sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' | head -1)
fi
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release"
echo "skip=true" >> "$GITHUB_OUTPUT"
@@ -165,15 +122,54 @@ jobs:
echo "Version: $VERSION (patch — platform version + badges only)"
fi
# -- STEP 1b: Promote CHANGELOG [Unreleased] to current version -----------
- name: "Step 1b: Promote CHANGELOG for release"
# -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------
- name: "Step 1b: Bump minor version for stable release"
if: steps.version.outputs.skip != 'true'
id: bump
run: |
VERSION="${{ steps.version.outputs.version }}"
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" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; }
MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1)))
MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2)))
# Minor bump, reset patch. Rollover if minor > 99
MINOR=$((MINOR + 1))
if [ $MINOR -gt 99 ]; then
MINOR=0
MAJOR=$((MAJOR + 1))
fi
VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR)
TODAY=$(date +%Y-%m-%d)
# Promote [Unreleased] section in CHANGELOG.md to release version
echo "Stable bump: ${CURRENT} → ${VERSION} (minor)"
# 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)
[ -n "$MANIFEST_VER" ] && 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
echo "${VERSION}" > update.txt
;;
*) ;;
esac
# Promote [Unreleased] section in CHANGELOG.md to new version
if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then
sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md
@@ -182,19 +178,19 @@ jobs:
echo "CHANGELOG promoted to [${VERSION}]"
fi
# Commit changelog promotion if changed
# Commit and push
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(release): promote CHANGELOG ${VERSION} [skip ci]"
git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]"
git push origin HEAD:main 2>&1
}
# Pass through version (no bump — release uses version as-is from dev)
# Override version output for rest of pipeline
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "major=${{ steps.version.outputs.major }}" >> "$GITHUB_OUTPUT"
echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT"
- name: Check if already released
if: steps.version.outputs.skip != 'true'
@@ -339,19 +335,8 @@ jobs:
steps.check.outputs.already_released != 'true'
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
if [ -f /tmp/mokostandards-api/cli/version_set_platform.php ]; then
php /tmp/mokostandards-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
else
# Fallback: update version in templateDetails.xml directly
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
sed -i "s|<version>[^<]*</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>$(date +%Y-%m-%d)</creationDate>|" "$MANIFEST"
fi
# Update README version table
sed -i "s/|\s*\*\*Version\*\*\s*|[^|]*/| **Version** | ${VERSION} /" README.md 2>/dev/null || true
fi
php /tmp/mokostandards-api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ----------------------------------------
- name: "Step 4: Update version badges"
@@ -377,35 +362,18 @@ jobs:
REPO="${{ github.repository }}"
# -- Parse extension metadata from XML manifest ----------------
# Check for Joomla <extension> or Community Builder <cbplugin>
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
CB_MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<cbplugin' {} \; 2>/dev/null | head -1)
IS_CB="false"
if [ -z "$MANIFEST" ] && [ -z "$CB_MANIFEST" ]; then
echo "Warning: No Joomla/CB XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
if [ -z "$MANIFEST" ]; then
echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# CB plugin takes precedence if no standard manifest
if [ -z "$MANIFEST" ] && [ -n "$CB_MANIFEST" ]; then
MANIFEST="$CB_MANIFEST"
IS_CB="true"
fi
# Extract fields using sed (portable — no grep -P)
EXT_NAME=$(sed -n 's/.*<name>\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1)
if [ "$IS_CB" = "true" ]; then
EXT_TYPE="cb_plugin"
EXT_ELEMENT=$(sed -n 's/.*<cbplugin[^>]*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1)
EXT_CLIENT=""
EXT_FOLDER="cb"
else
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)
fi
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)
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)
@@ -482,7 +450,6 @@ jobs:
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
@@ -592,7 +559,7 @@ jobs:
fi
[ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}"
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
# Build release name: "Pretty Name VERSION (type_element-VERSION)"
@@ -604,7 +571,6 @@ jobs:
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})"
@@ -675,7 +641,6 @@ jobs:
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
@@ -838,7 +803,6 @@ jobs:
template) TYPE_PREFIX="tpl_" ;;
library) TYPE_PREFIX="lib_" ;;
package) TYPE_PREFIX="pkg_" ;;
cb_plugin) TYPE_PREFIX="cb_" ;;
esac
ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip"
TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz"
@@ -886,7 +850,65 @@ jobs:
" 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY
fi
# -- (GitHub mirror removed — Gitea is the sole release platform) ----------
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" || true
else
gh release edit "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" || true
fi
# Upload assets to GitHub mirror
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
# -- Clean up lesser pre-releases (cascade) ---------------------------------
# stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev
@@ -917,74 +939,25 @@ jobs:
done
echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY
# -- STEP 11: Reset dev branch and bump to next minor -------------------------
- name: "Step 11: Reset dev and bump to next minor"
# -- STEP 11: Reset dev branch from main ------------------------------------
- name: "Step 11: Delete and recreate dev branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
# Calculate next minor version for dev
MAJOR=$((10#$(echo "$VERSION" | cut -d. -f1)))
MINOR=$((10#$(echo "$VERSION" | cut -d. -f2)))
MINOR=$((MINOR + 1))
if [ $MINOR -gt 99 ]; then
MINOR=0
MAJOR=$((MAJOR + 1))
fi
NEXT=$(printf "%02d.%02d.00" $MAJOR $MINOR)
# Bump version on dev via API (README + manifest)
# Update README.md on dev
README_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/README.md?ref=dev" 2>/dev/null || true)
README_SHA=$(echo "$README_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
README_CONTENT=$(echo "$README_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
if [ -n "$README_SHA" ] && [ -n "$README_CONTENT" ]; then
UPDATED=$(echo "$README_CONTENT" | sed "s/${VERSION}/${NEXT}/g")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \
"${API_BASE}/contents/README.md" \
-d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${README_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true
fi
# Update manifest on dev (Joomla or Dolibarr)
case "$PLATFORM" in
joomla)
MANIFEST_PATH="${{ steps.platform.outputs.manifest }}"
[ -n "$MANIFEST_PATH" ] && MANIFEST_PATH=$(echo "$MANIFEST_PATH" | sed 's|^\./||')
if [ -n "$MANIFEST_PATH" ]; then
ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${MANIFEST_PATH}'))")
MF_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
MF_SHA=$(echo "$MF_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
MF_CONTENT=$(echo "$MF_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
if [ -n "$MF_SHA" ] && [ -n "$MF_CONTENT" ]; then
UPDATED=$(echo "$MF_CONTENT" | sed "s|<version>${VERSION}</version>|<version>${NEXT}</version>|")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" \
"${API_BASE}/contents/${ENCODED_PATH}" \
-d "$(python3 -c "import json; print(json.dumps({'content':'${ENCODED}','sha':'${MF_SHA}','message':'chore(version): bump ${VERSION} → ${NEXT} (dev) [skip ci]','branch':'dev'}))")" > /dev/null 2>&1 || true
fi
fi
;;
dolibarr)
# Dolibarr handled by separate step below
;;
esac
echo "Dev branch bumped to ${NEXT}" >> $GITHUB_STEP_SUMMARY
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
@@ -108,8 +108,9 @@ jobs:
- name: Detect platform
id: platform
run: |
# Parse manifest for platform detection
PLATFORM=$(php /tmp/mokostandards-api/cli/manifest_read.php --path . --field platform 2>/dev/null)
# 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"
@@ -193,32 +194,3 @@ jobs:
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; }
# ── Changelog Gate ────────────────────────────────────────────────────
changelog:
name: Changelog Updated
runs-on: ubuntu-latest
if: github.base_ref == 'main'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check CHANGELOG.md was updated
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
if git diff --name-only "$BASE" "$HEAD" | grep -q "^CHANGELOG.md$"; then
echo "CHANGELOG.md updated"
else
# Allow [skip changelog] in PR title or body
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
if echo "$PR_TITLE $PR_BODY" | grep -qi "\[skip changelog\]"; then
echo "::warning::Changelog skip requested via [skip changelog]"
exit 0
fi
echo "::error::CHANGELOG.md must be updated before merging to main. Add [skip changelog] to the PR title to bypass."
exit 1
fi
@@ -55,11 +55,12 @@ jobs:
- name: Detect platform
id: platform
run: |
# Read platform from .gitea/manifest.xml
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .gitea/manifest.xml 2>/dev/null | head -1)
# 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/*" ! -path "./.gitea/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
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"
+19 -44
View File
@@ -1,10 +1,4 @@
<!--
## [Unreleased]
## [04.01.00] --- 2026-05-16
## [04.00.00] --- 2026-05-16
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
@@ -28,7 +22,7 @@
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
REPO: https://github.com/mokoconsulting-tech/MokoJoomTOS
VERSION: 04.00.00
VERSION: 03.08.04
PATH: ./CHANGELOG.md
BRIEF: Version history and release notes
-->
@@ -40,53 +34,34 @@ 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).
## [04.00.00] --- 2026-05-16
## [Unreleased]
## [04.00.00] - 2026-05-16
### Planned
### Added
- Multi-select support: configure multiple menu items to remain accessible during offline mode
- `services/provider.php` for Joomla 5 dependency injection container registration
- Gitea-hosted update server URL in plugin manifest
### Changed
- Match against full menu route path instead of alias only (fixes nested routes like `/legal/terms-of-service`)
- Plugin pretty name updated to Joomla convention: "System - Moko Terms of Service"
- Renamed `.mokogitea/` to `.gitea/` for standard Gitea compatibility
- MenuslugField now stores and displays full route paths (e.g., `legal/terms-of-service`)
### Removed
- GitHub update server references (fully migrated to Gitea)
- Legacy `src/plugins/` directory
## [03.09.00] - 2026-05-16
- GitHub Actions CI/CD workflows
- Pre-commit hooks for code quality
- Docker development environment
### Fixed
- Installation script now resolves Uncategorised category ID dynamically (no longer assumes ID 2)
- Installation script now resolves `com_content` component ID dynamically (removed hardcoded fallback 22)
- All `Exception` catches qualified with backslash for namespace safety
- Added missing `PLG_SYSTEM_MOKOJOOMTOS_ERROR_LOADING_MENU_ITEMS` language key to all locale files
- Standardized help text URL across all locale files
- Articles created during install are now owned by the installing admin user (not user ID 0)
- 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
- Added Gitea update server URL to plugin manifest (`updates.xml` on `main`)
- Removed obsolete `src/plugins/` legacy directory
- 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
### Removed
- Legacy duplicate manifest at `src/plugins/system/mokojoomtos/mokojoomtos.xml`
## [03.09.00] - 2026-02-28
## [03.08.04] - 2026-02-28
### Changed
- Updated version number to 03.09.00 across all files
- Updated version number to 03.08.04 across all files
- Fixed template chrome loading issue by changing event hook from onAfterInitialise to onAfterRoute
### Fixed
@@ -189,6 +164,6 @@ This template is ready for use in creating new Joomla components that follow org
- [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.09.00...HEAD
[03.09.00]: https://github.com/mokoconsulting-tech/MokoJoomTOS/releases/tag/v03.09.00
[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
+4 -4
View File
@@ -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
+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.01.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.01.00 |
| **Version** | 03.08.04 |
---
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,17 +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_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"
@@ -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,17 +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_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"
@@ -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,17 +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_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"
@@ -3,17 +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_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"
+72 -81
View File
@@ -8,107 +8,98 @@
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
/**
* MokoJoomTOS Offline Mode Bypass Plugin (Legacy)
* 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
*/
class PlgSystemMokojoomtos extends CMSPlugin
{
/**
* 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;
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* Application object
*
* @var \Joomla\CMS\Application\CMSApplication
* @since 1.0.0
*/
protected $app;
/**
* After route event handler
*
* @return void
*
* @since 04.00.00
*/
public function onAfterRoute()
{
// Only process for site application
if (!$this->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 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();
// 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 slugs (stored as array for multi-select)
$slugs = $this->params->get('tos_slug', []);
// Get the configured Terms of Service slug
$tosSlug = trim($this->params->get('tos_slug', 'terms-of-service'));
// Handle legacy single-value string format
if (is_string($slugs))
{
$slugs = array_filter([trim($slugs)]);
}
if (empty($tosSlug))
{
return;
}
if (empty($slugs))
{
return;
}
// Get the current URI path
$uri = Uri::getInstance();
$path = trim($uri->getPath(), '/');
// 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)), '/');
}
// 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);
// Check if the path matches any configured slug
foreach ($slugs as $slug)
{
$slug = trim($slug);
if (empty($slug))
{
continue;
}
// Set component-only view (no template chrome)
$input = $this->app->input;
$input->set('tmpl', 'component');
if ($path === $slug || strpos($path, $slug . '/') === 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';
return;
}
}
}
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
}
}
}
+6 -6
View File
@@ -25,19 +25,19 @@
DEFGROUP: MokoJoomTOS
INGROUP: plg_system_mokojoomtos
PATH: src/mokojoomtos.xml
VERSION: 04.00.00
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>00.00.01</version>
<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,7 +68,8 @@
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
-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;
}
);
}
};
+90 -97
View File
@@ -10,6 +10,7 @@ namespace Joomla\Plugin\System\MokoJoomTOS\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Uri\Uri;
use Joomla\Event\SubscriberInterface;
@@ -17,114 +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.
*
* 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->getApplication()->isClient('site'))
{
return;
}
/**
* Returns an array of events this subscriber will listen to.
*
* @return array
*
* @since 1.0.0
*/
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
];
}
// Get the global configuration
$config = $this->getApplication()->getConfig();
/**
* 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;
}
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the global configuration
$config = $this->app->getConfig();
// Only proceed if site is offline
if (!$config->get('offline'))
{
return;
}
// Get the configured slugs (stored as array for multi-select)
$slugs = $this->params->get('tos_slug', []);
// Get the configured Terms of Service slug
$tosSlug = trim($this->params->get('tos_slug', 'terms-of-service'));
if (empty($tosSlug))
{
return;
}
// Handle legacy single-value string format
if (is_string($slugs))
{
$slugs = array_filter([trim($slugs)]);
}
// 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)), '/');
}
if (empty($slugs))
{
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 any configured slug
foreach ($slugs as $slug)
{
$slug = trim($slug);
if (empty($slug))
{
continue;
}
if ($path === $slug || strpos($path, $slug . '/') === 0)
{
// Temporarily disable offline mode for this request
$config->set('offline', 0);
// Set component-only view (no template chrome)
$input = $this->getApplication()->input;
$input->set('tmpl', 'component');
// Also set in GET superglobal to ensure recognition
$_GET['tmpl'] = 'component';
return;
}
}
}
// 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 -66
View File
@@ -17,81 +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();
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'));
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'));
$db->setQuery($query);
$menuItems = $db->loadObjectList();
$db->setQuery($query);
$menuItems = $db->loadObjectList();
if ($menuItems)
{
$lastMenuType = '';
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;
}
foreach ($menuItems as $item)
{
// Add menu type separator for better organization
if ($item->menutype !== $lastMenuType)
{
if ($lastMenuType !== '')
{
$options[] = (object) [
'value' => '',
'text' => '──────────────',
'disable' => true
];
}
$lastMenuType = $item->menutype;
}
$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'
);
}
$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.00.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>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>development</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>alpha</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>beta</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>rc</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>plg_system_mokojoomtos</name>
<description>plg_system_mokojoomtos update</description>
<element>mokojoomtos</element>
<type>plugin</type>
<version>04.00.00</version>
<client>site</client>
<folder>system</folder>
<tags><tag>stable</tag></tags>
<infourl title="plg_system_mokojoomtos">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomTOS/releases/download/stable/plg_system_mokojoomtos-04.00.00.zip</downloadurl>
</downloads>
<sha256>6d8c6a03d6dc0a784a4442c9c46e1a79e8f65b3da7091c5857958653c5b6efe3</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<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>