Compare commits
70 Commits
main
..
development
| Author | SHA1 | Date | |
|---|---|---|---|
| b8a1b9769a | |||
| 90410c3add | |||
| 28bcf014bc | |||
| 70fb6e04b2 | |||
| f1f9b7eb73 | |||
| 6af960d1f0 | |||
| f7311a8583 | |||
| dcaee3e9ca | |||
| 6704138998 | |||
| 8bbae58d83 | |||
| e3bc5a0f42 | |||
| c0b03ac5f4 | |||
| 9a8d395cc8 | |||
| 2927e7420a | |||
| 1811e080ad | |||
| d1cc81624f | |||
| 1320a567f8 | |||
| 8d77ca9dd4 | |||
| c31ac0f4bf | |||
| d97695a858 | |||
| ffbb46f5e8 | |||
| e8dad8541e | |||
| d5cd995a79 | |||
| ab21d17563 | |||
| 560035655a | |||
| 3858869b24 | |||
| a053cc8631 | |||
| bfb28a2050 | |||
| 0067835a17 | |||
| 62e5cf6fb5 | |||
| e34f52fb23 | |||
| 348aaf1d8b | |||
| b63779a675 | |||
| aa61834e61 | |||
| 206239ab7d | |||
| d273f2e615 | |||
| 520422db8c | |||
| 035fe6d619 | |||
| 1b316ecc40 | |||
| 8c21553137 | |||
| 76d9fdd944 | |||
| d03c479aa4 | |||
| 09153a04c4 | |||
| b6f4b1f505 | |||
| 7b5231d7f7 | |||
| 56bdcf66ec | |||
| 29d17dee8f | |||
| d030a5d886 | |||
| 3f0d45438c | |||
| 64a8504794 | |||
| 9e22a4c49c | |||
| 1ab721cbe3 | |||
| 6bb1b43195 | |||
| 5a5a5713d6 | |||
| e55acc464a | |||
| 6ed0a9f221 | |||
| 072297a75d | |||
| c0adfe41dd | |||
| 579b0f13d8 | |||
| 2b7913f6e1 | |||
| 07482df7b9 | |||
| 61f96cba64 | |||
| 71f5d161e9 | |||
| 2db043412f | |||
| c913f12621 | |||
| 772c40bb56 | |||
| 49cc5becf6 | |||
| 738c878067 | |||
| aa5f3ab06a | |||
| 9326e2d11a |
@@ -205,12 +205,6 @@ jobs:
|
|||||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: "Detect platform"
|
|
||||||
id: platform
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: "Determine version bump level"
|
- name: "Determine version bump level"
|
||||||
id: bump
|
id: bump
|
||||||
run: |
|
run: |
|
||||||
@@ -234,18 +228,6 @@ jobs:
|
|||||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: "Read published version"
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "branch=main" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Published version: ${VERSION}"
|
|
||||||
|
|
||||||
- name: Update release notes and promote changelog
|
- name: Update release notes and promote changelog
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
name: "Generic: Project CI"
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -45,17 +45,17 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
php -v && composer --version
|
php -v && composer --version
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
run: |
|
run: |
|
||||||
if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then
|
if [ -d "/tmp/moko-platform" ] || [ -d "/opt/moko-platform" ]; then
|
||||||
echo "mokocli already available on runner — skipping clone"
|
echo "moko-platform already available on runner — skipping clone"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
|
/tmp/moko-platform 2>/dev/null || echo "moko-platform clone skipped — continuing without it"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -245,413 +245,10 @@ jobs:
|
|||||||
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check config.xml and access.xml for components
|
|
||||||
run: |
|
|
||||||
echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
# Find all component manifests (XML with type="component")
|
|
||||||
COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<extension[^>]*type="component"' {} ; 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -z "$COMP_MANIFESTS" ]; then
|
|
||||||
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
for MANIFEST in $COMP_MANIFESTS; do
|
|
||||||
COMP_DIR=$(dirname "$MANIFEST")
|
|
||||||
COMP_NAME=$(basename "$COMP_DIR")
|
|
||||||
echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# Check access.xml exists
|
|
||||||
ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
||||||
if [ -z "$ACCESS_FILE" ]; then
|
|
||||||
echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
if command -v php &> /dev/null; then
|
|
||||||
if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then
|
|
||||||
echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
for ACTION in core.admin core.manage; do
|
|
||||||
if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then
|
|
||||||
echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check config.xml exists
|
|
||||||
CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
||||||
if [ -z "$CONFIG_FILE" ]; then
|
|
||||||
echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
if command -v php &> /dev/null; then
|
|
||||||
if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then
|
|
||||||
echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SQL schema validation
|
|
||||||
run: |
|
|
||||||
echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
# Find SQL files in source/htdocs
|
|
||||||
SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
|
||||||
if [ -z "$SQL_FILES" ]; then
|
|
||||||
echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
for FILE in $SQL_FILES; do
|
|
||||||
# Basic syntax check: balanced parentheses, no empty files
|
|
||||||
SIZE=$(wc -c < "$FILE" | tr -d ' ')
|
|
||||||
if [ "$SIZE" -eq 0 ]; then
|
|
||||||
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for common SQL errors
|
|
||||||
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
|
|
||||||
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check update SQL files follow version numbering pattern
|
|
||||||
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
|
||||||
if [ -n "$UPDATE_DIR" ]; then
|
|
||||||
BAD_NAMES=0
|
|
||||||
for UFILE in "$UPDATE_DIR"/*.sql; do
|
|
||||||
[ ! -f "$UFILE" ] && continue
|
|
||||||
BASENAME=$(basename "$UFILE" .sql)
|
|
||||||
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
|
|
||||||
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
BAD_NAMES=$((BAD_NAMES + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$BAD_NAMES" -gt 0 ]; then
|
|
||||||
ERRORS=$((ERRORS + BAD_NAMES))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Manifest file references check
|
|
||||||
run: |
|
|
||||||
echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
MANIFEST_DIR=$(dirname "$MANIFEST")
|
|
||||||
|
|
||||||
# Check <filename> references
|
|
||||||
FILENAMES=$(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
|
||||||
for F in $FILENAMES; do
|
|
||||||
if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then
|
|
||||||
echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check <folder> references
|
|
||||||
FOLDERS=$(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
|
||||||
for F in $FOLDERS; do
|
|
||||||
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
|
|
||||||
echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check <file> references in package manifests (ZIP files won't exist in source)
|
|
||||||
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
|
||||||
if [ "$EXT_TYPE" != "package" ]; then
|
|
||||||
FILES=$(grep -oP '<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
|
||||||
for F in $FILES; do
|
|
||||||
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
|
|
||||||
echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Form XML validation
|
|
||||||
run: |
|
|
||||||
echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
|
||||||
if [ -z "$FORM_FILES" ]; then
|
|
||||||
echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
for FILE in $FORM_FILES; do
|
|
||||||
if command -v php &> /dev/null; then
|
|
||||||
if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then
|
|
||||||
echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
# Check for valid Joomla form structure
|
|
||||||
if ! grep -qE '<form|<field|<fieldset' "$FILE" 2>/dev/null; then
|
|
||||||
echo "- \`${FILE}\`: no \`<form>\`, \`<field>\`, or \`<fieldset>\` elements found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Deprecated Joomla API check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
SRC_DIR=""
|
|
||||||
for DIR in source/ src/ htdocs/; do
|
|
||||||
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$SRC_DIR" ]; then
|
|
||||||
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
# Joomla 3/4 deprecated patterns that break in Joomla 6
|
|
||||||
PATTERNS=(
|
|
||||||
'JFactory::'
|
|
||||||
'JText::'
|
|
||||||
'JHtml::'
|
|
||||||
'JRoute::'
|
|
||||||
'JUri::'
|
|
||||||
'JLog::'
|
|
||||||
'JTable::'
|
|
||||||
'JInput'
|
|
||||||
'CMSFactory::\$application'
|
|
||||||
'JApplicationCms'
|
|
||||||
)
|
|
||||||
|
|
||||||
for PATTERN in "${PATTERNS[@]}"; do
|
|
||||||
HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true)
|
|
||||||
if [ -n "$HITS" ]; then
|
|
||||||
COUNT=$(echo "$HITS" | wc -l)
|
|
||||||
echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + COUNT))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Template output escaping check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
|
||||||
if [ -z "$TMPL_FILES" ]; then
|
|
||||||
echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
for FILE in $TMPL_FILES; do
|
|
||||||
# Check for unescaped output: <?= $var ?> or echo $var without escape()
|
|
||||||
UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true)
|
|
||||||
if [ -n "$UNESCAPED" ]; then
|
|
||||||
HITS=$(echo "$UNESCAPED" | wc -l)
|
|
||||||
echo "- \`${FILE}\`: ${HITS} unescaped \`<?= \$var ?>\` output(s) — use \`<?= \$this->escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + HITS))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for echo without escaping in template context
|
|
||||||
RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true)
|
|
||||||
if [ -n "$RAW_ECHO" ]; then
|
|
||||||
HITS=$(echo "$RAW_ECHO" | wc -l)
|
|
||||||
echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + HITS))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Namespace consistency check
|
|
||||||
run: |
|
|
||||||
echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
# Find component/plugin manifests with <namespace> tags
|
|
||||||
MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<namespace' {} \; 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -z "$MANIFESTS" ]; then
|
|
||||||
echo "No manifests with \`<namespace>\` found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
for MANIFEST in $MANIFESTS; do
|
|
||||||
NS_PATH=$(grep -oP '<namespace[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
[ -z "$NS_PATH" ] && continue
|
|
||||||
MANIFEST_DIR=$(dirname "$MANIFEST")
|
|
||||||
|
|
||||||
echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# Check PHP files have matching namespace
|
|
||||||
while IFS= read -r -d '' PHP_FILE; do
|
|
||||||
FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1)
|
|
||||||
[ -z "$FILE_NS" ] && continue
|
|
||||||
|
|
||||||
# Namespace should start with the manifest namespace path
|
|
||||||
if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then
|
|
||||||
echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null)
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SPDX license header check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY
|
|
||||||
MISSING=0
|
|
||||||
|
|
||||||
SRC_DIR=""
|
|
||||||
for DIR in source/ src/ htdocs/; do
|
|
||||||
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$SRC_DIR" ]; then
|
|
||||||
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
TOTAL=0
|
|
||||||
while IFS= read -r -d '' FILE; do
|
|
||||||
TOTAL=$((TOTAL + 1))
|
|
||||||
if ! head -10 "$FILE" | grep -qi "SPDX"; then
|
|
||||||
echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
MISSING=$((MISSING + 1))
|
|
||||||
fi
|
|
||||||
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0)
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$MISSING" -gt 0 ]; then
|
|
||||||
echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Service provider check
|
|
||||||
run: |
|
|
||||||
echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
|
||||||
if [ -z "$PROVIDERS" ]; then
|
|
||||||
echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
for FILE in $PROVIDERS; do
|
|
||||||
# Must return a ServiceProviderInterface
|
|
||||||
if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then
|
|
||||||
echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Must have return statement
|
|
||||||
if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then
|
|
||||||
echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
release-readiness:
|
release-readiness:
|
||||||
name: Release Readiness Check
|
name: Release Readiness Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: "Publish to Composer"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
- '[0-9]*.[0-9]*.[0-9]*'
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Publish Package
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip publish]')
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-dev --no-interaction --prefer-dist --quiet
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Package version: ${VERSION}"
|
|
||||||
|
|
||||||
# Gitea Composer Registry — auto-publishes from tags
|
|
||||||
# The tag push itself registers the package at:
|
|
||||||
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
|
|
||||||
- name: Verify Gitea registry
|
|
||||||
run: |
|
|
||||||
echo "Gitea Composer registry auto-publishes from tags."
|
|
||||||
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
|
|
||||||
echo "Install: composer require mokoconsulting/mokocli"
|
|
||||||
|
|
||||||
# Packagist — notify of new version
|
|
||||||
- name: Notify Packagist
|
|
||||||
if: secrets.PACKAGIST_TOKEN != ''
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
echo "Notifying Packagist of version ${VERSION}..."
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
|
|
||||||
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
|
|
||||||
&& echo "Packagist notified" \
|
|
||||||
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -25,6 +25,10 @@
|
|||||||
name: "Universal: Secret Scanning"
|
name: "Universal: Secret Scanning"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 01.00.00
|
# VERSION: 02.46.32
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: mokocli.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
@@ -96,32 +96,6 @@ jobs:
|
|||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Secret Scanning ──────────────────────────────────────────────────
|
|
||||||
gitleaks:
|
|
||||||
name: Secret Scan
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Install Gitleaks
|
|
||||||
run: |
|
|
||||||
GITLEAKS_VERSION="8.21.2"
|
|
||||||
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
|
||||||
| tar -xz -C /usr/local/bin gitleaks
|
|
||||||
|
|
||||||
- name: Scan PR commits for secrets
|
|
||||||
run: |
|
|
||||||
if gitleaks detect --source . --verbose \
|
|
||||||
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
|
|
||||||
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "::error::Potential secrets detected in PR commits"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: MokoPlatform.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Universal
|
# INGROUP: MokoPlatform.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
||||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
||||||
# VERSION: 01.01.00
|
# VERSION: 01.01.00
|
||||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
||||||
@@ -45,16 +45,16 @@ jobs:
|
|||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
echo "Platform: ${PLATFORM:-all}"
|
echo "Platform: ${PLATFORM:-all}"
|
||||||
|
|
||||||
- name: Clone mokocli
|
- name: Clone mokoplatform
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokoplatform.git" /tmp/mokoplatform
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
cd /tmp/mokocli
|
cd /tmp/mokoplatform
|
||||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
|
||||||
- name: Run workflow sync
|
- name: Run workflow sync
|
||||||
@@ -70,4 +70,4 @@ jobs:
|
|||||||
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
php /tmp/mokoplatform/cli/workflow_sync.php ${ARGS}
|
||||||
|
|||||||
+5
-5
@@ -14,17 +14,13 @@
|
|||||||
INGROUP: MokoSuiteClient.Documentation
|
INGROUP: MokoSuiteClient.Documentation
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
PATH: ./CHANGELOG.md
|
PATH: ./CHANGELOG.md
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
BRIEF: Version history using `Keep a Changelog`
|
BRIEF: Version history using `Keep a Changelog`
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [02.45.00] --- 2026-06-20
|
|
||||||
|
|
||||||
## [02.45.00] --- 2026-06-20
|
|
||||||
|
|
||||||
## [02.44.00] --- 2026-06-20
|
## [02.44.00] --- 2026-06-20
|
||||||
|
|
||||||
## [02.44.00] --- 2026-06-20
|
## [02.44.00] --- 2026-06-20
|
||||||
@@ -32,3 +28,7 @@
|
|||||||
## [02.43.00] --- 2026-06-20
|
## [02.43.00] --- 2026-06-20
|
||||||
|
|
||||||
## [02.43.00] --- 2026-06-20
|
## [02.43.00] --- 2026-06-20
|
||||||
|
|
||||||
|
## [02.42.00] --- 2026-06-20
|
||||||
|
|
||||||
|
## [02.42.00] --- 2026-06-20
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Documentation
|
INGROUP: MokoSuiteClient.Documentation
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: ./CODE_OF_CONDUCT.md
|
PATH: ./CODE_OF_CONDUCT.md
|
||||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||||
-->
|
-->
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
DEFGROUP: mokoconsulting-tech.MokoSuiteClientBrand
|
DEFGROUP: mokoconsulting-tech.MokoSuiteClientBrand
|
||||||
INGROUP: MokoStandards.Governance
|
INGROUP: MokoStandards.Governance
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClientBrand
|
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClientBrand
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /GOVERNANCE.md
|
PATH: /GOVERNANCE.md
|
||||||
BRIEF: Project governance rules, roles, and decision process for MokoSuiteClientBrand
|
BRIEF: Project governance rules, roles, and decision process for MokoSuiteClientBrand
|
||||||
-->
|
-->
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
INGROUP: MokoSuiteClient.Documentation
|
INGROUP: MokoSuiteClient.Documentation
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
PATH: ./LICENSE.md
|
PATH: ./LICENSE.md
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
BRIEF: Project license (GPL-3.0-or-later)
|
BRIEF: Project license (GPL-3.0-or-later)
|
||||||
-->
|
-->
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient
|
INGROUP: MokoSuiteClient
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /README.md
|
PATH: /README.md
|
||||||
BRIEF: MokoSuiteClient platform plugin for Joomla
|
BRIEF: MokoSuiteClient platform plugin for Joomla
|
||||||
-->
|
-->
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
|||||||
INGROUP: [PROJECT_NAME].Documentation
|
INGROUP: [PROJECT_NAME].Documentation
|
||||||
REPO: [REPOSITORY_URL]
|
REPO: [REPOSITORY_URL]
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@
|
|||||||
INGROUP: MokoSuiteClient.Build
|
INGROUP: MokoSuiteClient.Build
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
FILE: build-guide.md
|
FILE: build-guide.md
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/
|
PATH: /docs/guides/
|
||||||
BRIEF: Build and packaging guide for the MokoSuiteClient system plugin
|
BRIEF: Build and packaging guide for the MokoSuiteClient system plugin
|
||||||
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
|
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Build Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Build Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## 1. Purpose
|
## 1. Purpose
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Guides
|
INGROUP: MokoSuiteClient.Guides
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/configuration-guide.md
|
PATH: /docs/guides/configuration-guide.md
|
||||||
BRIEF: Configuration guide for the MokoSuiteClient system plugin
|
BRIEF: Configuration guide for the MokoSuiteClient system plugin
|
||||||
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
|
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Configuration Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Configuration Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## 1. Objective
|
## 1. Objective
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Guides
|
INGROUP: MokoSuiteClient.Guides
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/installation-guide.md
|
PATH: /docs/guides/installation-guide.md
|
||||||
BRIEF: Installation guide for the MokoSuiteClient system plugin
|
BRIEF: Installation guide for the MokoSuiteClient system plugin
|
||||||
NOTE: First document in the guide set
|
NOTE: First document in the guide set
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Installation Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Installation Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Guides
|
INGROUP: MokoSuiteClient.Guides
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/operations-guide.md
|
PATH: /docs/guides/operations-guide.md
|
||||||
BRIEF: Operational guide for administering and managing the MokoSuiteClient system plugin
|
BRIEF: Operational guide for administering and managing the MokoSuiteClient system plugin
|
||||||
NOTE: Defines lifecycle, responsibilities, and operational behaviors
|
NOTE: Defines lifecycle, responsibilities, and operational behaviors
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Operations Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Operations Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Guides
|
INGROUP: MokoSuiteClient.Guides
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/rollback-and-recovery-guide.md
|
PATH: /docs/guides/rollback-and-recovery-guide.md
|
||||||
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
|
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
|
||||||
NOTE: Completes the core guide set for Suite plugin governance
|
NOTE: Completes the core guide set for Suite plugin governance
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Rollback and Recovery Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Guides
|
INGROUP: MokoSuiteClient.Guides
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/testing-guide.md
|
PATH: /docs/guides/testing-guide.md
|
||||||
BRIEF: Testing guide for MokoSuiteClient v02.01.08
|
BRIEF: Testing guide for MokoSuiteClient v02.01.08
|
||||||
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
|
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Testing Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Testing Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## 1. Prerequisites
|
## 1. Prerequisites
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Guides
|
INGROUP: MokoSuiteClient.Guides
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/troubleshooting-guide.md
|
PATH: /docs/guides/troubleshooting-guide.md
|
||||||
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuiteClient plugin
|
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoSuiteClient plugin
|
||||||
NOTE: Designed for administrators and Suite operations teams
|
NOTE: Designed for administrators and Suite operations teams
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Troubleshooting Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Guides
|
INGROUP: MokoSuiteClient.Guides
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/guides/upgrade-and-versioning-guide.md
|
PATH: /docs/guides/upgrade-and-versioning-guide.md
|
||||||
BRIEF: Guide for updating, versioning, and maintaining the MokoSuiteClient plugin
|
BRIEF: Guide for updating, versioning, and maintaining the MokoSuiteClient plugin
|
||||||
NOTE: Defines release flow, version rules, and upgrade validation
|
NOTE: Defines release flow, version rules, and upgrade validation
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.45.00)
|
# MokoSuiteClient Upgrade and Versioning Guide (VERSION: 02.46.32)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -10,13 +10,13 @@
|
|||||||
DEFGROUP: Joomla.Plugin
|
DEFGROUP: Joomla.Plugin
|
||||||
INGROUP: MokoSuiteClient.Documentation
|
INGROUP: MokoSuiteClient.Documentation
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
PATH: /docs/index.md
|
PATH: /docs/index.md
|
||||||
BRIEF: Master index of all documentation for the MokoSuiteClient plugin
|
BRIEF: Master index of all documentation for the MokoSuiteClient plugin
|
||||||
NOTE: Automatically maintained index for all guide canvases
|
NOTE: Automatically maintained index for all guide canvases
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Documentation Index (VERSION: 02.45.00)
|
# MokoSuiteClient Documentation Index (VERSION: 02.46.32)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
INGROUP: MokoSuiteClient
|
INGROUP: MokoSuiteClient
|
||||||
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
PATH: /docs/plugin-basic.md
|
PATH: /docs/plugin-basic.md
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
BRIEF: Baseline documentation for the MokoSuiteClient system plugin
|
BRIEF: Baseline documentation for the MokoSuiteClient system plugin
|
||||||
NOTE: Foundational reference for internal and external stakeholders
|
NOTE: Foundational reference for internal and external stakeholders
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# MokoSuiteClient Plugin Overview (VERSION: 02.45.00)
|
# MokoSuiteClient Plugin Overview (VERSION: 02.46.32)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ DEFGROUP: MokoSuiteClient.Documentation
|
|||||||
INGROUP: MokoStandards.Templates
|
INGROUP: MokoStandards.Templates
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClient
|
REPO: https://github.com/mokoconsulting-tech/MokoSuiteClient
|
||||||
PATH: /docs/update-server.md
|
PATH: /docs/update-server.md
|
||||||
VERSION: 02.45.00
|
VERSION: 02.46.32
|
||||||
BRIEF: How this extension's Joomla update server file (update.xml) is managed
|
BRIEF: How this extension's Joomla update server file (update.xml) is managed
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOSUITECLIENT="MokoSuiteClient"
|
COM_MOKOSUITECLIENT="MokoSuite"
|
||||||
COM_MOKOSUITECLIENT_DESCRIPTION="MokoSuiteClient admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management."
|
COM_MOKOSUITECLIENT_DESCRIPTION="MokoSuiteClient admin dashboard and REST API. Control panel for managing site features, health monitoring, and remote management."
|
||||||
COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuiteClient Control Panel"
|
COM_MOKOSUITECLIENT_DASHBOARD_TITLE="MokoSuiteClient Control Panel"
|
||||||
COM_MOKOSUITECLIENT_MENU_DASHBOARD="Dashboard"
|
COM_MOKOSUITECLIENT_MENU_DASHBOARD="Dashboard"
|
||||||
|
|||||||
@@ -215,3 +215,15 @@ INSERT IGNORE INTO `#__mokosuiteclient_retention_policies` (`id`, `content_type`
|
|||||||
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
|
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
|
||||||
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
|
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- License Cache — stores MokoGitea validation results
|
||||||
|
-- ============================================================
|
||||||
|
CREATE TABLE IF NOT EXISTS `#__mokosuite_license_cache` (
|
||||||
|
`dlid_hash` CHAR(64) NOT NULL COMMENT 'SHA-256 of DLID (never store raw DLID)',
|
||||||
|
`response_data` TEXT NOT NULL COMMENT 'JSON validation response from MokoGitea',
|
||||||
|
`checked_at` DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY (`dlid_hash`),
|
||||||
|
KEY `idx_checked` (`checked_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class HtmlView extends BaseHtmlView
|
|||||||
protected $wafChartData = [];
|
protected $wafChartData = [];
|
||||||
protected $loginChartData = [];
|
protected $loginChartData = [];
|
||||||
protected $mokoExtensions = [];
|
protected $mokoExtensions = [];
|
||||||
|
public $supportPin = '';
|
||||||
|
|
||||||
public function display($tpl = null)
|
public function display($tpl = null)
|
||||||
{
|
{
|
||||||
@@ -33,6 +34,28 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
$this->plugins = $model->getFeaturePlugins();
|
$this->plugins = $model->getFeaturePlugins();
|
||||||
$this->siteInfo = $model->getSiteInfo();
|
$this->siteInfo = $model->getSiteInfo();
|
||||||
|
|
||||||
|
// Daily support PIN from health token
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$db = \Joomla\CMS\Factory::getDbo();
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select($db->quoteName('params'))
|
||||||
|
->from($db->quoteName('#__extensions'))
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuiteclient'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||||
|
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||||
|
);
|
||||||
|
$token = (json_decode((string) $db->loadResult()))->health_api_token ?? '';
|
||||||
|
|
||||||
|
if (!empty($token))
|
||||||
|
{
|
||||||
|
$hash = hash_hmac('sha256', gmdate('Y-m-d'), $token);
|
||||||
|
$this->supportPin = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (\Throwable $e) {}
|
||||||
$this->recentLogins = $model->getRecentLogins(5);
|
$this->recentLogins = $model->getRecentLogins(5);
|
||||||
$this->pendingUpdates = $model->getPendingUpdates();
|
$this->pendingUpdates = $model->getPendingUpdates();
|
||||||
$this->checkedOutItems = $model->getCheckedOutItems();
|
$this->checkedOutItems = $model->getCheckedOutItems();
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
|
|||||||
<span class="mokosuiteclient-info-label">MokoSuiteClient</span>
|
<span class="mokosuiteclient-info-label">MokoSuiteClient</span>
|
||||||
<span class="mokosuiteclient-info-value"><span class="badge bg-primary"><?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span></span>
|
<span class="mokosuiteclient-info-value"><span class="badge bg-primary"><?php echo $this->escape($siteInfo->mokosuiteclient_version); ?></span></span>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (!empty($this->supportPin)): ?>
|
||||||
|
<div class="mokosuiteclient-info-item">
|
||||||
|
<span class="mokosuiteclient-info-label">Support PIN</span>
|
||||||
|
<span class="mokosuiteclient-info-value"><span class="badge bg-dark" style="font-family:monospace;letter-spacing:0.08em;cursor:help;" title="Daily verification PIN — rotates at midnight UTC. Ask your provider for this code to verify identity."><span class="icon-key small me-1" aria-hidden="true"></span><?php echo $this->escape($this->supportPin); ?></span></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<div class="mokosuiteclient-info-item">
|
<div class="mokosuiteclient-info-item">
|
||||||
<span class="mokosuiteclient-info-label">Joomla</span>
|
<span class="mokosuiteclient-info-label">Joomla</span>
|
||||||
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->joomla_version); ?></span></span>
|
<span class="mokosuiteclient-info-value"><span class="badge bg-secondary"><?php echo $this->escape($siteInfo->joomla_version); ?></span></span>
|
||||||
@@ -311,7 +317,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-muted"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td>
|
<td class="text-muted"><?php echo $this->escape(mb_substr($item->title, 0, 30)); ?></td>
|
||||||
<td class="text-muted"><?php echo $this->escape($item->username ?? ''); ?></td>
|
<td class="text-muted"><?php echo $this->escape($item->username ?? ''); ?></td>
|
||||||
<td class="text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, 'M d H:i'); ?></td>
|
<td class="text-muted"><?php echo HTMLHelper::_('date', $item->checked_out_time, Text::_('DATE_FORMAT_LC4')); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -342,7 +348,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-muted"><code><?php echo $this->escape($block->ip); ?></code></td>
|
<td class="text-muted"><code><?php echo $this->escape($block->ip); ?></code></td>
|
||||||
<td class="text-muted"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td>
|
<td class="text-muted"><span class="badge bg-danger"><?php echo $this->escape($block->rule); ?></span></td>
|
||||||
<td class="text-muted"><?php echo HTMLHelper::_('date', $block->created, 'M d H:i'); ?></td>
|
<td class="text-muted"><?php echo HTMLHelper::_('date', $block->created, Text::_('DATE_FORMAT_LC4')); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -369,7 +375,7 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-muted"><?php echo $this->escape($login->username ?? ''); ?></td>
|
<td class="text-muted"><?php echo $this->escape($login->username ?? ''); ?></td>
|
||||||
<td class="text-muted"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td>
|
<td class="text-muted"><code><?php echo $this->escape($login->ip_address ?? ''); ?></code></td>
|
||||||
<td class="text-muted"><?php echo HTMLHelper::_('date', $login->log_date, 'M d H:i'); ?></td>
|
<td class="text-muted"><?php echo HTMLHelper::_('date', $login->log_date, Text::_('DATE_FORMAT_LC4')); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ defined('_JEXEC') or die;
|
|||||||
|
|
||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
use Joomla\CMS\HTML\HTMLHelper;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Session\Session;
|
use Joomla\CMS\Session\Session;
|
||||||
|
|
||||||
@@ -140,8 +141,8 @@ $typeBadge = [
|
|||||||
<td><?php echo $this->escape($r->user_name ?? ''); ?><br><small class="text-muted"><?php echo $this->escape($r->user_email ?? ''); ?></small></td>
|
<td><?php echo $this->escape($r->user_name ?? ''); ?><br><small class="text-muted"><?php echo $this->escape($r->user_email ?? ''); ?></small></td>
|
||||||
<td><span class="badge <?php echo $typeBadge[$r->type] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->type); ?></span></td>
|
<td><span class="badge <?php echo $typeBadge[$r->type] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->type); ?></span></td>
|
||||||
<td><span class="badge <?php echo $statusBadge[$r->status] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->status); ?></span></td>
|
<td><span class="badge <?php echo $statusBadge[$r->status] ?? 'bg-secondary'; ?>"><?php echo ucfirst($r->status); ?></span></td>
|
||||||
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $r->created, 'M d, Y H:i'); ?></td>
|
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $r->created, Text::_('DATE_FORMAT_LC2')); ?></td>
|
||||||
<td class="text-nowrap small"><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, 'M d, Y H:i') : '—'; ?></td>
|
<td class="text-nowrap small"><?php echo $r->processed ? HTMLHelper::_('date', $r->processed, Text::_('DATE_FORMAT_LC2')) : '—'; ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ($r->status === 'pending'): ?>
|
<?php if ($r->status === 'pending'): ?>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
use Joomla\CMS\HTML\HTMLHelper;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Session\Session;
|
use Joomla\CMS\Session\Session;
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ $priorities = $this->priorities ?? [];
|
|||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<strong><?php echo $this->escape($t->created_by_name); ?></strong>
|
<strong><?php echo $this->escape($t->created_by_name); ?></strong>
|
||||||
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?></small>
|
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $t->created, Text::_('DATE_FORMAT_LC2')); ?></small>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge bg-dark">Original</span>
|
<span class="badge bg-dark">Original</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +60,7 @@ $priorities = $this->priorities ?? [];
|
|||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<strong><?php echo $this->escape($reply->user_name ?? 'System'); ?></strong>
|
<strong><?php echo $this->escape($reply->user_name ?? 'System'); ?></strong>
|
||||||
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $reply->created, 'M d, Y H:i'); ?></small>
|
<small class="text-muted ms-2"><?php echo HTMLHelper::_('date', $reply->created, Text::_('DATE_FORMAT_LC2')); ?></small>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($reply->is_internal): ?>
|
<?php if ($reply->is_internal): ?>
|
||||||
<span class="badge bg-warning text-dark">Internal Note</span>
|
<span class="badge bg-warning text-dark">Internal Note</span>
|
||||||
@@ -147,9 +148,9 @@ $priorities = $this->priorities ?? [];
|
|||||||
<?php if (!empty($t->contact_phone)): ?><br><small><?php echo $this->escape($t->contact_phone); ?></small><?php endif; ?>
|
<?php if (!empty($t->contact_phone)): ?><br><small><?php echo $this->escape($t->contact_phone); ?></small><?php endif; ?>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<tr><td class="text-muted">Created</td><td><?php echo HTMLHelper::_('date', $t->created, 'M d, Y H:i'); ?></td></tr>
|
<tr><td class="text-muted">Created</td><td><?php echo HTMLHelper::_('date', $t->created, Text::_('DATE_FORMAT_LC2')); ?></td></tr>
|
||||||
<?php if ($t->resolved): ?><tr><td class="text-muted">Resolved</td><td><?php echo HTMLHelper::_('date', $t->resolved, 'M d, Y H:i'); ?></td></tr><?php endif; ?>
|
<?php if ($t->resolved): ?><tr><td class="text-muted">Resolved</td><td><?php echo HTMLHelper::_('date', $t->resolved, Text::_('DATE_FORMAT_LC2')); ?></td></tr><?php endif; ?>
|
||||||
<?php if ($t->closed): ?><tr><td class="text-muted">Closed</td><td><?php echo HTMLHelper::_('date', $t->closed, 'M d, Y H:i'); ?></td></tr><?php endif; ?>
|
<?php if ($t->closed): ?><tr><td class="text-muted">Closed</td><td><?php echo HTMLHelper::_('date', $t->closed, Text::_('DATE_FORMAT_LC2')); ?></td></tr><?php endif; ?>
|
||||||
<tr><td class="text-muted">Replies</td><td><?php echo $t->reply_count; ?></td></tr>
|
<tr><td class="text-muted">Replies</td><td><?php echo $t->reply_count; ?></td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,7 +168,7 @@ $priorities = $this->priorities ?? [];
|
|||||||
$responseOverdue = !$t->sla_responded && strtotime($t->sla_response_due) < time();
|
$responseOverdue = !$t->sla_responded && strtotime($t->sla_response_due) < time();
|
||||||
?>
|
?>
|
||||||
<span class="<?php echo $t->sla_responded ? 'text-success' : ($responseOverdue ? 'text-danger fw-bold' : ''); ?>">
|
<span class="<?php echo $t->sla_responded ? 'text-success' : ($responseOverdue ? 'text-danger fw-bold' : ''); ?>">
|
||||||
<?php echo $t->sla_responded ? 'Responded' : HTMLHelper::_('date', $t->sla_response_due, 'M d H:i'); ?>
|
<?php echo $t->sla_responded ? 'Responded' : HTMLHelper::_('date', $t->sla_response_due, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
<?php echo $responseOverdue ? ' OVERDUE' : ''; ?>
|
<?php echo $responseOverdue ? ' OVERDUE' : ''; ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,7 +180,7 @@ $priorities = $this->priorities ?? [];
|
|||||||
$resolutionOverdue = !!empty($t->status_is_closed) && strtotime($t->sla_resolution_due) < time();
|
$resolutionOverdue = !!empty($t->status_is_closed) && strtotime($t->sla_resolution_due) < time();
|
||||||
?>
|
?>
|
||||||
<span class="<?php echo !empty($t->status_is_closed) ? 'text-success' : ($resolutionOverdue ? 'text-danger fw-bold' : ''); ?>">
|
<span class="<?php echo !empty($t->status_is_closed) ? 'text-success' : ($resolutionOverdue ? 'text-danger fw-bold' : ''); ?>">
|
||||||
<?php echo !empty($t->status_is_closed) ? 'Met' : HTMLHelper::_('date', $t->sla_resolution_due, 'M d H:i'); ?>
|
<?php echo !empty($t->status_is_closed) ? 'Met' : HTMLHelper::_('date', $t->sla_resolution_due, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
<?php echo $resolutionOverdue ? ' OVERDUE' : ''; ?>
|
<?php echo $resolutionOverdue ? ' OVERDUE' : ''; ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ $token = Session::getFormToken();
|
|||||||
echo '<em>Unassigned</em>';
|
echo '<em>Unassigned</em>';
|
||||||
}
|
}
|
||||||
?></td>
|
?></td>
|
||||||
<td class="small"><?php echo HTMLHelper::_('date', $t->created, 'M d H:i'); ?></td>
|
<td class="small"><?php echo HTMLHelper::_('date', $t->created, Text::_('DATE_FORMAT_LC4')); ?></td>
|
||||||
<td class="small">
|
<td class="small">
|
||||||
<?php if ($t->sla_response_due && !$t->sla_responded): ?>
|
<?php if ($t->sla_response_due && !$t->sla_responded): ?>
|
||||||
<span title="Response due"><?php echo HTMLHelper::_('date', $t->sla_response_due, 'M d H:i'); ?></span>
|
<span title="Response due"><?php echo HTMLHelper::_('date', $t->sla_response_due, Text::_('DATE_FORMAT_LC4')); ?></span>
|
||||||
<?php elseif ($t->sla_resolution_due): ?>
|
<?php elseif ($t->sla_resolution_due): ?>
|
||||||
<span title="Resolution due"><?php echo HTMLHelper::_('date', $t->sla_resolution_due, 'M d H:i'); ?></span>
|
<span title="Resolution due"><?php echo HTMLHelper::_('date', $t->sla_resolution_due, Text::_('DATE_FORMAT_LC4')); ?></span>
|
||||||
<?php else: ?>—<?php endif; ?>
|
<?php else: ?>—<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ defined('_JEXEC') or die;
|
|||||||
|
|
||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
use Joomla\CMS\HTML\HTMLHelper;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Session\Session;
|
use Joomla\CMS\Session\Session;
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ $ruleBadge = [
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($logs as $log): ?>
|
<?php foreach ($logs as $log): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $log->created, 'M d H:i:s'); ?></td>
|
<td class="text-nowrap small"><?php echo HTMLHelper::_('date', $log->created, Text::_('DATE_FORMAT_LC4')); ?></td>
|
||||||
<td><code><?php echo htmlspecialchars($log->ip); ?></code></td>
|
<td><code><?php echo htmlspecialchars($log->ip); ?></code></td>
|
||||||
<td><span class="badge <?php echo $ruleBadge[$log->rule] ?? 'bg-secondary'; ?>"><?php echo htmlspecialchars($log->rule); ?></span></td>
|
<td><span class="badge <?php echo $ruleBadge[$log->rule] ?? 'bg-secondary'; ?>"><?php echo htmlspecialchars($log->rule); ?></span></td>
|
||||||
<td class="small" style="max-width:250px;overflow:hidden;text-overflow:ellipsis" title="<?php echo htmlspecialchars($log->uri); ?>"><?php echo htmlspecialchars(mb_substr($log->uri, 0, 60)); ?></td>
|
<td class="small" style="max-width:250px;overflow:hidden;text-overflow:ellipsis" title="<?php echo htmlspecialchars($log->uri); ?>"><?php echo htmlspecialchars(mb_substr($log->uri, 0, 60)); ?></td>
|
||||||
@@ -148,7 +149,7 @@ $ruleBadge = [
|
|||||||
<tr>
|
<tr>
|
||||||
<td><code class="small"><?php echo htmlspecialchars($tip->ip); ?></code></td>
|
<td><code class="small"><?php echo htmlspecialchars($tip->ip); ?></code></td>
|
||||||
<td class="fw-bold"><?php echo $tip->cnt; ?></td>
|
<td class="fw-bold"><?php echo $tip->cnt; ?></td>
|
||||||
<td class="small text-nowrap"><?php echo HTMLHelper::_('date', $tip->last_seen, 'M d'); ?></td>
|
<td class="small text-nowrap"><?php echo HTMLHelper::_('date', $tip->last_seen, Text::_('DATE_FORMAT_LC4')); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($tip->ip); ?>"
|
<button type="button" class="btn btn-sm btn-outline-danger btn-ban-ip" data-ip="<?php echo htmlspecialchars($tip->ip); ?>"
|
||||||
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.banIpFromLog&format=json'); ?>"
|
data-url="<?php echo Route::_('index.php?option=com_mokosuiteclient&task=display.banIpFromLog&format=json'); ?>"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>MokoSuiteClient admin dashboard and REST API. Provides a control panel for managing MokoSuiteClient feature plugins, site health monitoring, and remote management endpoints.</description>
|
<description>MokoSuiteClient admin dashboard and REST API. Provides a control panel for managing MokoSuiteClient feature plugins, site health monitoring, and remote management endpoints.</description>
|
||||||
|
|
||||||
<namespace path="src">Moko\Component\MokoSuiteClient</namespace>
|
<namespace path="src">Moko\Component\MokoSuiteClient</namespace>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>MOD_MOKOSUITECLIENT_CACHE_DESC</description>
|
<description>MOD_MOKOSUITECLIENT_CACHE_DESC</description>
|
||||||
<namespace path="src">Moko\Module\MokoSuiteCache</namespace>
|
<namespace path="src">Moko\Module\MokoSuiteCache</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ namespace Moko\Module\MokoSuiteClientCache\Administrator\Dispatcher;
|
|||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
|
use Joomla\CMS\Dispatcher\AbstractModuleDispatcher;
|
||||||
|
use Joomla\CMS\Uri\Uri;
|
||||||
|
|
||||||
class Dispatcher extends AbstractModuleDispatcher
|
class Dispatcher extends AbstractModuleDispatcher
|
||||||
{
|
{
|
||||||
protected function getLayoutData()
|
protected function getLayoutData()
|
||||||
{
|
{
|
||||||
return parent::getLayoutData();
|
$data = parent::getLayoutData();
|
||||||
|
$data['domain'] = parse_url(Uri::root(), PHP_URL_HOST) ?: '';
|
||||||
|
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use Joomla\CMS\Session\Session;
|
|||||||
$token = Session::getFormToken();
|
$token = Session::getFormToken();
|
||||||
$cacheUrl = 'index.php?option=com_mokosuiteclient&task=clearCache&format=json';
|
$cacheUrl = 'index.php?option=com_mokosuiteclient&task=clearCache&format=json';
|
||||||
$tempUrl = 'index.php?option=com_mokosuiteclient&task=clearTemp&format=json';
|
$tempUrl = 'index.php?option=com_mokosuiteclient&task=clearTemp&format=json';
|
||||||
|
$domain = $domain ?? '';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -21,9 +22,15 @@ $tempUrl = 'index.php?option=com_mokosuiteclient&task=clearTemp&format=json';
|
|||||||
.mokosuiteclient-cleaner-btn { cursor:pointer; padding:0.2rem 0.5rem; font-size:0.8rem; border-radius:3px; text-decoration:none; color:var(--template-text-dark,#495057); transition:background 0.15s; white-space:nowrap; }
|
.mokosuiteclient-cleaner-btn { cursor:pointer; padding:0.2rem 0.5rem; font-size:0.8rem; border-radius:3px; text-decoration:none; color:var(--template-text-dark,#495057); transition:background 0.15s; white-space:nowrap; }
|
||||||
.mokosuiteclient-cleaner-btn:hover { background:rgba(0,0,0,0.08); color:var(--template-text-dark,#212529); text-decoration:none; }
|
.mokosuiteclient-cleaner-btn:hover { background:rgba(0,0,0,0.08); color:var(--template-text-dark,#212529); text-decoration:none; }
|
||||||
.mokosuiteclient-cleaner-sep { color:var(--template-text-dark,#adb5bd); padding:0 0.1rem; font-size:0.8rem; }
|
.mokosuiteclient-cleaner-sep { color:var(--template-text-dark,#adb5bd); padding:0 0.1rem; font-size:0.8rem; }
|
||||||
|
.mokosuiteclient-domain { font-family:monospace; font-size:0.75rem; color:var(--template-text-dark,#6c757d); cursor:pointer; padding:0.15rem 0.4rem; border-radius:3px; transition:background 0.15s; }
|
||||||
|
.mokosuiteclient-domain:hover { background:rgba(0,0,0,0.06); }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="header-item-content mokosuiteclient-cleaner">
|
<div class="header-item-content mokosuiteclient-cleaner">
|
||||||
|
<?php if ($domain): ?>
|
||||||
|
<span class="mokosuiteclient-domain" id="mokosuiteclient-domain" title="Support key — click to copy"><?php echo htmlspecialchars($domain); ?></span>
|
||||||
|
<span class="mokosuiteclient-cleaner-sep">|</span>
|
||||||
|
<?php endif; ?>
|
||||||
<span class="mokosuiteclient-cleaner-label">Clear:</span>
|
<span class="mokosuiteclient-cleaner-label">Clear:</span>
|
||||||
<a href="#" class="mokosuiteclient-cleaner-btn" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache">
|
<a href="#" class="mokosuiteclient-cleaner-btn" id="mokosuiteclient-clear-cache" title="Clear all Joomla cache">
|
||||||
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache
|
<span class="icon-bolt" aria-hidden="true" id="mokosuiteclient-cache-icon"></span> Cache
|
||||||
@@ -85,5 +92,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
setupCleaner('mokosuiteclient-clear-cache', 'mokosuiteclient-cache-icon', '<?php echo $cacheUrl; ?>', '<?php echo $token; ?>');
|
setupCleaner('mokosuiteclient-clear-cache', 'mokosuiteclient-cache-icon', '<?php echo $cacheUrl; ?>', '<?php echo $token; ?>');
|
||||||
setupCleaner('mokosuiteclient-clear-temp', 'mokosuiteclient-temp-icon', '<?php echo $tempUrl; ?>', '<?php echo $token; ?>');
|
setupCleaner('mokosuiteclient-clear-temp', 'mokosuiteclient-temp-icon', '<?php echo $tempUrl; ?>', '<?php echo $token; ?>');
|
||||||
|
|
||||||
|
// Click-to-copy domain
|
||||||
|
var domainEl = document.getElementById('mokosuiteclient-domain');
|
||||||
|
if (domainEl) {
|
||||||
|
domainEl.addEventListener('click', function() {
|
||||||
|
navigator.clipboard.writeText(domainEl.textContent.trim()).then(function() {
|
||||||
|
var orig = domainEl.textContent;
|
||||||
|
domainEl.textContent = 'Copied!';
|
||||||
|
setTimeout(function() { domainEl.textContent = orig; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>MOD_MOKOSUITECLIENT_CATEGORIES_DESC</description>
|
<description>MOD_MOKOSUITECLIENT_CATEGORIES_DESC</description>
|
||||||
<namespace path="src">Moko\Module\MokoSuiteClientCategories</namespace>
|
<namespace path="src">Moko\Module\MokoSuiteClientCategories</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>MOD_MOKOSUITECLIENT_CPANEL_DESC</description>
|
<description>MOD_MOKOSUITECLIENT_CPANEL_DESC</description>
|
||||||
<namespace path="src">Moko\Module\MokoSuiteCpanel</namespace>
|
<namespace path="src">Moko\Module\MokoSuiteCpanel</namespace>
|
||||||
|
|
||||||
@@ -28,14 +28,6 @@
|
|||||||
label="MOD_MOKOSUITECLIENT_CPANEL_FIELDSET_DISPLAY"
|
label="MOD_MOKOSUITECLIENT_CPANEL_FIELDSET_DISPLAY"
|
||||||
description="MOD_MOKOSUITECLIENT_CPANEL_FIELDSET_DISPLAY_DESC">
|
description="MOD_MOKOSUITECLIENT_CPANEL_FIELDSET_DISPLAY_DESC">
|
||||||
|
|
||||||
<field name="collapsed" type="radio" default="1"
|
|
||||||
label="MOD_MOKOSUITECLIENT_CPANEL_COLLAPSED_LABEL"
|
|
||||||
description="MOD_MOKOSUITECLIENT_CPANEL_COLLAPSED_DESC"
|
|
||||||
layout="joomla.form.field.radio.switcher">
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
<field name="show_health" type="radio" default="1"
|
<field name="show_health" type="radio" default="1"
|
||||||
label="MOD_MOKOSUITECLIENT_CPANEL_SHOW_HEALTH_LABEL"
|
label="MOD_MOKOSUITECLIENT_CPANEL_SHOW_HEALTH_LABEL"
|
||||||
layout="joomla.form.field.radio.switcher">
|
layout="joomla.form.field.radio.switcher">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
|
|||||||
$data['currentIp'] = $helper->getCurrentIp();
|
$data['currentIp'] = $helper->getCurrentIp();
|
||||||
$data['ssl'] = $helper->getSslStatus();
|
$data['ssl'] = $helper->getSslStatus();
|
||||||
|
|
||||||
// Support PIN derived from health token
|
// Daily support PIN derived from health token + today's date (UTC)
|
||||||
$data['supportPin'] = '';
|
$data['supportPin'] = '';
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -65,7 +65,9 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
|
|||||||
|
|
||||||
if (!empty($token))
|
if (!empty($token))
|
||||||
{
|
{
|
||||||
$data['supportPin'] = 'MOKO-' . strtoupper(substr($token, 0, 4) . '-' . substr($token, 4, 4));
|
$date = gmdate('Y-m-d');
|
||||||
|
$hash = hash_hmac('sha256', $date, $token);
|
||||||
|
$data['supportPin'] = 'MOKO-' . strtoupper(substr($hash, 0, 4)) . '-' . strtoupper(substr($hash, 4, 4));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (\Throwable $e) {}
|
catch (\Throwable $e) {}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ $healthOk = $healthOk ?? true;
|
|||||||
$counts = $counts ?? (object) ['articles' => 0, 'users' => 0, 'extensions' => 0, 'updates' => 0];
|
$counts = $counts ?? (object) ['articles' => 0, 'users' => 0, 'extensions' => 0, 'updates' => 0];
|
||||||
$disk = $disk ?? (object) ['free_mb' => null, 'total_mb' => null];
|
$disk = $disk ?? (object) ['free_mb' => null, 'total_mb' => null];
|
||||||
$currentIp = $currentIp ?? '';
|
$currentIp = $currentIp ?? '';
|
||||||
$collapsed = $params->get('collapsed', 0);
|
$collapsed = true;
|
||||||
$showHealth = $params->get('show_health', 1);
|
$showHealth = $params->get('show_health', 1);
|
||||||
$showStats = $params->get('show_stats', 1);
|
$showStats = $params->get('show_stats', 1);
|
||||||
$showDisk = $params->get('show_disk', 1);
|
$showDisk = $params->get('show_disk', 1);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>MokoSuiteClient admin sidebar menu — renders a dedicated MokoSuiteClient section in the admin menu before Joomla's default menu.</description>
|
<description>MokoSuiteClient admin sidebar menu — renders a dedicated MokoSuiteClient section in the admin menu before Joomla's default menu.</description>
|
||||||
<namespace path="src">Moko\Module\MokoSuiteClientMenu</namespace>
|
<namespace path="src">Moko\Module\MokoSuiteClientMenu</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* MokoSuiteClient Admin Sidebar Menu
|
* MokoSuiteClient Admin Sidebar Menu
|
||||||
*
|
*
|
||||||
* Each installed Moko component gets its own top-level collapsible section.
|
* Each installed Moko component gets its own top-level collapsible section.
|
||||||
* com_mokosuiteclienthq is always pinned first. com_mokosuiteclient uses static views
|
* com_mokosuitehq is always pinned first. com_mokosuiteclient uses static views
|
||||||
* as children. All other components auto-discover their submenu items.
|
* as children. All other components auto-discover their submenu items.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ else
|
|||||||
// com_mokosuiteclient not in admin menu — add it manually
|
// com_mokosuiteclient not in admin menu — add it manually
|
||||||
$mokoComponents['com_mokosuiteclient'] = [
|
$mokoComponents['com_mokosuiteclient'] = [
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
'title' => 'MokoSuiteClient',
|
'title' => 'MokoSuite',
|
||||||
'link' => 'index.php?option=com_mokosuiteclient',
|
'link' => 'index.php?option=com_mokosuiteclient',
|
||||||
'icon' => 'icon-shield-alt',
|
'icon' => 'icon-shield-alt',
|
||||||
'element' => 'com_mokosuiteclient',
|
'element' => 'com_mokosuiteclient',
|
||||||
@@ -109,16 +109,37 @@ else
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sort: com_mokosuiteclienthq first, then alphabetical by title ─────────
|
// ── Sort: HQ first, Client second, then alphabetical ─────────
|
||||||
$hq = null;
|
$hq = null;
|
||||||
|
$client = null;
|
||||||
$rest = [];
|
$rest = [];
|
||||||
|
|
||||||
foreach ($mokoComponents as $key => $comp)
|
foreach ($mokoComponents as $key => $comp)
|
||||||
{
|
{
|
||||||
if ($key === 'com_mokosuiteclienthq')
|
// Shorten display titles:
|
||||||
|
// MokoSuiteClient → MokoSuite, MokoSuiteHQ → MokoHQ
|
||||||
|
// Everything else: MokoSuiteBackup → Backup, MokoSuiteOpenGraph → OpenGraph
|
||||||
|
if ($key === 'com_mokosuiteclient')
|
||||||
|
{
|
||||||
|
$comp['title'] = 'MokoSuite';
|
||||||
|
}
|
||||||
|
elseif ($key === 'com_mokosuitehq')
|
||||||
|
{
|
||||||
|
$comp['title'] = preg_replace('/^MokoSuite/i', 'Moko', $comp['title']);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$comp['title'] = preg_replace('/^MokoSuite\s*/i', '', $comp['title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($key === 'com_mokosuitehq')
|
||||||
{
|
{
|
||||||
$hq = $comp;
|
$hq = $comp;
|
||||||
}
|
}
|
||||||
|
elseif ($key === 'com_mokosuiteclient')
|
||||||
|
{
|
||||||
|
$client = $comp;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$rest[$key] = $comp;
|
$rest[$key] = $comp;
|
||||||
@@ -132,6 +153,10 @@ if ($hq !== null)
|
|||||||
{
|
{
|
||||||
$sorted[] = $hq;
|
$sorted[] = $hq;
|
||||||
}
|
}
|
||||||
|
if ($client !== null)
|
||||||
|
{
|
||||||
|
$sorted[] = $client;
|
||||||
|
}
|
||||||
foreach ($rest as $comp)
|
foreach ($rest as $comp)
|
||||||
{
|
{
|
||||||
$sorted[] = $comp;
|
$sorted[] = $comp;
|
||||||
@@ -139,8 +164,7 @@ foreach ($rest as $comp)
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.sidebar-wrapper .mokosuiteclient-ext-item > a { padding-inline-start: 1.5rem; }
|
.sidebar-wrapper .mokosuiteclient-ext-child > a { padding-inline-start: 0.5rem; }
|
||||||
.sidebar-wrapper .mokosuiteclient-ext-child > a { padding-inline-start: 2.5rem; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<ul class="nav flex-column main-nav">
|
<ul class="nav flex-column main-nav">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* DEFGROUP: Joomla.Plugin
|
* DEFGROUP: Joomla.Plugin
|
||||||
* INGROUP: MokoSuiteClient
|
* INGROUP: MokoSuiteClient
|
||||||
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
* VERSION: 02.45.00
|
* VERSION: 02.46.32
|
||||||
* PATH: /src/Extension/MokoSuiteClient.php
|
* PATH: /src/Extension/MokoSuiteClient.php
|
||||||
* NOTE: Core system plugin for MokoSuiteClient admin tools suite
|
* NOTE: Core system plugin for MokoSuiteClient admin tools suite
|
||||||
*/
|
*/
|
||||||
@@ -968,10 +968,15 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
|||||||
|
|
||||||
if (!in_array($akTable, $tables))
|
if (!in_array($akTable, $tables))
|
||||||
{
|
{
|
||||||
return [
|
// Check for MokoSuiteBackup instead
|
||||||
'status' => 'ok',
|
if (!in_array($prefix . 'mokosuitebackup_records', $tables))
|
||||||
'installed' => false,
|
{
|
||||||
];
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'installed' => false,
|
||||||
|
'message' => 'No backup solution installed (Akeeba Backup or MokoSuiteBackup)',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the most recent backup
|
// Get the most recent backup
|
||||||
@@ -1083,6 +1088,7 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
|||||||
return [
|
return [
|
||||||
'status' => 'ok',
|
'status' => 'ok',
|
||||||
'installed' => false,
|
'installed' => false,
|
||||||
|
'message' => 'Admin Tools is not installed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1209,7 +1215,7 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
|||||||
|
|
||||||
if (!in_array($prefix . 'scheduler_tasks', $tables))
|
if (!in_array($prefix . 'scheduler_tasks', $tables))
|
||||||
{
|
{
|
||||||
return ['status' => 'ok', 'available' => false];
|
return ['status' => 'ok', 'available' => false, 'message' => 'Task Scheduler not available'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$db->setQuery(
|
$db->setQuery(
|
||||||
@@ -1701,6 +1707,7 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
|
'message' => $issues ? implode('; ', $issues) : 'All configuration settings are optimal',
|
||||||
'debug' => $debug,
|
'debug' => $debug,
|
||||||
'error_report' => $errorReport,
|
'error_report' => $errorReport,
|
||||||
'gzip' => $gzip,
|
'gzip' => $gzip,
|
||||||
@@ -1709,7 +1716,6 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
|||||||
'force_ssl' => $forceSSL,
|
'force_ssl' => $forceSSL,
|
||||||
'caching' => $caching,
|
'caching' => $caching,
|
||||||
'lifetime' => $lifetime,
|
'lifetime' => $lifetime,
|
||||||
'issues' => $issues ?: null,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2561,15 +2567,18 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
|||||||
|
|
||||||
if ($response->code >= 200 && $response->code < 300)
|
if ($response->code >= 200 && $response->code < 300)
|
||||||
{
|
{
|
||||||
$this->app->enqueueMessage('MokoSuiteClientHQ heartbeat: site registered', 'message');
|
$this->app->enqueueMessage('MokoSuiteHQ heartbeat: site registered successfully.', 'message');
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
$body = json_decode($response->body, true);
|
||||||
|
$msg = $body['error'] ?? $body['message'] ?? ('HTTP ' . $response->code);
|
||||||
Log::add(
|
Log::add(
|
||||||
\sprintf('Heartbeat HTTP %d: %s', $response->code, $response->body),
|
\sprintf('Heartbeat HTTP %d: %s', $response->code, $response->body),
|
||||||
Log::WARNING,
|
Log::WARNING,
|
||||||
'mokosuiteclient'
|
'mokosuiteclient'
|
||||||
);
|
);
|
||||||
|
$this->app->enqueueMessage('MokoSuiteHQ heartbeat failed: ' . $msg, 'warning');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (\Throwable $e)
|
catch (\Throwable $e)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* FILE INFORMATION
|
* FILE INFORMATION
|
||||||
* DEFGROUP: Joomla.Plugin
|
* DEFGROUP: Joomla.Plugin
|
||||||
* INGROUP: MokoSuiteClient
|
* INGROUP: MokoSuiteClient
|
||||||
* VERSION: 02.45.00
|
* VERSION: 02.46.32
|
||||||
* PATH: /src/Field/CopyableTokenField.php
|
* PATH: /src/Field/CopyableTokenField.php
|
||||||
* BRIEF: Read-only token field with a copy-to-clipboard button
|
* BRIEF: Read-only token field with a copy-to-clipboard button
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,367 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package MokoSuiteClient
|
||||||
|
* @subpackage plg_system_mokosuiteclient
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Plugin\System\MokoSuiteClient\Helper;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\Database\DatabaseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MokoGitea License Validator — core DRM enforcement for the MokoSuite platform.
|
||||||
|
*
|
||||||
|
* Validates the site's DLID against MokoGitea, caches the result,
|
||||||
|
* and provides entitlement checking for all suite modules.
|
||||||
|
*
|
||||||
|
* Default Gitea server: git.mokoconsulting.tech
|
||||||
|
*
|
||||||
|
* @since 02.45.00
|
||||||
|
*/
|
||||||
|
final class LicenseValidator
|
||||||
|
{
|
||||||
|
/** @var string Default MokoGitea server address */
|
||||||
|
private const DEFAULT_GITEA_URL = 'https://git.mokoconsulting.tech';
|
||||||
|
|
||||||
|
/** @var int Cache TTL in seconds (24 hours) */
|
||||||
|
private const CACHE_TTL = 86400;
|
||||||
|
|
||||||
|
/** @var int Grace period in days after expiry before deactivation */
|
||||||
|
private const DEFAULT_GRACE_DAYS = 7;
|
||||||
|
|
||||||
|
/** @var object|null Cached license data for current request */
|
||||||
|
private static ?object $cachedLicense = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the site's DLID against MokoGitea.
|
||||||
|
* Returns cached result if still valid; calls API if expired.
|
||||||
|
*/
|
||||||
|
public static function validate(bool $forceRefresh = false): object
|
||||||
|
{
|
||||||
|
if (self::$cachedLicense && !$forceRefresh) {
|
||||||
|
return self::$cachedLicense;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||||
|
$dlid = self::getDlid();
|
||||||
|
|
||||||
|
if (!$dlid) {
|
||||||
|
return self::$cachedLicense = (object) [
|
||||||
|
'valid' => false,
|
||||||
|
'status' => 'no_dlid',
|
||||||
|
'message' => 'No license key configured',
|
||||||
|
'entitlements'=> [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check DB cache first
|
||||||
|
if (!$forceRefresh) {
|
||||||
|
$cached = self::getCachedResult($db, $dlid);
|
||||||
|
if ($cached) {
|
||||||
|
return self::$cachedLicense = $cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call MokoGitea API
|
||||||
|
$result = self::callGiteaApi($dlid);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
self::cacheResult($db, $dlid, $result);
|
||||||
|
|
||||||
|
return self::$cachedLicense = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current license includes entitlement for a specific extension.
|
||||||
|
*
|
||||||
|
* @param string $extension Extension element name (e.g., 'com_mokosuite_crm', 'com_mokosuiterestaurant')
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isEntitled(string $extension): bool
|
||||||
|
{
|
||||||
|
$license = self::validate();
|
||||||
|
|
||||||
|
if (!$license->valid) return false;
|
||||||
|
|
||||||
|
// Map extension names to repo identifiers
|
||||||
|
$repoMap = [
|
||||||
|
'com_mokosuite' => 'MokoSuite',
|
||||||
|
'com_mokosuite_crm' => 'MokoSuiteCRM',
|
||||||
|
'com_mokosuite_erp' => 'MokoSuiteERP',
|
||||||
|
'com_mokosuitechild' => 'MokoSuiteChild',
|
||||||
|
'com_mokosuitecreate' => 'MokoSuiteCreate',
|
||||||
|
'com_mokosuitenpo' => 'MokoSuiteNPO',
|
||||||
|
'com_mokosuitefield' => 'MokoSuiteField',
|
||||||
|
'com_mokosuitepos' => 'MokoSuitePOS',
|
||||||
|
'com_mokoshop' => 'MokoSuiteShop',
|
||||||
|
'com_mokosuitehrm' => 'MokoSuiteHRM',
|
||||||
|
'com_mokosuitemrp' => 'MokoSuiteMRP',
|
||||||
|
'com_mokosuiterestaurant' => 'MokoSuiteRestaurant',
|
||||||
|
];
|
||||||
|
|
||||||
|
$repo = $repoMap[$extension] ?? $extension;
|
||||||
|
$entitlements = $license->entitlements ?? [];
|
||||||
|
|
||||||
|
// Base is always entitled if license is valid
|
||||||
|
if ($repo === 'MokoSuite') return true;
|
||||||
|
|
||||||
|
return in_array($repo, $entitlements, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full license status for admin display.
|
||||||
|
*/
|
||||||
|
public static function getStatus(): object
|
||||||
|
{
|
||||||
|
$license = self::validate();
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
'valid' => $license->valid ?? false,
|
||||||
|
'status' => $license->status ?? 'unknown',
|
||||||
|
'tier' => $license->tier ?? 'none',
|
||||||
|
'entitlements' => $license->entitlements ?? [],
|
||||||
|
'expires_at' => $license->expires_at ?? null,
|
||||||
|
'seats' => $license->seats ?? 0,
|
||||||
|
'seats_used' => $license->seats_used ?? 0,
|
||||||
|
'days_remaining'=> self::getDaysRemaining($license),
|
||||||
|
'in_grace' => self::isInGracePeriod($license),
|
||||||
|
'gitea_url' => self::getGiteaUrl(),
|
||||||
|
'dlid_configured' => (bool) self::getDlid(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available seat count.
|
||||||
|
*/
|
||||||
|
public static function getAvailableSeats(): int
|
||||||
|
{
|
||||||
|
$license = self::validate();
|
||||||
|
$total = (int) ($license->seats ?? 0);
|
||||||
|
$used = (int) ($license->seats_used ?? 0);
|
||||||
|
|
||||||
|
if ($total === 0) return PHP_INT_MAX; // Unlimited seats
|
||||||
|
|
||||||
|
return max(0, $total - $used);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report a heartbeat to MokoGitea (active installation check).
|
||||||
|
* Called by task scheduler daily.
|
||||||
|
*/
|
||||||
|
public static function heartbeat(): object
|
||||||
|
{
|
||||||
|
$dlid = self::getDlid();
|
||||||
|
if (!$dlid) return (object) ['success' => false, 'error' => 'No DLID'];
|
||||||
|
|
||||||
|
$giteaUrl = self::getGiteaUrl();
|
||||||
|
$siteUrl = \Joomla\CMS\Uri\Uri::root();
|
||||||
|
$joomlaVersion = (new \Joomla\CMS\Version())->getShortVersion();
|
||||||
|
|
||||||
|
// Count installed suite modules
|
||||||
|
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||||
|
$db->setQuery($db->getQuery(true)
|
||||||
|
->select('element')
|
||||||
|
->from('#__extensions')
|
||||||
|
->where($db->quoteName('element') . ' LIKE ' . $db->quote('com_mokosuite%'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||||
|
->where($db->quoteName('enabled') . ' = 1'));
|
||||||
|
$installedModules = $db->loadColumn() ?: [];
|
||||||
|
|
||||||
|
$response = self::httpPost($giteaUrl . '/api/v1/licenses/heartbeat', [
|
||||||
|
'dlid' => $dlid,
|
||||||
|
'site_url' => $siteUrl,
|
||||||
|
'joomla_version' => $joomlaVersion,
|
||||||
|
'installed_modules' => $installedModules,
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private methods ─────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configured DLID from component params.
|
||||||
|
*/
|
||||||
|
private static function getDlid(): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$params = Factory::getApplication()->getParams('com_mokosuite');
|
||||||
|
return trim($params->get('dlid', ''));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Component not installed or params not available
|
||||||
|
try {
|
||||||
|
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||||
|
$db->setQuery($db->getQuery(true)
|
||||||
|
->select('params')
|
||||||
|
->from('#__extensions')
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('component')));
|
||||||
|
$paramsJson = $db->loadResult();
|
||||||
|
$params = json_decode($paramsJson ?: '{}', false);
|
||||||
|
return trim($params->dlid ?? '');
|
||||||
|
} catch (\Throwable $e2) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the MokoGitea server URL from config.
|
||||||
|
*/
|
||||||
|
private static function getGiteaUrl(): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$params = Factory::getApplication()->getParams('com_mokosuite');
|
||||||
|
return rtrim($params->get('gitea_url', self::DEFAULT_GITEA_URL), '/');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return self::DEFAULT_GITEA_URL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call MokoGitea license validation API.
|
||||||
|
*/
|
||||||
|
private static function callGiteaApi(string $dlid): object
|
||||||
|
{
|
||||||
|
$giteaUrl = self::getGiteaUrl();
|
||||||
|
|
||||||
|
$response = self::httpGet($giteaUrl . '/api/v1/licenses/validate?dlid=' . urlencode($dlid));
|
||||||
|
|
||||||
|
if (isset($response->valid)) {
|
||||||
|
return (object) [
|
||||||
|
'valid' => (bool) $response->valid,
|
||||||
|
'status' => $response->status ?? 'unknown',
|
||||||
|
'tier' => $response->tier ?? '',
|
||||||
|
'entitlements' => $response->entitlements ?? $response->repo_scope ?? [],
|
||||||
|
'expires_at' => $response->expires_at ?? null,
|
||||||
|
'seats' => (int) ($response->seats ?? 0),
|
||||||
|
'seats_used' => (int) ($response->seats_used ?? 0),
|
||||||
|
'message' => $response->message ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// API error — use cached result if available, otherwise fail gracefully
|
||||||
|
return (object) [
|
||||||
|
'valid' => false,
|
||||||
|
'status' => 'api_error',
|
||||||
|
'message' => $response->error ?? 'Could not reach license server',
|
||||||
|
'entitlements' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached validation result from database.
|
||||||
|
*/
|
||||||
|
private static function getCachedResult(DatabaseInterface $db, string $dlid): ?object
|
||||||
|
{
|
||||||
|
$dlidHash = hash('sha256', $dlid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db->setQuery($db->getQuery(true)
|
||||||
|
->select('response_data, checked_at')
|
||||||
|
->from('#__mokosuite_license_cache')
|
||||||
|
->where($db->quoteName('dlid_hash') . ' = ' . $db->quote($dlidHash))
|
||||||
|
->where('checked_at > DATE_SUB(NOW(), INTERVAL ' . (int) self::CACHE_TTL . ' SECOND)'));
|
||||||
|
$cached = $db->loadObject();
|
||||||
|
|
||||||
|
if ($cached && $cached->response_data) {
|
||||||
|
$data = json_decode($cached->response_data, false);
|
||||||
|
if ($data) return $data;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Table may not exist yet — that's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache validation result in database.
|
||||||
|
*/
|
||||||
|
private static function cacheResult(DatabaseInterface $db, string $dlid, object $result): void
|
||||||
|
{
|
||||||
|
$dlidHash = hash('sha256', $dlid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Upsert
|
||||||
|
$db->setQuery('REPLACE INTO #__mokosuite_license_cache (dlid_hash, response_data, checked_at) VALUES ('
|
||||||
|
. $db->quote($dlidHash) . ', '
|
||||||
|
. $db->quote(json_encode($result)) . ', '
|
||||||
|
. $db->quote(Factory::getDate()->toSql()) . ')');
|
||||||
|
$db->execute();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Cache table may not exist — non-fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate days remaining on license.
|
||||||
|
*/
|
||||||
|
private static function getDaysRemaining(object $license): ?int
|
||||||
|
{
|
||||||
|
if (empty($license->expires_at)) return null;
|
||||||
|
|
||||||
|
$now = new \DateTime('today');
|
||||||
|
$expiry = new \DateTime($license->expires_at);
|
||||||
|
$diff = (int) $now->diff($expiry)->format('%r%a');
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if license is in grace period (expired but within grace window).
|
||||||
|
*/
|
||||||
|
private static function isInGracePeriod(object $license): bool
|
||||||
|
{
|
||||||
|
$days = self::getDaysRemaining($license);
|
||||||
|
if ($days === null || $days >= 0) return false;
|
||||||
|
|
||||||
|
$graceDays = self::DEFAULT_GRACE_DAYS;
|
||||||
|
try {
|
||||||
|
$graceDays = (int) Factory::getApplication()->getParams('com_mokosuite')->get('license_grace_days', self::DEFAULT_GRACE_DAYS);
|
||||||
|
} catch (\Throwable $e) {}
|
||||||
|
|
||||||
|
return abs($days) <= $graceDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP GET helper.
|
||||||
|
*/
|
||||||
|
private static function httpGet(string $url): object
|
||||||
|
{
|
||||||
|
$response = file_get_contents($url, false, stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'header' => 'Accept: application/json',
|
||||||
|
'ignore_errors' => true,
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
return json_decode($response ?: '{}', false) ?: (object) ['error' => 'No response'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP POST helper.
|
||||||
|
*/
|
||||||
|
private static function httpPost(string $url, array $data): object
|
||||||
|
{
|
||||||
|
$response = file_get_contents($url, false, stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => "Content-Type: application/json\r\nAccept: application/json",
|
||||||
|
'ignore_errors' => true,
|
||||||
|
'timeout' => 10,
|
||||||
|
'content' => json_encode($data),
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
return json_decode($response ?: '{}', false) ?: (object) ['error' => 'No response'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
|
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>MokoSuiteClient core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
|
<description>MokoSuiteClient core system plugin — coordinates feature plugins, heartbeat, health checks, and admin customizations.</description>
|
||||||
<namespace path=".">Moko\Plugin\System\MokoSuiteClient</namespace>
|
<namespace path=".">Moko\Plugin\System\MokoSuiteClient</namespace>
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
filter="url" />
|
filter="url" />
|
||||||
|
|
||||||
<field name="monitor_signing_key" type="hidden"
|
<field name="monitor_signing_key" type="hidden"
|
||||||
default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUUMvcnVrWE0zZHB0aDg2DQpGSkRXTjM0ZjQ2cUtJem1SMmFtTWUyZ2dWbWxsWnFyMHJkRFk4OTdtQ05FRkk4Q0NwNGR5amkwOU5ETnAvalFxDQovL2JGdUFNOUFTZU5oQTlmRlpwSG5UMGkzY3N4V3RSS2NnMnRkR0wzUXhNRFVBeFJYQ1RSQXVPSWZybGp6Ky85DQpWZ0ZtWHU3M1VSaU9XY1lLeFErejFoZkRGK2ZxRTRlYW9QcUlsY2J5dmtKd2lkSkRWUEEwc2RtbVlUTFg2Q29xDQpQalVDRENlbkZoUXNteVMzM29KSXArK0c3ZzU5NmRYelZIczRQSjIwNnc0Z3JlckRRZk5GVytzZndHSnl3NjBrDQpUQTVmUzF2Wit4NEt2UUh6V1ErYS9xRS9sSGxFVzdOTWVJWExNWGczSDd1eXBabXlVU2t3S0k0djFQQXRGWmtkDQpBaVpPZWZpVkFnTUJBQUVDZ2dFQVI0VGJyVDR0NWJ5MDhIQW0wcTR3WVF4REhEbVlJbzNXdDZ5MURmYU11OVMzDQpDYW5TMm9oazJzaE9TcGhhU2hFajI3WjBKY2hYdjhYWURvbU1BZmVsN3I5eDZjQ2FhTVdUNEdCMU5Zckp1NDhBDQprV2NteTkwWitPNTZQZkZJeTJXdXV6dFRxaFdZb0ZDSTBOZlU2bGw5SzhpSFl6VWx1MzZSSklweWx5OXFPKyt4DQpmTUZYcUovSkk0bVp6NW0raDBnbFMvN21VZ0EvUTRjbVJnRHJ3dkc3bEpBRjhWSDBEdW1uRWJkWkZvSi9XbU9JDQpSTi9lemhqczYrbU9hTnUwQWRsclpLU3QwRWZVYjl3QTFLQm5JMVVDU2w0Y1lidXVpL29jOWo1aGl6RGJvRWRyDQpJL1U5Y2FYUmZvb0pMNlUwOXN1VTdyTlFLbFRhMXM4NVhvY3htT0JMK1FLQmdRRHg5QzB5MjQ5SG1paXJ2WExIDQpBUXdUTjRyMjdhUTZMMFc2SHdDNHdzMUhleDRpeWRXT1lIcWdBSnY4VHZyeVpHOW1SaFh1U1ROTjYxV1UvTWFNDQphQVYwVjJ4Y0RrdDNFUnhNak1XRmhXUTh0cjN2RUtqWjFnOVJXOGhiTE9VYXVCcmJhMlI4RWNZYXFLZXlxR3N4DQpCa0VLZlRIUzNmUysraXNLZ2EzUU1mcjB6d0tCZ1FES3o2SGVKZ0tKRTVMM1ppbkhxaUFyVm5SZ2pYcFZrMWpvDQp6VXh5eTkwNEhmNGlmVXNIZklpdzVpN0VNR0U0RE5ob2MvZUJxcW1oM1N2ejJMUDNzOHUrL0hVZFllVzJIV1hhDQpKZlpMRE5BM0U3WDNkSVJ6MFg5UTh2OHcxaFpQeUxYOUlYeUVyUTNGZHFVdyt1Tko1VFZJell0RHppNnRKTjkvDQpGZGlxS0Q2ZFd3S0JnUURnQnE5bS9LWmdyTnRsa1FkYVBaejVtaDhBWGE4RzlNaEIrZnpJRmc3T1ZhL2tsQzg1DQpJaG5JVm1nWHFPVndWQkJWaVNVN09lbllCc042TE1hR01MYUVMNEkwaGtQWG5pOHVyZFVodVEzRHJZeVZjejUwDQpYR0JZZTN3Njk0bTJRS3NWYVExa1YyeXZPR1AxNXoxQTZrS0V2TURLTnhzclRTVlhHQlZneFRaUlB3S0JnUURBDQp1RFVVcUFIWXlDVHJ1c1VRMm5UZk9iUTAyN3ZYL2NDSzJDdEJHc0FJUjFmcTVpeVozSmozb0lQb0lpRC81aFR1DQpqT1F3N3o5cWRJVURublRGZUxDdnQ2NkNVVGk3cVl2VGxDZEtnYzZKeDgwdWJDWkErRjZIU2FGOWdyS0k5aTBaDQpjT3ltRnR2elBCOFZRQk1qY1E0Rk0yeVc3aUlrbmRsVEppdFE1aFU1NlFLQmdEZ1JIOXBEcGZwWlZ2V2g2MldGDQp5OGZzWUo1ODhzQmRMUlpTYTRuNi9XbjdUcUp1bWg2aWpFcDVyZFdnQkVtaDlJSk9jRUlhZ05mK0s5MXdoaThvDQpTeW01ajJpL1pjVVFYNFJSTDNxQ1RZZWVQVnZ3RHc3aWNLWVowTGQ2S1pFMmdEaDRPbEg4ejU0Zkl3a2tMSzRFDQpCcmtJNWppa05QSkJFR25zTm9zU3pWN2QNCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0NCg=="
|
default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ2xZNnNzOTZpeTZOOGMKTHRxbndhbnU4eEozdDcrdDhXT3hoY0Yyclc2QmlmOVhNaEpnYkw0c055N0wwV1dTT2tkMmZxalBNcDFtOFNyNAo1VnNycjE3cFc5b0FNMmtmdFdsaTZ1NkhTVEYyN2pVVUJrT3o4MHZMRklMMGNGNkJCUkpYN2JVWkRpamdUMjc1ClREb3dXZy82Zk9GeWFEelBHUkJuYXFacTljU2lEYWoyNlpSTVZIbktQUERWTG92VzRPTDQzL2gwZ3BtN25nUGIKdWJlLzFFTDRUMHFRbm1Xc2FEOFZ6VStoRXFGSDRTVUtMaDVNeklGbUxFZzRlZ0xCbTBXcWdxbzZRQVBnZDVPYgoybXhmQndta3RLVm5hcWR6eG9KSytzaTVuZkYreGpxbWRMZThUdmEyTHNuTUxlZmsrODVoQ3hxS2x1eWRta1lXCjlvUk5qcDhiQWdNQkFBRUNnZ0VBQkZOUS9NSVZaV2gxdlZUMFh3TFBvUEkyZjI4TTBrM0gzN0t4MXBxK2t5QzYKenRyK1pBczBCaEFEWjAwNHJOUmRYaG45N0QxVXBJYVdLeUJFZkNZQUEzWmxneS9WQmdGR21sR3VuMWNvdGdXUQoyYzg0SWhLdzNzVFFqL2dJWUxOelFWMTBLUTJYd0JZVHZ1MWhjRFpLeUxCUGJTQ1F4cEhQUGdVcUNRNFljR3lFClErVmc1dHJUYk8wQ2xCZ1U5bkVnYU1RakRJZ0F3WVZPV203dUxJTW84UC9nT3FuT2tmaFhzdzl3VTJVYWxFeTEKRmRZbGhMbGJ0ZS9MZ3lkYlJ2RStjNEtqZVp0Z3ptc1RneEh2dzM5YVVmZUZTclFRT0FjcXc0alNzUjdMck9UZAp5bDhpelRrZVBrTVFMamFqR0pabWdPbitkRzhtUlpMa3FKcWdGaVpqRVFLQmdRRFV0L0xlU0h5SmhvY3VFL240CkZreEpaclJoWUVsWnc2WlZJUnQzWDlPQ1Nmaklab3I1ZkZlczhvUzZySFhKdGZYeWx4QUxOSjJjTUhKTTViVnUKbUFSUFU4cThBeVc0OE03cHAyNmtVVTMxNXc2OU1SUkhzbWgyekRabEtDeG5GM1NSQ3U4YW95d3hZc3RUZ3hkTgo2bDhLNHZsS1dsN3FYblBhWjZjb3lQSU9od0tCZ1FESENuRmRRdW5SMVI2dkxGaVFZMTRiT3QwT0tzVGJYMUJyCmpvUGZySkxvRm5mSCs4VDVnNUdxYkV5T2p0WG1tRXhmTFFpcDBQVXRtc1E0YXlJRFBZYWZtU3RpK2dtQXZFd1MKZTlKcVYxYlRuazUrYnVRZ2FlOW16REpJWkxaczRJUlhrd1Q5aDZ4Q2xKeS80TGJSRHdBU3dUVGJlY01hN3A4UgpQN0p0bjdsYnpRS0JnQzNOR2FjUTFuZktGb3N1VS9FOTQ5a2VHeEtvWjhMREpLcEp3WjgzYTlRdTF6bFhFdTlhCi9ZbklnaG1yam9VSy85VG0vOVpaMHVIUmNKcnNEdCtzTGFsaThsRC9JSDBzcEhDYzAyN2Y3cmhXc3M2N3BaRTIKY2RXNmJLL2xNWUpWQTQxRFhHNVEyZkFjUklsTHZaWFNNL3FsR21ZUEJVYlRaWUNPTnVqS000dzdBb0dBU1dBdwpLcEZnWVZxUDFVUWo0aGEvdW9vWXRBQlFVZzd4TnJWektDSVdoampDTDVkQkpqcTZtSGtVUC9tb0lUcEQ3VkpNCnYwMnBGUWJaRDNOdk5vS1gvbjRZNElRTXZNaXR3cUtqRDFEalVXQXF6N0ZScUNGbGdDQUc2V2szVnl2dG5kczEKRzhISVgwTXFCaEp4VXVDVXhsVXpoelY4RjVHZ1VsdUpDNkMyVklFQ2dZQkJWSkxpZlNVOTlHWGZtK3dPd0RWcgo2bHZoUFgxOTBGVktWQXY3aVVWTXBwWXg4Y0QxYkcyUjRLT29JbnkxYTlxdjA2ZGFzeGVQOStkVjJVMWU3MWl5CkFXWDRBVHIrYitvSGk2eUk1MXRHRk54RUxiNXZYMVpYM3VNaDlWM29iYUpuSFNjYllpKzBBNjlyRmNuNEZuLzUKWXJybWxLTzRlRHFVZkswbVFJVCtwUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"
|
||||||
filter="raw" />
|
filter="raw" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</fields>
|
</fields>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* DEFGROUP: Joomla.Plugin
|
* DEFGROUP: Joomla.Plugin
|
||||||
* INGROUP: MokoSuiteClient
|
* INGROUP: MokoSuiteClient
|
||||||
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
* VERSION: 02.45.00
|
* VERSION: 02.46.32
|
||||||
* PATH: /src/script.php
|
* PATH: /src/script.php
|
||||||
* BRIEF: Installation script for MokoSuiteClient plugin
|
* BRIEF: Installation script for MokoSuiteClient plugin
|
||||||
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
|
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* DEFGROUP: Joomla.Plugin
|
* DEFGROUP: Joomla.Plugin
|
||||||
* INGROUP: MokoSuiteClient
|
* INGROUP: MokoSuiteClient
|
||||||
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
* REPO: https://github.com/mokoconsulting-tech/mokosuiteclient
|
||||||
* VERSION: 02.45.00
|
* VERSION: 02.46.32
|
||||||
* PATH: /src/services/provider.php
|
* PATH: /src/services/provider.php
|
||||||
* BRIEF: Service provider for dependency injection in Joomla 5.x
|
* BRIEF: Service provider for dependency injection in Joomla 5.x
|
||||||
* NOTE: Registers the plugin with Joomla's DI container
|
* NOTE: Registers the plugin with Joomla's DI container
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC</description>
|
<description>PLG_SYSTEM_MOKOSUITECLIENT_BACKUP_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientBackup</namespace>
|
<namespace path="src">Moko\Plugin\System\MokoSuiteClientBackup</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -118,13 +118,11 @@ class Backup extends CMSPlugin implements SubscriberInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prefer MokoSuiteBackup's own helper (clean public API)
|
// Prefer MokoSuiteBackup's own helper (clean public API)
|
||||||
$helperClass = 'Joomla\\Component\\MokoSuiteBackup\\Administrator\\Utility\\BackupStatusHelper';
|
$helperClass = 'Joomla\\Component\\MokoSuiteBackup\\Administrator\\Helper\\BackupStatusHelper';
|
||||||
|
|
||||||
if (class_exists($helperClass))
|
if (class_exists($helperClass))
|
||||||
{
|
{
|
||||||
$staleDays = (int) $this->params->get('stale_days', 7);
|
return $helperClass::getStatusSummary();
|
||||||
|
|
||||||
return $helperClass::getStatus($staleDays);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: direct table query for older MokoSuiteBackup versions
|
// Fallback: direct table query for older MokoSuiteBackup versions
|
||||||
@@ -244,20 +242,52 @@ class Backup extends CMSPlugin implements SubscriberInterface
|
|||||||
? round($latest->total_size / 1048576)
|
? round($latest->total_size / 1048576)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Total counts (all time)
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||||
|
);
|
||||||
|
$allTime = (int) $db->loadResult();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||||
|
->where($db->quoteName('status') . ' = ' . $db->quote('fail'))
|
||||||
|
);
|
||||||
|
$allFailed = (int) $db->loadResult();
|
||||||
|
|
||||||
|
// Recent failures
|
||||||
|
$cutoff7d = date('Y-m-d H:i:s', strtotime('-7 days'));
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||||
|
->where($db->quoteName('status') . ' = ' . $db->quote('fail'))
|
||||||
|
->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff7d))
|
||||||
|
);
|
||||||
|
$recentFailed = (int) $db->loadResult();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'installed' => true,
|
'installed' => true,
|
||||||
'status' => $status,
|
'latest' => [
|
||||||
'last_backup' => $latest->backupstart,
|
'status' => $latest->status,
|
||||||
'last_status' => $latest->status,
|
'backup_type' => $latest->backup_type ?? 'full',
|
||||||
'last_size_mb' => $sizeMb,
|
'description' => $latest->description ?? '',
|
||||||
'days_since' => $daysSince,
|
'backup_start' => $latest->backupstart,
|
||||||
'backup_type' => $latest->backup_type,
|
'backup_end' => $latest->backupend ?? null,
|
||||||
'origin' => $latest->origin,
|
'total_size' => (int) ($latest->total_size ?? 0),
|
||||||
'total_backups' => $totalBackups,
|
'origin' => $latest->origin ?? 'backend',
|
||||||
'recent_7d' => $recentBackups,
|
],
|
||||||
'fail_count_7d' => $failCount7d,
|
'totals' => [
|
||||||
'files_exist' => (bool) $latest->filesexist,
|
'all_time' => $allTime,
|
||||||
'description' => $latest->description,
|
'all_success' => $totalBackups,
|
||||||
|
'all_failed' => $allFailed,
|
||||||
|
'recent_total' => $recentBackups + $recentFailed,
|
||||||
|
'recent_success' => $recentBackups,
|
||||||
|
'recent_failed' => $recentFailed,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_DBIP_DESC</description>
|
<description>PLG_SYSTEM_MOKOSUITECLIENT_DBIP_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDBIP</namespace>
|
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDBIP</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DESC</description>
|
<description>PLG_SYSTEM_MOKOSUITECLIENT_DEVTOOLS_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDevTools</namespace>
|
<namespace path="src">Moko\Plugin\System\MokoSuiteClientDevTools</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_FIREWALL_DESC</description>
|
<description>PLG_SYSTEM_MOKOSUITECLIENT_FIREWALL_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientFirewall</namespace>
|
<namespace path="src">Moko\Plugin\System\MokoSuiteClientFirewall</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_LICENSE_DESC</description>
|
<description>PLG_SYSTEM_MOKOSUITECLIENT_LICENSE_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientLicense</namespace>
|
<namespace path="src">Moko\Plugin\System\MokoSuiteClientLicense</namespace>
|
||||||
<files><folder>src</folder><folder>services</folder><folder>language</folder></files>
|
<files><folder>src</folder><folder>services</folder><folder>language</folder></files>
|
||||||
|
|||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
; MokoSuiteClient Health Monitor Plugin
|
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
; License: GPL-3.0-or-later
|
|
||||||
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR="System - MokoSuiteClient Monitor"
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_DESC="Sends heartbeat data to a MokoSuiteClientHQ control panel for centralized site monitoring."
|
|
||||||
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_FIELDSET_BASIC="Monitoring"
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_FIELDSET_BASIC_DESC="Configure heartbeat reporting to MokoSuiteClientHQ."
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_HEARTBEAT_LABEL="Send Heartbeat"
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_HEARTBEAT_DESC="Send heartbeat data to MokoSuiteClientHQ when plugin settings are saved."
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_LABEL="MokoSuiteClientHQ URL"
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_DESC="URL of the MokoSuiteClientHQ control panel (e.g. https://mokoconsulting.tech). The heartbeat is sent to /api/index.php/v1/mokosuiteclienthq/heartbeat on this host."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoSuiteClient Health Monitor Plugin - System strings
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR="System - MokoSuiteClient Monitor"
|
|
||||||
PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_DESC="Site health monitoring, MokoSuiteClientHQ heartbeat integration, and diagnostics."
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
|
||||||
<name>System - MokoSuiteClient Monitor</name>
|
|
||||||
<element>mokosuiteclient_monitor</element>
|
|
||||||
<author>Moko Consulting</author>
|
|
||||||
<creationDate>2026-06-02</creationDate>
|
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
|
||||||
<license>GPL-3.0-or-later</license>
|
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
|
||||||
<version>02.45.00</version>
|
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_DESC</description>
|
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientMonitor</namespace>
|
|
||||||
|
|
||||||
<files>
|
|
||||||
<folder>src</folder>
|
|
||||||
<folder>services</folder>
|
|
||||||
<folder>language</folder>
|
|
||||||
</files>
|
|
||||||
|
|
||||||
<languages folder="language">
|
|
||||||
<language tag="en-GB">en-GB/plg_system_mokosuiteclient_monitor.ini</language>
|
|
||||||
<language tag="en-GB">en-GB/plg_system_mokosuiteclient_monitor.sys.ini</language>
|
|
||||||
</languages>
|
|
||||||
|
|
||||||
<config>
|
|
||||||
<fields name="params">
|
|
||||||
<fieldset name="basic"
|
|
||||||
label="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_FIELDSET_BASIC"
|
|
||||||
description="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_FIELDSET_BASIC_DESC">
|
|
||||||
|
|
||||||
<field name="heartbeat_enabled" type="radio" default="1"
|
|
||||||
label="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_HEARTBEAT_LABEL"
|
|
||||||
description="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_HEARTBEAT_DESC"
|
|
||||||
class="btn-group btn-group-yesno">
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
<field name="base_url" type="url"
|
|
||||||
default="https://waas.dev.mokoconsulting.tech"
|
|
||||||
label="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_LABEL"
|
|
||||||
description="PLG_SYSTEM_MOKOSUITECLIENT_MONITOR_BASE_URL_DESC"
|
|
||||||
filter="url" />
|
|
||||||
|
|
||||||
<field name="signing_key" type="hidden"
|
|
||||||
default="LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUUMvcnVrWE0zZHB0aDg2DQpGSkRXTjM0ZjQ2cUtJem1SMmFtTWUyZ2dWbWxsWnFyMHJkRFk4OTdtQ05FRkk4Q0NwNGR5amkwOU5ETnAvalFxDQovL2JGdUFNOUFTZU5oQTlmRlpwSG5UMGkzY3N4V3RSS2NnMnRkR0wzUXhNRFVBeFJYQ1RSQXVPSWZybGp6Ky85DQpWZ0ZtWHU3M1VSaU9XY1lLeFErejFoZkRGK2ZxRTRlYW9QcUlsY2J5dmtKd2lkSkRWUEEwc2RtbVlUTFg2Q29xDQpQalVDRENlbkZoUXNteVMzM29KSXArK0c3ZzU5NmRYelZIczRQSjIwNnc0Z3JlckRRZk5GVytzZndHSnl3NjBrDQpUQTVmUzF2Wit4NEt2UUh6V1ErYS9xRS9sSGxFVzdOTWVJWExNWGczSDd1eXBabXlVU2t3S0k0djFQQXRGWmtkDQpBaVpPZWZpVkFnTUJBQUVDZ2dFQVI4VGJyVDR0NWJ5MDhIQW0wcTR3WVF4REhEbVlJbzNXdDZ5MURmYU11OVMzDQpDYW5TMm9oazJzaE9TcGhhU2hFajI3WjBKY2hYdjhYWURvbU1BZmVsN3I5eDZjQ2FhTVdUNEdCMU5Zckp1NDhBDQprV2NteTkwWitPNTZQZkZJeTJXdXV6dFRxaFdZb0ZDSTBOZlU2bGw5SzhpSFl6VWx1MzZSSklweWx5OXFPKyt4DQpmTUZYcUovSkk0bVp6NW0raDBnbFMvN21VZ0EvUTRjbVJnRHJ3dkc3bEpBRjhWSDBEdW1uRWJkWkZvSi9XbU9JDQpSTi9lemhqczYrbU9hTnUwQWRsclpLU3QwRWZVYjl3QTFLQm5JMVVDU2w0Y1lidXVpL29jOWo1aGl6RGJvRWRyDQpJL1U5Y2FYUmZvb0pMNlUwOXN1VTdyTlFLbFRhMXM4NVhvY3htT0JMK1FLQmdRRHg5QzB5MjQ5SG1paXJ2WExIDQpBUXdUTjRyMjdhUTZMMFc2SHdDNHdzMUhleDRpeWRXT1lIcWdBSnY4VHZyeVpHOW1SaFh1U1ROTjYxV1UvTWFNDQphQVYwVjJ4Y0RrdDNFUnhNak1XRmhXUTh0cjN2RUtqWjFnOVJXOGhiTE9VYXVCcmJhMlI4RWNZYXFLZXlxR3N4DQpCa0VLZlRIUzNmUysraXNLZ2EzUU1mcjB6d0tCZ1FES3o2SGVKZ0tKRTVMM1ppbkhxaUFyVm5SZ2pYcFZrMWpvDQp6VXh5eTkwNEhmNGlmVXNIZklpdzVpN0VNR0U0RE5ob2MvZUJxcW1oM1N2ejJMUDNzOHUrL0hVZFllVzJIV1hhDQpKZlpMRE5BM0U3WDNkSVJ6MFg5UTh2OHcxaFpQeUxYOUlYeUVyUTNGZHFVdyt1Tko1VFZJell0RHppNnRKTjkvDQpGZGlxS0Q2ZFd3S0JnUURnQnE5bS9LWmdyTnRsa1FkYVBaejVtaDhBWGE4RzlNaEIrZnpJRmc3T1ZhL2tsQzg1DQpJaG5JVm1nWHFPVndWQkJWaVNVN09lbllCc042TE1hR01MYUVMNEkwaGtQWG5pOHVyZFVodVEzRHJZeVZjejUwDQpYR0JZZTN3Njk0bTJRS3NWYVExa1YyeXZPR1AxNXoxQTZrS0V2TURLTnhzclRTVlhHQlZneFRaUlB3S0JnUURBDQp1RFVVcUFIWXlDVHJ1c1VRMm5UZk9iUTAyN3ZYL2NDSzJDdEJHc0FJUjFmcTVpeVozSmozb0lQb0lpRC81aFR1DQpqT1F3N3o5cWRJVURublRGZUxDdnQ2NkNVVGk3cVl2VGxDZEtnYzZKeDgwdWJDWkErRjZIU2FGOWdyS0k5aTBaDQpjT3ltRnR2elBCOFZRQk1qY1E4Rk0yeVc3aUlrbmRsVEppdFE1aFU1NlFLQmdEZ1JIOXBEcGZwWlZ2V2g2MldGDQp5OGZzWUo1ODhzQmRMUlpTYTRuNi9XbjdUcUp1bWg2aWpFcDVyZFdnQkVtaDlJSk9jRUlhZ05mK0s5MXdoaThvDQpTeW01ajJpL1pjVVFYNFJSTDNxQ1RZZWVQVnZ3RHc3aWNLWVowTGQ2S1pFMmdEaDRPbEg4ejU0Zkl3a2tMSzRFDQpCcmtJNWppa05QSkJFR25zTm9zU3pWN2QNCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0NCg=="
|
|
||||||
filter="raw" />
|
|
||||||
</fieldset>
|
|
||||||
</fields>
|
|
||||||
</config>
|
|
||||||
</extension>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteClient
|
|
||||||
* @subpackage plg_system_mokosuiteclient_monitor
|
|
||||||
* @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 Moko\Plugin\System\MokoSuiteClientMonitor\Extension\Monitor;
|
|
||||||
|
|
||||||
return new class implements ServiceProviderInterface
|
|
||||||
{
|
|
||||||
public function register(Container $container): void
|
|
||||||
{
|
|
||||||
$container->set(
|
|
||||||
PluginInterface::class,
|
|
||||||
function (Container $container) {
|
|
||||||
$dispatcher = $container->get(DispatcherInterface::class);
|
|
||||||
$plugin = new Monitor($dispatcher, (array) PluginHelper::getPlugin('system', 'mokosuiteclient_monitor'));
|
|
||||||
$plugin->setApplication(Factory::getApplication());
|
|
||||||
|
|
||||||
return $plugin;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteClient
|
|
||||||
* @subpackage plg_system_mokosuiteclient_monitor
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Plugin\System\MokoSuiteClientMonitor\Extension;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Log\Log;
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
|
||||||
use Joomla\CMS\Uri\Uri;
|
|
||||||
use Joomla\CMS\Version;
|
|
||||||
use Joomla\Event\SubscriberInterface;
|
|
||||||
use Moko\Plugin\System\MokoSuiteClient\Helper\MokoSuiteClientHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MokoSuiteClient Health Monitor Plugin
|
|
||||||
*
|
|
||||||
* Sends heartbeat data to a MokoSuiteClientHQ control panel instance.
|
|
||||||
* Each request is RSA-signed with a private key distributed via
|
|
||||||
* the package manifest, verified by Base using the matching public key.
|
|
||||||
*
|
|
||||||
* @since 02.32.00
|
|
||||||
*/
|
|
||||||
class Monitor extends CMSPlugin implements SubscriberInterface
|
|
||||||
{
|
|
||||||
protected $autoloadLanguage = true;
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onExtensionAfterSave' => 'onExtensionAfterSave',
|
|
||||||
'onAfterInitialise' => 'onAfterInitialise',
|
|
||||||
'onExtensionAfterInstall' => 'onExtensionAfterInstall',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send heartbeat on first admin page load after install/update.
|
|
||||||
*/
|
|
||||||
public function onAfterInitialise(): void
|
|
||||||
{
|
|
||||||
$app = $this->getApplication();
|
|
||||||
if (!$app->isClient('administrator')) return;
|
|
||||||
if (!$this->params->get('heartbeat_enabled', 1)) return;
|
|
||||||
|
|
||||||
$session = \Joomla\CMS\Factory::getSession();
|
|
||||||
if ($session->get('mokosuiteclient.heartbeat_sent', false)) return;
|
|
||||||
|
|
||||||
// Check if version changed since last heartbeat
|
|
||||||
$lastVersion = $this->params->get('_last_heartbeat_version', '');
|
|
||||||
$currentVersion = $this->getMokoSuiteClientVersion();
|
|
||||||
|
|
||||||
if ($lastVersion !== $currentVersion)
|
|
||||||
{
|
|
||||||
$session->set('mokosuiteclient.heartbeat_sent', true);
|
|
||||||
$this->sendHeartbeat();
|
|
||||||
|
|
||||||
// Store version so we don't re-send every session
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$this->params->set('_last_heartbeat_version', $currentVersion);
|
|
||||||
|
|
||||||
$extension = new \Joomla\CMS\Table\Extension(Factory::getDbo());
|
|
||||||
$extension->load(['element' => 'mokosuiteclient_monitor', 'folder' => 'system', 'type' => 'plugin']);
|
|
||||||
$extension->params = $this->params->toString();
|
|
||||||
$extension->store();
|
|
||||||
}
|
|
||||||
catch (\Throwable $e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send heartbeat immediately after package install/update.
|
|
||||||
*/
|
|
||||||
public function onExtensionAfterInstall($installer, $eid): void
|
|
||||||
{
|
|
||||||
if (!$this->params->get('heartbeat_enabled', 1)) return;
|
|
||||||
|
|
||||||
try { $this->sendHeartbeat(); }
|
|
||||||
catch (\Throwable $e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After saving this plugin or the core plugin, send heartbeat.
|
|
||||||
*/
|
|
||||||
public function onExtensionAfterSave($event): void
|
|
||||||
{
|
|
||||||
// Joomla 6: single event object; Joomla 5: individual args
|
|
||||||
if (is_object($event) && method_exists($event, 'getArgument'))
|
|
||||||
{
|
|
||||||
$context = $event->getArgument('context', $event->getArgument(0, ''));
|
|
||||||
$table = $event->getArgument('subject', $event->getArgument(1, null));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$context = $event;
|
|
||||||
$table = func_get_arg(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($context !== 'com_plugins.plugin' || !$table)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$element = $table->element ?? '';
|
|
||||||
|
|
||||||
if (!\in_array($element, ['mokosuiteclient', 'mokosuiteclient_monitor'], true))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->params->get('heartbeat_enabled', 1))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->sendHeartbeat();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send heartbeat to the MokoSuiteClientHQ control panel.
|
|
||||||
*
|
|
||||||
* The request is RSA-signed: the client signs domain|timestamp|token
|
|
||||||
* with its private key. Base verifies with the matching public key.
|
|
||||||
*/
|
|
||||||
private function sendHeartbeat(): void
|
|
||||||
{
|
|
||||||
$baseUrl = rtrim($this->params->get('base_url', ''), '/');
|
|
||||||
|
|
||||||
if (empty($baseUrl))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$coreParams = MokoSuiteClientHelper::getCoreParams();
|
|
||||||
$healthToken = $coreParams->get('health_api_token', '');
|
|
||||||
|
|
||||||
if (empty($healthToken))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$siteUrl = rtrim(Uri::root(), '/');
|
|
||||||
$domain = parse_url($siteUrl, PHP_URL_HOST) ?: '';
|
|
||||||
|
|
||||||
if (empty($domain))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = $this->getApplication();
|
|
||||||
$config = Factory::getConfig();
|
|
||||||
$timestamp = time();
|
|
||||||
|
|
||||||
$payload = [
|
|
||||||
'token' => $healthToken,
|
|
||||||
'domain' => $domain,
|
|
||||||
'site_name' => $config->get('sitename', 'Joomla'),
|
|
||||||
'site_url' => $siteUrl,
|
|
||||||
'joomla_version' => (new Version())->getShortVersion(),
|
|
||||||
'php_version' => PHP_VERSION,
|
|
||||||
'mokosuiteclient_version' => $this->getMokoSuiteClientVersion(),
|
|
||||||
'timestamp' => $timestamp,
|
|
||||||
'client_info' => [
|
|
||||||
'company' => $config->get('sitename', ''),
|
|
||||||
'email' => $config->get('mailfrom', ''),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Include live health data
|
|
||||||
$healthData = $this->fetchLocalHealth($siteUrl, $healthToken);
|
|
||||||
|
|
||||||
if ($healthData !== null)
|
|
||||||
{
|
|
||||||
$payload['health'] = $healthData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSA sign the request
|
|
||||||
$headers = ['Content-Type: application/json'];
|
|
||||||
$signature = $this->signRequest($domain, $timestamp, $healthToken);
|
|
||||||
|
|
||||||
if ($signature !== null)
|
|
||||||
{
|
|
||||||
$headers[] = 'X-MokoSuite-Signature: ' . $signature;
|
|
||||||
$headers[] = 'X-MokoSuite-Timestamp: ' . $timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
$endpoint = $baseUrl . '/api/index.php/v1/mokosuitehq/heartbeat';
|
|
||||||
$json = json_encode($payload, JSON_UNESCAPED_SLASHES);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$http = \Joomla\CMS\Http\HttpFactory::getHttp(
|
|
||||||
new \Joomla\Registry\Registry(['follow_location' => true, 'transport.curl' => ['certpath' => false]]),
|
|
||||||
['curl', 'stream']
|
|
||||||
);
|
|
||||||
|
|
||||||
$headerMap = [];
|
|
||||||
foreach ($headers as $h)
|
|
||||||
{
|
|
||||||
[$key, $val] = explode(': ', $h, 2);
|
|
||||||
$headerMap[$key] = $val;
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = $http->post($endpoint, $json, $headerMap, 15);
|
|
||||||
$code = $response->code;
|
|
||||||
$body = json_decode($response->body, true);
|
|
||||||
|
|
||||||
if ($code >= 200 && $code < 300)
|
|
||||||
{
|
|
||||||
$app->enqueueMessage(
|
|
||||||
'MokoSuiteClientHQ heartbeat: ' . ($body['status'] ?? 'ok'),
|
|
||||||
'message'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log::add(
|
|
||||||
\sprintf('Monitor heartbeat HTTP %d: %s', $code, $body['error'] ?? 'Unknown'),
|
|
||||||
Log::WARNING,
|
|
||||||
'mokosuiteclient'
|
|
||||||
);
|
|
||||||
$app->enqueueMessage(
|
|
||||||
'MokoSuiteClientHQ heartbeat failed (HTTP ' . $code . ')',
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (\Throwable $e)
|
|
||||||
{
|
|
||||||
Log::add('Monitor heartbeat failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RSA-sign the request message.
|
|
||||||
*
|
|
||||||
* @param string $domain Site domain.
|
|
||||||
* @param int $timestamp Unix timestamp.
|
|
||||||
* @param string $token Health API token.
|
|
||||||
*
|
|
||||||
* @return string|null Base64-encoded signature, or null if signing fails.
|
|
||||||
*/
|
|
||||||
private function signRequest(string $domain, int $timestamp, string $token): ?string
|
|
||||||
{
|
|
||||||
$signingKeyB64 = $this->params->get('signing_key', '');
|
|
||||||
|
|
||||||
// Fall back to manifest XML default if not yet saved in params
|
|
||||||
if (empty($signingKeyB64))
|
|
||||||
{
|
|
||||||
$manifestFile = JPATH_PLUGINS . '/system/mokosuiteclient_monitor/mokosuiteclient_monitor.xml';
|
|
||||||
|
|
||||||
if (is_file($manifestFile))
|
|
||||||
{
|
|
||||||
$xml = simplexml_load_file($manifestFile);
|
|
||||||
|
|
||||||
if ($xml)
|
|
||||||
{
|
|
||||||
foreach ($xml->xpath('//field[@name="signing_key"]') as $field)
|
|
||||||
{
|
|
||||||
$signingKeyB64 = (string) $field['default'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($signingKeyB64))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$privateKeyPem = base64_decode($signingKeyB64);
|
|
||||||
|
|
||||||
if (empty($privateKeyPem))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $domain . '|' . $timestamp . '|' . $token;
|
|
||||||
$privateKey = openssl_pkey_get_private($privateKeyPem);
|
|
||||||
|
|
||||||
if ($privateKey === false)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$signature = '';
|
|
||||||
|
|
||||||
if (openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256))
|
|
||||||
{
|
|
||||||
return base64_encode($signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch health data from the local site's health endpoint.
|
|
||||||
*/
|
|
||||||
private function fetchLocalHealth(string $siteUrl, string $healthToken): ?array
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$http = \Joomla\CMS\Http\HttpFactory::getHttp(
|
|
||||||
new \Joomla\Registry\Registry(['follow_location' => true, 'transport.curl' => ['certpath' => false]]),
|
|
||||||
['curl', 'stream']
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $http->get(
|
|
||||||
$siteUrl . '/?mokosuiteclient=health',
|
|
||||||
['Authorization' => 'Bearer ' . $healthToken, 'Accept' => 'application/json'],
|
|
||||||
10
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($response->code !== 200 || empty($response->body))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return json_decode($response->body, true) ?: null;
|
|
||||||
}
|
|
||||||
catch (\Throwable $e)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the installed MokoSuiteClient package version.
|
|
||||||
*/
|
|
||||||
private function getMokoSuiteClientVersion(): string
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$extension = new \Joomla\CMS\Table\Extension(Factory::getDbo());
|
|
||||||
$extension->load(['element' => 'pkg_mokosuiteclient', 'type' => 'package']);
|
|
||||||
$manifest = json_decode($extension->manifest_cache ?? '{}');
|
|
||||||
|
|
||||||
return $manifest->version ?? '';
|
|
||||||
}
|
|
||||||
catch (\Throwable $e)
|
|
||||||
{
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_OFFLINE_DESC</description>
|
<description>PLG_SYSTEM_MOKOSUITECLIENT_OFFLINE_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientOffline</namespace>
|
<namespace path="src">Moko\Plugin\System\MokoSuiteClientOffline</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_SYSTEM_MOKOSUITECLIENT_TENANT_DESC</description>
|
<description>PLG_SYSTEM_MOKOSUITECLIENT_TENANT_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\System\MokoSuiteClientTenant</namespace>
|
<namespace path="src">Moko\Plugin\System\MokoSuiteClientTenant</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
|
<description>Runs scheduled helpdesk automation rules — auto-close resolved tickets, SLA breach escalation, and time-based actions.</description>
|
||||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientTickets</namespace>
|
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientTickets</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ class TicketAutomation extends CMSPlugin implements SubscriberInterface
|
|||||||
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE',
|
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE',
|
||||||
'method' => 'runAutoClose',
|
'method' => 'runAutoClose',
|
||||||
],
|
],
|
||||||
|
'mokosuiteclient.license.validate' => [
|
||||||
|
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_VALIDATE',
|
||||||
|
'method' => 'runLicenseValidation',
|
||||||
|
],
|
||||||
|
'mokosuiteclient.license.heartbeat' => [
|
||||||
|
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_HEARTBEAT',
|
||||||
|
'method' => 'runLicenseHeartbeat',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $autoloadLanguage = true;
|
protected $autoloadLanguage = true;
|
||||||
@@ -310,4 +318,50 @@ class TicketAutomation extends CMSPlugin implements SubscriberInterface
|
|||||||
|
|
||||||
return trim($textBody);
|
return trim($textBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily license revalidation — refresh cached license status from MokoGitea.
|
||||||
|
* Recommended schedule: daily at 3:00 AM.
|
||||||
|
*/
|
||||||
|
private function runLicenseValidation(ExecuteTaskEvent $event): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::validate(true);
|
||||||
|
|
||||||
|
$status = $result->valid ? 'valid' : ($result->status ?? 'invalid');
|
||||||
|
$tier = $result->tier ?? 'none';
|
||||||
|
$entitlements = count($result->entitlements ?? []);
|
||||||
|
|
||||||
|
$this->logTask(sprintf(
|
||||||
|
'License validation: status=%s tier=%s entitlements=%d',
|
||||||
|
$status, $tier, $entitlements
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$result->valid) {
|
||||||
|
Log::add('License validation failed: ' . ($result->message ?? 'unknown'), Log::WARNING, 'mokosuite.license');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Status::OK;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logTask('License validation error: ' . $e->getMessage());
|
||||||
|
return Status::KNOCKOUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily heartbeat — report active installation to MokoGitea.
|
||||||
|
* Recommended schedule: daily at 4:00 AM.
|
||||||
|
*/
|
||||||
|
private function runLicenseHeartbeat(ExecuteTaskEvent $event): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::heartbeat();
|
||||||
|
|
||||||
|
$this->logTask('License heartbeat: ' . ($result->success ?? false ? 'sent' : ($result->error ?? 'failed')));
|
||||||
|
return Status::OK;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logTask('License heartbeat error: ' . $e->getMessage());
|
||||||
|
return Status::KNOCKOUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_TASK_MOKOSUITECLIENTDEMO_DESC</description>
|
<description>PLG_TASK_MOKOSUITECLIENTDEMO_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace>
|
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientDemo</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: MokoSuiteClient
|
* INGROUP: MokoSuiteClient
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
||||||
* PATH: /src/packages/plg_system_mokosuiteclient/Service/DemoResetService.php
|
* PATH: /src/packages/plg_system_mokosuiteclient/Service/DemoResetService.php
|
||||||
* VERSION: 02.45.00
|
* VERSION: 02.46.32
|
||||||
* BRIEF: Content-only snapshot/restore for demo site reset
|
* BRIEF: Content-only snapshot/restore for demo site reset
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>PLG_TASK_MOKOSUITECLIENTSYNC_DESC</description>
|
<description>PLG_TASK_MOKOSUITECLIENTSYNC_DESC</description>
|
||||||
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace>
|
<namespace path="src">Moko\Plugin\Task\MokoSuiteClientSync</namespace>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: MokoSuiteClient
|
* INGROUP: MokoSuiteClient
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
||||||
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncReceiver.php
|
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncReceiver.php
|
||||||
* VERSION: 02.45.00
|
* VERSION: 02.46.32
|
||||||
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
|
* BRIEF: Receiver-side content sync — applies incoming payload to local DB
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* INGROUP: MokoSuiteClient
|
* INGROUP: MokoSuiteClient
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient
|
||||||
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncService.php
|
* PATH: /src/packages/plg_system_mokosuiteclient/Service/ContentSyncService.php
|
||||||
* VERSION: 02.45.00
|
* VERSION: 02.46.32
|
||||||
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
|
* BRIEF: Sender-side content sync — builds payload and pushes to remote sites
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<license>GPL-3.0-or-later</license>
|
<license>GPL-3.0-or-later</license>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<description>Joomla Web Services API routes for MokoSuiteClient site management — health checks, cache, updates, backups, and site info.</description>
|
<description>Joomla Web Services API routes for MokoSuiteClient site management — health checks, cache, updates, backups, and site info.</description>
|
||||||
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteClient</namespace>
|
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteClient</namespace>
|
||||||
<files>
|
<files>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoSuiteClient</name>
|
<name>Package - MokoSuiteClient</name>
|
||||||
<packagename>mokosuiteclient</packagename>
|
<packagename>mokosuiteclient</packagename>
|
||||||
<version>02.45.00</version>
|
<version>02.46.32</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
<file type="module" id="mod_mokosuiteclient_menu" client="administrator">mod_mokosuiteclient_menu.zip</file>
|
<file type="module" id="mod_mokosuiteclient_menu" client="administrator">mod_mokosuiteclient_menu.zip</file>
|
||||||
<file type="module" id="mod_mokosuiteclient_cache" client="administrator">mod_mokosuiteclient_cache.zip</file>
|
<file type="module" id="mod_mokosuiteclient_cache" client="administrator">mod_mokosuiteclient_cache.zip</file>
|
||||||
<file type="module" id="mod_mokosuiteclient_categories" client="administrator">mod_mokosuiteclient_categories.zip</file>
|
<file type="module" id="mod_mokosuiteclient_categories" client="administrator">mod_mokosuiteclient_categories.zip</file>
|
||||||
<file type="plugin" id="plg_system_mokosuiteclient_backup" group="system">plg_system_mokosuiteclient_backup.zip</file>
|
|
||||||
<file type="plugin" id="plg_webservices_mokosuiteclient" group="webservices">plg_webservices_mokosuiteclient.zip</file>
|
<file type="plugin" id="plg_webservices_mokosuiteclient" group="webservices">plg_webservices_mokosuiteclient.zip</file>
|
||||||
<file type="plugin" id="plg_task_mokosuiteclientdemo" group="task">plg_task_mokosuiteclientdemo.zip</file>
|
<file type="plugin" id="plg_task_mokosuiteclientdemo" group="task">plg_task_mokosuiteclientdemo.zip</file>
|
||||||
<file type="plugin" id="plg_task_mokosuiteclientsync" group="task">plg_task_mokosuiteclientsync.zip</file>
|
<file type="plugin" id="plg_task_mokosuiteclientsync" group="task">plg_task_mokosuiteclientsync.zip</file>
|
||||||
|
|||||||
+20
-16
@@ -46,16 +46,18 @@ class Pkg_MokosuiteclientInstallerScript
|
|||||||
{
|
{
|
||||||
$this->saveDownloadKey();
|
$this->saveDownloadKey();
|
||||||
|
|
||||||
|
// Remove DEFAULT '' from element column — a previous version set this
|
||||||
|
// which caused Joomla to install plugin files to the group root
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getDbo();
|
||||||
$db->setQuery("ALTER TABLE " . $db->quoteName('#__extensions')
|
$db->setQuery("ALTER TABLE " . $db->quoteName('#__extensions')
|
||||||
. " MODIFY " . $db->quoteName('element') . " VARCHAR(100) NOT NULL DEFAULT ''");
|
. " MODIFY " . $db->quoteName('element') . " VARCHAR(100) NOT NULL");
|
||||||
$db->execute();
|
$db->execute();
|
||||||
}
|
}
|
||||||
catch (\Throwable $e)
|
catch (\Throwable $e)
|
||||||
{
|
{
|
||||||
// Non-fatal — column may already have a default
|
// Non-fatal — column may already be correct
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +84,6 @@ class Pkg_MokosuiteclientInstallerScript
|
|||||||
$this->enablePlugin('system', 'mokosuiteclient_devtools');
|
$this->enablePlugin('system', 'mokosuiteclient_devtools');
|
||||||
$this->enablePlugin('system', 'mokosuiteclient_offline');
|
$this->enablePlugin('system', 'mokosuiteclient_offline');
|
||||||
$this->enablePlugin('system', 'mokosuiteclient_dbip');
|
$this->enablePlugin('system', 'mokosuiteclient_dbip');
|
||||||
$this->enablePlugin('system', 'mokosuiteclient_backup');
|
|
||||||
$this->enablePlugin('webservices', 'mokosuiteclient');
|
$this->enablePlugin('webservices', 'mokosuiteclient');
|
||||||
$this->enablePlugin('task', 'mokosuiteclientdemo');
|
$this->enablePlugin('task', 'mokosuiteclientdemo');
|
||||||
$this->enablePlugin('task', 'mokosuiteclientsync');
|
$this->enablePlugin('task', 'mokosuiteclientsync');
|
||||||
@@ -112,18 +113,9 @@ class Pkg_MokosuiteclientInstallerScript
|
|||||||
// Mark MokoSuiteClient extensions as protected (prevents disable/uninstall at framework level)
|
// Mark MokoSuiteClient extensions as protected (prevents disable/uninstall at framework level)
|
||||||
$this->protectExtensions();
|
$this->protectExtensions();
|
||||||
|
|
||||||
// Migrate all Moko update server URLs to new format
|
|
||||||
$this->migrateUpdateServerUrls();
|
|
||||||
|
|
||||||
// Clean up stale/duplicate update sites
|
|
||||||
$this->cleanupStaleUpdateSites();
|
|
||||||
|
|
||||||
// Restore download key saved in preflight
|
// Restore download key saved in preflight
|
||||||
$this->restoreDownloadKey();
|
$this->restoreDownloadKey();
|
||||||
|
|
||||||
// Fix orphaned update records (extension_id=0)
|
|
||||||
$this->fixUpdateRecords();
|
|
||||||
|
|
||||||
// Trigger heartbeat registration
|
// Trigger heartbeat registration
|
||||||
$this->sendHeartbeat();
|
$this->sendHeartbeat();
|
||||||
|
|
||||||
@@ -464,6 +456,16 @@ class Pkg_MokosuiteclientInstallerScript
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Only enable if the plugin files actually exist on disk
|
||||||
|
$pluginDir = JPATH_PLUGINS . '/' . $group . '/' . $element;
|
||||||
|
|
||||||
|
if (!is_dir($pluginDir))
|
||||||
|
{
|
||||||
|
Log::add('Skipping enable for ' . $group . '/' . $element . ' — files not installed', Log::DEBUG, 'mokosuiteclient');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getDbo();
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->update($db->quoteName('#__extensions'))
|
->update($db->quoteName('#__extensions'))
|
||||||
@@ -535,7 +537,6 @@ class Pkg_MokosuiteclientInstallerScript
|
|||||||
$db->quote('mokosuiteclientdemo'),
|
$db->quote('mokosuiteclientdemo'),
|
||||||
$db->quote('mokosuiteclientsync'),
|
$db->quote('mokosuiteclientsync'),
|
||||||
$db->quote('mokosuiteclient_tickets'),
|
$db->quote('mokosuiteclient_tickets'),
|
||||||
$db->quote('mokosuiteclient_backup'),
|
|
||||||
$db->quote('mokoonyx'),
|
$db->quote('mokoonyx'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -547,8 +548,6 @@ class Pkg_MokosuiteclientInstallerScript
|
|||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
$db->execute();
|
$db->execute();
|
||||||
|
|
||||||
// Ensure update server stays enabled
|
|
||||||
$this->enableUpdateServer();
|
|
||||||
}
|
}
|
||||||
catch (\Throwable $e)
|
catch (\Throwable $e)
|
||||||
{
|
{
|
||||||
@@ -970,19 +969,24 @@ class Pkg_MokosuiteclientInstallerScript
|
|||||||
if ($error)
|
if ($error)
|
||||||
{
|
{
|
||||||
Log::add('Heartbeat connection failed: ' . $error, Log::WARNING, 'mokosuiteclient');
|
Log::add('Heartbeat connection failed: ' . $error, Log::WARNING, 'mokosuiteclient');
|
||||||
|
Factory::getApplication()->enqueueMessage('MokoSuiteHQ heartbeat failed: ' . $error, 'warning');
|
||||||
}
|
}
|
||||||
elseif ($code >= 200 && $code < 300)
|
elseif ($code >= 200 && $code < 300)
|
||||||
{
|
{
|
||||||
Factory::getApplication()->enqueueMessage('MokoSuiteClientHQ heartbeat: site registered', 'message');
|
Factory::getApplication()->enqueueMessage('MokoSuiteHQ heartbeat: site registered successfully.', 'message');
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
$body = json_decode($response, true);
|
||||||
|
$msg = $body['error'] ?? $body['message'] ?? ('HTTP ' . $code);
|
||||||
Log::add(sprintf('Heartbeat HTTP %d: %s', $code, $response), Log::WARNING, 'mokosuiteclient');
|
Log::add(sprintf('Heartbeat HTTP %d: %s', $code, $response), Log::WARNING, 'mokosuiteclient');
|
||||||
|
Factory::getApplication()->enqueueMessage('MokoSuiteHQ heartbeat failed: ' . $msg, 'warning');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (\Throwable $e)
|
catch (\Throwable $e)
|
||||||
{
|
{
|
||||||
Log::add('Heartbeat failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
Log::add('Heartbeat failed: ' . $e->getMessage(), Log::WARNING, 'mokosuiteclient');
|
||||||
|
Factory::getApplication()->enqueueMessage('MokoSuiteHQ heartbeat failed: ' . $e->getMessage(), 'warning');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user