Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26635f933c | |||
| 8d80c218da | |||
| 807da034c9 | |||
| e4d704dd84 | |||
| a805351dd1 | |||
| c5d4445bc1 | |||
| 5460c7b211 | |||
| 5ab6296ad5 | |||
| c1521cb235 | |||
| 25e06fd08c | |||
| 61abf72437 | |||
| 8c5ed1ed76 | |||
| e9d7889417 | |||
| 4c15f12426 | |||
| 8d082de47f | |||
| 41c1bb5d68 | |||
| 4f14009003 | |||
| 8e51abee54 | |||
| f884314e28 | |||
| e495c786fb | |||
| c3cbad1a00 | |||
| a34b715cff | |||
| 3ded91608a | |||
| 119379bb16 | |||
| 8eab341c1e | |||
| 70df427cfe | |||
| 2aee667d00 | |||
| 6bab1ad5fa | |||
| 5ee4f7a578 | |||
| 44e309c57f | |||
| 6ed0eee4a1 | |||
| 3d88281e72 | |||
| 1f225689ba | |||
| e5ca71f2c5 | |||
| 610f875ad9 | |||
| 507b2c9448 | |||
| aa36a01a7f |
@@ -5,7 +5,7 @@
|
|||||||
<display-name>Package - MokoJoomBackup</display-name>
|
<display-name>Package - MokoJoomBackup</display-name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
|
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
|
||||||
<version>01.00.00</version>
|
<version>01.01.07-dev</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
|
|
||||||
|
name: "Universal: Auto Version Bump"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- rc
|
||||||
|
- 'feature/**'
|
||||||
|
- 'patch/**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
name: Version Bump
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
|
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||||
|
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
if [ -d "/opt/moko-platform/cli" ]; then
|
||||||
|
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Bump version
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/version_auto_bump.php \
|
||||||
|
--path . --branch "${GITHUB_REF_NAME}" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
@@ -102,13 +102,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--skip-update-stream
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
@@ -167,7 +168,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability stable --bump minor --branch main \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--skip-update-stream
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoPlatform.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|
||||||
|
name: "Branch Cleanup"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Delete merged branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
github.event.pull_request.head.ref != 'dev' &&
|
||||||
|
github.event.pull_request.head.ref != 'main'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Delete source branch
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
|
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "$STATUS" = "204" ]; then
|
||||||
|
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "$STATUS" = "404" ]; then
|
||||||
|
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
fi
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: moko-platform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
name: "Universal: Repository Cleanup"
|
name: "Universal: Repository Cleanup"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# VERSION: 01.01.07
|
||||||
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
|
name: "Universal: Issue Branch"
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-branch:
|
||||||
|
name: Create feature branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create branch and comment
|
||||||
|
run: |
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|
||||||
|
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
||||||
|
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||||
|
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
||||||
|
|
||||||
|
# Check dev branch exists
|
||||||
|
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/branches/dev" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${DEV_EXISTS}" != "200" ]; then
|
||||||
|
echo "No dev branch -- skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create branch from dev
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/branches" \
|
||||||
|
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${HTTP}" = "201" ]; then
|
||||||
|
echo "Created branch: ${BRANCH}"
|
||||||
|
|
||||||
|
# Comment on issue with branch link
|
||||||
|
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||||
|
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||||
|
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/comments" \
|
||||||
|
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
|
||||||
|
|
||||||
|
echo "Commented on issue #${ISSUE_NUM}"
|
||||||
|
else
|
||||||
|
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
|
||||||
|
fi
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Notifications
|
# INGROUP: moko-platform.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
name: "Universal: Notifications"
|
name: "Universal: Notifications"
|
||||||
|
|||||||
@@ -105,6 +105,19 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found in source files"
|
||||||
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
@@ -134,6 +147,98 @@ jobs:
|
|||||||
echo "PHP lint: ${ERRORS} error(s)"
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
|
|
||||||
|
- name: Joomla JEXEC guard check
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
# Skip vendor, node_modules, and index.html stub files
|
||||||
|
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
||||||
|
# Check first 10 lines for JEXEC or JPATH guard
|
||||||
|
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
||||||
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "JEXEC guard: OK"
|
||||||
|
|
||||||
|
- name: Joomla directory listing protection
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
MISSING=0
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
|
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "Directory protection: ${MISSING} missing (advisory)"
|
||||||
|
|
||||||
|
- name: Joomla script file and asset checks
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
# Check scriptfile exists if declared
|
||||||
|
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
|
if [ -n "$SCRIPTFILE" ]; then
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
||||||
|
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Require joomla.asset.json and validate it
|
||||||
|
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$ASSET_JSON" ]; then
|
||||||
|
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
||||||
|
echo "::error::joomla.asset.json is not valid JSON"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
echo "joomla.asset.json: valid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate all XML files in src/ are well-formed
|
||||||
|
XML_ERRORS=0
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
while IFS= read -r -d '' xmlfile; do
|
||||||
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
||||||
|
XML_ERRORS=$((XML_ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
||||||
|
fi
|
||||||
|
if [ "$XML_ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "XML well-formedness: OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
echo "Joomla asset checks: OK"
|
||||||
|
|
||||||
- name: Validate platform manifest
|
- name: Validate platform manifest
|
||||||
run: |
|
run: |
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
@@ -151,6 +256,13 @@ jobs:
|
|||||||
for ELEMENT in name version description; do
|
for ELEMENT in name version description; do
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
done
|
done
|
||||||
|
# Block legacy raw/branch update server URLs on MokoGitea
|
||||||
|
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||||
|
if [ -n "$RAW_URLS" ]; then
|
||||||
|
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||||
|
echo "$RAW_URLS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "Joomla manifest valid"
|
echo "Joomla manifest valid"
|
||||||
;;
|
;;
|
||||||
dolibarr)
|
dolibarr)
|
||||||
@@ -183,6 +295,138 @@ jobs:
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
- name: Validate Joomla language files
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Require both en-GB and en-US language directories
|
||||||
|
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$LANG_ROOT" ]; then
|
||||||
|
echo "No language/ directory found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
||||||
|
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that en-GB and en-US have matching .ini files
|
||||||
|
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
||||||
|
[ ! -f "$GB_INI" ] && continue
|
||||||
|
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
||||||
|
if [ ! -f "$US_INI" ]; then
|
||||||
|
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
||||||
|
[ ! -f "$US_INI" ] && continue
|
||||||
|
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
||||||
|
if [ ! -f "$GB_INI" ]; then
|
||||||
|
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all .ini language files
|
||||||
|
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
||||||
|
if [ -z "$INI_FILES" ]; then
|
||||||
|
echo "No .ini language files found"
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
||||||
|
|
||||||
|
for FILE in $INI_FILES; do
|
||||||
|
FNAME=$(basename "$FILE")
|
||||||
|
LINENUM=0
|
||||||
|
SEEN_KEYS=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
LINENUM=$((LINENUM + 1))
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
echo "$line" | grep -qE '^\s*;' && continue
|
||||||
|
echo "$line" | grep -qE '^\s*$' && continue
|
||||||
|
|
||||||
|
# Must match KEY="VALUE" format
|
||||||
|
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract key and check for duplicates
|
||||||
|
KEY=$(echo "$line" | sed 's/=.*//')
|
||||||
|
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
SEEN_KEYS="${SEEN_KEYS}
|
||||||
|
${KEY}"
|
||||||
|
done < "$FILE"
|
||||||
|
|
||||||
|
echo " ${FILE}: checked ${LINENUM} lines"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cross-check en-GB vs en-US key consistency
|
||||||
|
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
||||||
|
for GB_FILE in "$GB_DIR"/*.ini; do
|
||||||
|
[ ! -f "$GB_FILE" ] && continue
|
||||||
|
FNAME=$(basename "$GB_FILE")
|
||||||
|
US_FILE="$US_DIR/$FNAME"
|
||||||
|
[ ! -f "$US_FILE" ] && continue
|
||||||
|
|
||||||
|
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
||||||
|
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
# Keys in en-GB but not en-US
|
||||||
|
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_US" ]; then
|
||||||
|
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
||||||
|
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keys in en-US but not en-GB
|
||||||
|
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_GB" ]; then
|
||||||
|
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
||||||
|
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Language File Validation"
|
||||||
|
echo "| Metric | Count |"
|
||||||
|
echo "|---|---|"
|
||||||
|
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
||||||
|
echo "| Errors | ${ERRORS} |"
|
||||||
|
echo "| Warnings | ${WARNINGS} |"
|
||||||
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Language files: OK (${WARNINGS} warning(s))"
|
||||||
|
|
||||||
- name: Check changelog has unreleased entry
|
- name: Check changelog has unreleased entry
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Release
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Pre-release channel'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- release-candidate
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve metadata and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) TAG="development" ;;
|
||||||
|
alpha) TAG="alpha" ;;
|
||||||
|
beta) TAG="beta" ;;
|
||||||
|
release-candidate) TAG="release-candidate" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Set stability suffix, bump preserves it, fix consistency
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
|
||||||
|
--branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Read final version (includes suffix, e.g. 01.02.15-dev)
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||||
|
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||||
|
|
||||||
|
# Commit version bump
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auto-detect element via manifest_element.php
|
||||||
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
|
--repo "${GITEA_REPO}" --github-output
|
||||||
|
|
||||||
|
# Read back element outputs
|
||||||
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
|
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
id: release
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
|
|
||||||
|
- name: Build package and upload
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
|
- name: Update updates.xml
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml -- skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Sync updates.xml to all branches"
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
|
||||||
|
for BRANCH in main dev; do
|
||||||
|
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||||
|
echo "Syncing updates.xml -> ${BRANCH}"
|
||||||
|
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
||||||
|
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
||||||
|
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
||||||
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git add updates.xml
|
||||||
|
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
||||||
|
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
||||||
|
fi
|
||||||
|
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
|
--token "${TOKEN}" \
|
||||||
|
--api-base "${API_BASE}"
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
name: "Universal: Security Audit"
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/update-server.yml
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||||
|
#
|
||||||
|
# Thin wrapper around moko-platform CLI tools.
|
||||||
|
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||||
|
#
|
||||||
|
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||||
|
|
||||||
|
name: "Update Server"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-xml:
|
||||||
|
name: Update Server
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||||
|
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve stability and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
# Configure git for bot pushes
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
# Determine stability from branch or manual input
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
|
STABILITY="rc"
|
||||||
|
elif [[ "$BRANCH" == beta/* ]]; then
|
||||||
|
STABILITY="beta"
|
||||||
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
|
STABILITY="alpha"
|
||||||
|
else
|
||||||
|
STABILITY="development"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gitea release tag per stability
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) TAG="development" ;;
|
||||||
|
alpha) TAG="alpha" ;;
|
||||||
|
beta) TAG="beta" ;;
|
||||||
|
rc) TAG="release-candidate" ;;
|
||||||
|
*) TAG="stable" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Bump patch, set platform suffix, fix consistency — version_bump preserves suffix
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
|
||||||
|
--branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Read final version (includes suffix, e.g. 01.02.15-dev)
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Commit version bump if changed
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Create release and upload package
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Create or update Gitea release
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
|
# Build package and upload
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
|
- name: Update updates.xml
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push updates.xml
|
||||||
|
git add updates.xml
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Sync updates.xml to main
|
||||||
|
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import base64, json, urllib.request, sys
|
||||||
|
with open('updates.xml', 'rb') as f:
|
||||||
|
content = base64.b64encode(f.read()).decode()
|
||||||
|
payload = json.dumps({
|
||||||
|
'content': content,
|
||||||
|
'sha': '${FILE_SHA}',
|
||||||
|
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||||
|
'branch': 'main'
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/contents/updates.xml',
|
||||||
|
data=payload, method='PUT',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${GITEA_TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
print('updates.xml synced to main')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SFTP deploy to dev server
|
||||||
|
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||||
|
env:
|
||||||
|
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# Permission check: admin or maintain role required
|
||||||
|
ACTOR="${{ github.actor }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
|
case "$PERMISSION" in
|
||||||
|
admin|maintain|write) ;;
|
||||||
|
*)
|
||||||
|
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||||
|
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
|
||||||
|
PORT="${DEV_PORT:-22}"
|
||||||
|
REMOTE="${DEV_PATH%/}"
|
||||||
|
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||||
|
if [ -n "$DEV_KEY" ]; then
|
||||||
|
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
DISPLAY="${VERSION}"
|
||||||
|
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
+29
-36
@@ -2,62 +2,55 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## 01.00.00 — 2026-06-02
|
## 01.01 — 2026-06-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Admin dashboard view as default landing page with status cards, quick actions, and system health checklist (#28)
|
||||||
|
- Console plugin (plg_console_mokobackup) — CLI commands: run, list, profiles, restore, cleanup (#29)
|
||||||
|
- Content plugin (plg_content_mokobackup) — auto-backup before extension install/update (#30)
|
||||||
|
- Actionlog plugin (plg_actionlog_mokobackup) — logs backup and profile actions to User Action Logs (#31)
|
||||||
|
- BackupEngine dispatches onMokoBackupAfterRun event for plugin listeners
|
||||||
|
- Update site notice on dashboard and post-install
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Renamed Kickstart to MokoRestore throughout
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- SQL update migration and error handling
|
||||||
|
- Removed orphaned scriptfile from component manifest
|
||||||
|
- Consolidated admin files into single files block
|
||||||
|
|
||||||
|
## 01.00 — 2026-06-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial package structure with component, system plugin, task plugin, and webservices plugin
|
- Initial package structure with component, system plugin, task plugin, and webservices plugin
|
||||||
- Joomla Scheduled Tasks integration (plg_task_mokobackup) — create multiple tasks, each running a different backup profile on its own schedule
|
- Joomla Scheduled Tasks integration (plg_task_mokobackup) — create multiple tasks, each running a different backup profile on its own schedule
|
||||||
- Individual form fields for all profile settings (no raw JSON)
|
- Individual form fields for all profile settings (no raw JSON)
|
||||||
- FTP/FTPS uploader with recursive directory creation, passive mode, SSL, and size verification
|
- FTP/FTPS uploader with recursive directory creation, passive mode, SSL, and size verification
|
||||||
- Google Drive uploader using OAuth2 refresh tokens and resumable upload API (5 MB chunks)
|
- Google Drive uploader using OAuth2 refresh tokens and resumable upload API
|
||||||
|
- S3-compatible remote storage: AWS S3, Wasabi, Backblaze B2, MinIO (#16)
|
||||||
- RemoteUploaderInterface for pluggable storage backends
|
- RemoteUploaderInterface for pluggable storage backends
|
||||||
- Remote upload integrated into BackupEngine as Step 3 after archive creation
|
- Remote upload integrated into BackupEngine with option to delete local copy after upload
|
||||||
- Option to delete local copy after successful remote upload (per-profile setting)
|
|
||||||
- Restore engine with file restoration and database import
|
- Restore engine with file restoration and database import
|
||||||
- Standalone Kickstart restore script (restore.php) — self-contained site restoration without Joomla, like Akeeba Kickstart
|
- MokoRestore standalone restore script — self-contained site restoration without Joomla
|
||||||
- "Include Restore Script" toggle per profile — wraps backup with restore.php + site-backup.zip
|
- "Include Restore Script" toggle per profile
|
||||||
- FileRestorer class with protected file handling (preserves configuration.php, .htaccess)
|
- FileRestorer with protected file handling (preserves configuration.php, .htaccess)
|
||||||
- DatabaseImporter with streaming line-by-line SQL execution and error tolerance
|
- DatabaseImporter with streaming line-by-line SQL execution and error tolerance
|
||||||
- Admin dashboard quickicon widget — backup status at a glance with warnings (#18)
|
- Admin dashboard quickicon widget — backup status at a glance with warnings (#18)
|
||||||
- Differential backups — only back up files changed since last full backup (#19)
|
- Differential backups — only back up files changed since last full backup (#19)
|
||||||
- DifferentialScanner: builds file manifests (path/size/mtime) and compares against base
|
- DifferentialScanner with file manifests stored in backup records
|
||||||
- File manifest stored in backup record for future differential comparisons
|
|
||||||
- Automatic full-backup fallback when no base manifest exists
|
|
||||||
- JPA archive format import for Akeeba Backup migration (#20)
|
- JPA archive format import for Akeeba Backup migration (#20)
|
||||||
- JpaUnarchiver: parses Akeeba JPA binary format (headers, gzip, permissions)
|
|
||||||
- RestoreEngine auto-detects JPA vs ZIP format
|
|
||||||
- AES-256 archive encryption with per-profile password (#17)
|
- AES-256 archive encryption with per-profile password (#17)
|
||||||
- Encrypted archive support in RestoreEngine (password parameter)
|
- SHA-256 checksum verification for backup integrity (#15)
|
||||||
- Encrypted archive support in Kickstart restore.php (password field in UI)
|
|
||||||
- SHA-256 checksum computed and stored after archive creation (#15)
|
|
||||||
- "Verify Integrity" toolbar button re-computes hash and compares against stored checksum
|
|
||||||
- S3-compatible remote storage: AWS S3, Wasabi, Backblaze B2, MinIO (#16)
|
|
||||||
- S3 uploader with AWS Signature V4, single PUT for files <= 100 MB, multipart for larger
|
|
||||||
- S3 fields in profile form with showon conditional visibility
|
|
||||||
- Akeeba importer now maps S3 credentials from Akeeba profiles
|
|
||||||
- Email notifications on backup success/failure via Joomla mailer (#14)
|
- Email notifications on backup success/failure via Joomla mailer (#14)
|
||||||
- Per-profile notification settings: recipient emails, notify on success/failure
|
- Akeeba Backup Pro importer — profiles, filters, remote storage, and backup history
|
||||||
- Failure emails include last 30 lines of backup log for debugging
|
|
||||||
- mcp_mokobackup MCP server updated with MokoBackupClient for dual-backend support (#21)
|
|
||||||
- Akeeba Backup Pro importer — imports profiles, filters, remote storage settings, and backup history
|
|
||||||
- Auto-disables Akeeba plugins and scheduled tasks after successful import
|
- Auto-disables Akeeba plugins and scheduled tasks after successful import
|
||||||
- "Import from Akeeba" toolbar button in Profiles view (only shown when Akeeba tables detected)
|
|
||||||
- Supports both INI-format and JSON-format Akeeba configuration parsing
|
|
||||||
- Maps Akeeba filter format (per-root, nested) to newline-separated exclusion fields
|
|
||||||
- Profile selector dropdown in Backup Records view for choosing which profile to run
|
|
||||||
- AJAX step-based backup engine for shared hosting (overcomes max_execution_time)
|
- AJAX step-based backup engine for shared hosting (overcomes max_execution_time)
|
||||||
- SteppedBackupEngine: breaks backup into per-table DB dumps and file batches
|
|
||||||
- SteppedSession: persistent state between AJAX requests via temp JSON files
|
|
||||||
- Progress bar modal in admin UI with real-time phase/percentage updates
|
- Progress bar modal in admin UI with real-time phase/percentage updates
|
||||||
- AjaxController for init/step endpoints with CSRF protection
|
|
||||||
- Per-profile archive settings: format, compression level, split size, backup directory
|
- Per-profile archive settings: format, compression level, split size, backup directory
|
||||||
- Backup engine with step-based execution for large sites
|
- Backup engine with database dumper, file scanner, and ZIP archive builder
|
||||||
- Database dumper with table-level granularity
|
|
||||||
- File scanner with directory exclusion filters
|
|
||||||
- ZIP archive builder
|
|
||||||
- Backup profiles with independent configurations
|
- Backup profiles with independent configurations
|
||||||
- Backup record management (list, download, delete)
|
- Backup record management (list, download, delete)
|
||||||
- Admin dashboard with backup history
|
|
||||||
- CLI script for cron/scheduled backups
|
- CLI script for cron/scheduled backups
|
||||||
- REST API compatible with MokoBackup MCP server
|
- REST API compatible with MokoBackup MCP server
|
||||||
- System plugin for automatic backup cleanup with configurable retention
|
- System plugin for automatic backup cleanup with configurable retention
|
||||||
|
|||||||
@@ -3,43 +3,29 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# MokoJoomBackup — Full-site backup and restore for Joomla
|
# MokoJoomBackup — Full-site backup and restore for Joomla
|
||||||
|
#
|
||||||
|
# Builds and releases are handled by CI workflows (pre-release.yml,
|
||||||
|
# auto-release.yml). This Makefile provides local validation helpers
|
||||||
|
# and workflow dispatch shortcuts.
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CONFIGURATION - Customize these for your extension
|
# CONFIGURATION
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# Extension Configuration
|
|
||||||
EXTENSION_NAME := mokobackup
|
EXTENSION_NAME := mokobackup
|
||||||
EXTENSION_TYPE := package
|
EXTENSION_TYPE := package
|
||||||
# Options: module, plugin, component, package, template
|
|
||||||
EXTENSION_VERSION := 1.0.0
|
|
||||||
|
|
||||||
# Module Configuration (for modules only)
|
|
||||||
MODULE_TYPE := site
|
|
||||||
# Options: site, admin
|
|
||||||
|
|
||||||
# Plugin Configuration (for plugins only)
|
|
||||||
PLUGIN_GROUP := system
|
|
||||||
# Options: system, content, user, authentication, etc.
|
|
||||||
|
|
||||||
# Directories
|
|
||||||
SRC_DIR := src
|
SRC_DIR := src
|
||||||
BUILD_DIR := build
|
|
||||||
DIST_DIR := dist
|
|
||||||
DOCS_DIR := docs
|
|
||||||
|
|
||||||
# Joomla Installation (for local testing - customize paths)
|
# Gitea
|
||||||
JOOMLA_ROOT := /var/www/html/joomla
|
GITEA_URL := https://git.mokoconsulting.tech
|
||||||
JOOMLA_VERSION := 4
|
GITEA_ORG := MokoConsulting
|
||||||
|
GITEA_REPO := MokoJoomBackup
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
PHP := php
|
PHP := php
|
||||||
COMPOSER := composer
|
COMPOSER := composer
|
||||||
NPM := npm
|
|
||||||
PHPCS := vendor/bin/phpcs
|
PHPCS := vendor/bin/phpcs
|
||||||
PHPCBF := vendor/bin/phpcbf
|
|
||||||
PHPUNIT := vendor/bin/phpunit
|
|
||||||
ZIP := zip
|
|
||||||
|
|
||||||
# Coding Standards
|
# Coding Standards
|
||||||
PHPCS_STANDARD := Joomla
|
PHPCS_STANDARD := Joomla
|
||||||
@@ -58,146 +44,122 @@ COLOR_RED := \033[31m
|
|||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Show this help message
|
help: ## Show this help message
|
||||||
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
||||||
@echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)║ MokoJoomBackup Makefile ║$(COLOR_RESET)"
|
||||||
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
|
|
||||||
@echo ""
|
|
||||||
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
.PHONY: install-deps
|
# -- Validation ----------------------------------------------------------------
|
||||||
install-deps: ## Install all dependencies (Composer + npm)
|
|
||||||
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) install; \
|
|
||||||
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: ## Run PHP linter (syntax check)
|
lint: ## Run PHP syntax check on all source files
|
||||||
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
|
||||||
@find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
|
@ERROR=0; \
|
||||||
-exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
|
find $(SRC_DIR) -name "*.php" -exec $(PHP) -l {} \; 2>&1 | grep -v "No syntax errors" || true; \
|
||||||
|
if find $(SRC_DIR) -name "*.php" -exec $(PHP) -l {} \; 2>&1 | grep -q "Parse error"; then \
|
||||||
|
echo "$(COLOR_RED)✗ Syntax errors found$(COLOR_RESET)"; exit 1; \
|
||||||
|
fi
|
||||||
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
|
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
|
||||||
|
|
||||||
.PHONY: phpcs
|
.PHONY: phpcs
|
||||||
phpcs: ## Run PHP CodeSniffer (Joomla standards)
|
phpcs: ## Run PHP CodeSniffer (Joomla standards)
|
||||||
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
|
||||||
@if [ -f "$(PHPCS)" ]; then \
|
@if [ -f "$(PHPCS)" ]; then \
|
||||||
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
|
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php $(SRC_DIR); \
|
||||||
else \
|
else \
|
||||||
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
|
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: composer install$(COLOR_RESET)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: validate
|
.PHONY: validate
|
||||||
validate: lint phpcs ## Run all validation checks
|
validate: lint ## Run all local validation checks
|
||||||
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
|
@echo "$(COLOR_GREEN)✓ Validation passed$(COLOR_RESET)"
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: validate-xml
|
||||||
clean: ## Clean build artifacts
|
validate-xml: ## Validate all XML manifests are well-formed
|
||||||
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)Validating XML manifests...$(COLOR_RESET)"
|
||||||
@rm -rf $(BUILD_DIR) $(DIST_DIR)
|
@ERROR=0; \
|
||||||
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
|
for f in $$(find $(SRC_DIR) -name "*.xml"); do \
|
||||||
|
$(PHP) -r "new SimpleXMLElement(file_get_contents('$$f'));" 2>/dev/null \
|
||||||
|
|| { echo "$(COLOR_RED)✗ Invalid XML: $$f$(COLOR_RESET)"; ERROR=1; }; \
|
||||||
|
done; \
|
||||||
|
[ $$ERROR -eq 0 ] && echo "$(COLOR_GREEN)✓ All XML manifests valid$(COLOR_RESET)" || exit 1
|
||||||
|
|
||||||
|
# -- Dependencies --------------------------------------------------------------
|
||||||
|
|
||||||
|
.PHONY: install-deps
|
||||||
|
install-deps: ## Install PHP dependencies via Composer
|
||||||
|
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
|
||||||
|
@if [ -f "composer.json" ]; then \
|
||||||
|
$(COMPOSER) install; \
|
||||||
|
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: security-check
|
||||||
|
security-check: ## Run security audit on dependencies
|
||||||
|
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
|
||||||
|
@if [ -f "composer.json" ]; then \
|
||||||
|
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -- Minify --------------------------------------------------------------------
|
||||||
|
|
||||||
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
|
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
|
||||||
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
||||||
|
|
||||||
.PHONY: minify
|
.PHONY: minify
|
||||||
minify: ## Minify CSS/JS assets
|
minify: ## Minify CSS/JS assets
|
||||||
@echo "Minifying assets..."
|
@echo "$(COLOR_BLUE)Minifying assets...$(COLOR_RESET)"
|
||||||
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
||||||
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
||||||
elif [ -f "scripts/minify.js" ]; then \
|
elif [ -f "scripts/minify.js" ]; then \
|
||||||
node scripts/minify.js; \
|
node scripts/minify.js; \
|
||||||
else \
|
else \
|
||||||
echo "No minify script found"; \
|
echo "$(COLOR_YELLOW)⚠ No minify script found$(COLOR_RESET)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: build
|
# -- Release (CI workflow dispatch) --------------------------------------------
|
||||||
build: clean validate minify ## Build extension package
|
|
||||||
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
|
|
||||||
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
|
|
||||||
|
|
||||||
# Determine package prefix based on extension type
|
|
||||||
@case "$(EXTENSION_TYPE)" in \
|
|
||||||
module) \
|
|
||||||
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
plugin) \
|
|
||||||
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
component) \
|
|
||||||
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
package) \
|
|
||||||
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
template) \
|
|
||||||
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
*) \
|
|
||||||
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
|
|
||||||
exit 1; \
|
|
||||||
;; \
|
|
||||||
esac; \
|
|
||||||
\
|
|
||||||
mkdir -p "$$BUILD_TARGET"; \
|
|
||||||
\
|
|
||||||
echo "Building $$PACKAGE_PREFIX..."; \
|
|
||||||
\
|
|
||||||
rsync -av --progress \
|
|
||||||
--exclude='$(BUILD_DIR)' \
|
|
||||||
--exclude='$(DIST_DIR)' \
|
|
||||||
--exclude='.git*' \
|
|
||||||
--exclude='vendor/' \
|
|
||||||
--exclude='node_modules/' \
|
|
||||||
--exclude='tests/' \
|
|
||||||
--exclude='Makefile' \
|
|
||||||
--exclude='composer.json' \
|
|
||||||
--exclude='composer.lock' \
|
|
||||||
--exclude='package.json' \
|
|
||||||
--exclude='package-lock.json' \
|
|
||||||
--exclude='phpunit.xml' \
|
|
||||||
--exclude='*.md' \
|
|
||||||
--exclude='.editorconfig' \
|
|
||||||
. "$$BUILD_TARGET/"; \
|
|
||||||
\
|
|
||||||
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
|
|
||||||
\
|
|
||||||
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: package
|
|
||||||
package: build ## Alias for build
|
|
||||||
@echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release: validate build ## Create a release (validate + build)
|
release: validate validate-xml ## Trigger pre-release build via CI workflow
|
||||||
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)Triggering pre-release workflow...$(COLOR_RESET)"
|
||||||
|
@if ! command -v curl >/dev/null 2>&1; then \
|
||||||
|
echo "$(COLOR_RED)✗ curl required$(COLOR_RESET)"; exit 1; \
|
||||||
|
fi
|
||||||
|
@if [ -z "$$MOKOGITEA_TOKEN" ]; then \
|
||||||
|
echo "$(COLOR_RED)✗ MOKOGITEA_TOKEN not set$(COLOR_RESET)"; exit 1; \
|
||||||
|
fi
|
||||||
|
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token $$MOKOGITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$(GITEA_URL)/api/v1/repos/$(GITEA_ORG)/$(GITEA_REPO)/actions/workflows/pre-release.yml/dispatches" \
|
||||||
|
-d "{\"ref\":\"$$BRANCH\",\"inputs\":{\"stability\":\"development\"}}" \
|
||||||
|
&& echo "$(COLOR_GREEN)✓ Pre-release dispatched on $$BRANCH (development channel)$(COLOR_RESET)" \
|
||||||
|
|| { echo "$(COLOR_RED)✗ Dispatch failed$(COLOR_RESET)"; exit 1; }
|
||||||
|
|
||||||
|
.PHONY: release-rc
|
||||||
|
release-rc: validate validate-xml ## Trigger release-candidate build via CI workflow
|
||||||
|
@echo "$(COLOR_BLUE)Triggering RC pre-release workflow...$(COLOR_RESET)"
|
||||||
|
@if [ -z "$$MOKOGITEA_TOKEN" ]; then \
|
||||||
|
echo "$(COLOR_RED)✗ MOKOGITEA_TOKEN not set$(COLOR_RESET)"; exit 1; \
|
||||||
|
fi
|
||||||
|
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token $$MOKOGITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$(GITEA_URL)/api/v1/repos/$(GITEA_ORG)/$(GITEA_REPO)/actions/workflows/pre-release.yml/dispatches" \
|
||||||
|
-d "{\"ref\":\"$$BRANCH\",\"inputs\":{\"stability\":\"release-candidate\"}}" \
|
||||||
|
&& echo "$(COLOR_GREEN)✓ Pre-release dispatched on $$BRANCH (release-candidate channel)$(COLOR_RESET)" \
|
||||||
|
|| { echo "$(COLOR_RED)✗ Dispatch failed$(COLOR_RESET)"; exit 1; }
|
||||||
|
|
||||||
|
# -- Info ----------------------------------------------------------------------
|
||||||
|
|
||||||
.PHONY: version
|
.PHONY: version
|
||||||
version: ## Display version information
|
version: ## Display version from package manifest
|
||||||
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
|
@VERSION=$$(grep '<version>' $(SRC_DIR)/pkg_mokobackup.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/'); \
|
||||||
@echo " Name: $(EXTENSION_NAME)"
|
echo "$(COLOR_BLUE)$(EXTENSION_NAME)$(COLOR_RESET) v$$VERSION ($(EXTENSION_TYPE))"
|
||||||
@echo " Type: $(EXTENSION_TYPE)"
|
|
||||||
@echo " Version: $(EXTENSION_VERSION)"
|
|
||||||
|
|
||||||
.PHONY: security-check
|
|
||||||
security-check: ## Run security checks on dependencies
|
|
||||||
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: install-deps validate build ## Run complete build pipeline
|
|
||||||
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
|
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoJoomBackup
|
# MokoJoomBackup
|
||||||
|
|
||||||
<!-- VERSION: 01.00.00 -->
|
<!-- VERSION: 01.01.07 -->
|
||||||
|
|
||||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
PKG_MOKOBACKUP="Package - MokoJoomBackup"
|
PKG_MOKOBACKUP="Package - MokoJoomBackup"
|
||||||
PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||||
PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
||||||
|
PKG_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
PKG_MOKOBACKUP="Package - MokoJoomBackup"
|
PKG_MOKOBACKUP="Package - MokoJoomBackup"
|
||||||
PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||||
PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
||||||
|
PKG_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage com_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
-->
|
||||||
|
<config>
|
||||||
|
<fieldset name="general" label="COM_MOKOBACKUP_CONFIG_GENERAL">
|
||||||
|
<field
|
||||||
|
name="default_backup_dir"
|
||||||
|
type="text"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC"
|
||||||
|
default="administrator/components/com_mokobackup/backups"
|
||||||
|
filter="path"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="default_profile"
|
||||||
|
type="sql"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE_DESC"
|
||||||
|
query="SELECT id AS value, title AS text FROM #__mokobackup_profiles WHERE published = 1 ORDER BY ordering ASC"
|
||||||
|
default="1"
|
||||||
|
>
|
||||||
|
<option value="1">Default Backup Profile</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="show_update_notice"
|
||||||
|
type="radio"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC"
|
||||||
|
default="1"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset name="cleanup" label="COM_MOKOBACKUP_CONFIG_CLEANUP">
|
||||||
|
<field
|
||||||
|
name="max_age_days"
|
||||||
|
type="number"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_MAX_AGE"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_MAX_AGE_DESC"
|
||||||
|
default="30"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="max_backups"
|
||||||
|
type="number"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_MAX_BACKUPS"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_MAX_BACKUPS_DESC"
|
||||||
|
default="10"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset name="notifications" label="COM_MOKOBACKUP_CONFIG_NOTIFICATIONS">
|
||||||
|
<field
|
||||||
|
name="notify_email"
|
||||||
|
type="text"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
|
<field
|
||||||
|
name="notify_on_success"
|
||||||
|
type="radio"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS_DESC"
|
||||||
|
default="0"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="notify_on_failure"
|
||||||
|
type="radio"
|
||||||
|
label="COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE"
|
||||||
|
description="COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE_DESC"
|
||||||
|
default="1"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset name="permissions" label="JCONFIG_PERMISSIONS_LABEL"
|
||||||
|
description="JCONFIG_PERMISSIONS_DESC">
|
||||||
|
<field
|
||||||
|
name="rules"
|
||||||
|
type="rules"
|
||||||
|
label="JCONFIG_PERMISSIONS_LABEL"
|
||||||
|
filter="rules"
|
||||||
|
validate="rules"
|
||||||
|
component="com_mokobackup"
|
||||||
|
section="component"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</config>
|
||||||
@@ -70,10 +70,10 @@
|
|||||||
maxlength="512"
|
maxlength="512"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
name="include_kickstart"
|
name="include_mokorestore"
|
||||||
type="radio"
|
type="radio"
|
||||||
label="COM_MOKOBACKUP_FIELD_INCLUDE_KICKSTART"
|
label="COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE"
|
||||||
description="COM_MOKOBACKUP_FIELD_INCLUDE_KICKSTART_DESC"
|
description="COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC"
|
||||||
default="0"
|
default="0"
|
||||||
class="btn-group"
|
class="btn-group"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,9 +8,24 @@ COM_MOKOBACKUP="MokoJoomBackup"
|
|||||||
COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
||||||
|
|
||||||
; Submenu
|
; Submenu
|
||||||
|
COM_MOKOBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||||
COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records"
|
COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||||
COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||||
|
|
||||||
|
; Dashboard view
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_STORAGE="Storage Used"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
||||||
|
|
||||||
; Backups view
|
; Backups view
|
||||||
COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records"
|
COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records"
|
||||||
COM_MOKOBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
|
COM_MOKOBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
|
||||||
@@ -75,8 +90,8 @@ COM_MOKOBACKUP_FIELD_SPLIT_SIZE="Split Size (MB)"
|
|||||||
COM_MOKOBACKUP_FIELD_SPLIT_SIZE_DESC="Split archive into parts of this size in MB. 0 = no splitting."
|
COM_MOKOBACKUP_FIELD_SPLIT_SIZE_DESC="Split archive into parts of this size in MB. 0 = no splitting."
|
||||||
COM_MOKOBACKUP_FIELD_BACKUP_DIR="Backup Directory"
|
COM_MOKOBACKUP_FIELD_BACKUP_DIR="Backup Directory"
|
||||||
COM_MOKOBACKUP_FIELD_BACKUP_DIR_DESC="Relative path from Joomla root where backup archives are stored"
|
COM_MOKOBACKUP_FIELD_BACKUP_DIR_DESC="Relative path from Joomla root where backup archives are stored"
|
||||||
COM_MOKOBACKUP_FIELD_INCLUDE_KICKSTART="Include Restore Script"
|
COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE="Include Restore Script"
|
||||||
COM_MOKOBACKUP_FIELD_INCLUDE_KICKSTART_DESC="Include a standalone restore.php inside the backup archive. This creates a self-contained package that can restore the site on a blank server without Joomla installed — like Akeeba Kickstart."
|
COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="Include MokoRestore (standalone restore.php) inside the backup archive. Creates a self-contained package that can restore the site on a blank server without Joomla installed."
|
||||||
|
|
||||||
; Exclusion filter fields
|
; Exclusion filter fields
|
||||||
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS="Exclude Directories"
|
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS="Exclude Directories"
|
||||||
@@ -189,6 +204,32 @@ COM_MOKOBACKUP_FIELD_S3_PATH_DESC="Optional path prefix inside the bucket (e.g.
|
|||||||
COM_MOKOBACKUP_TOOLBAR_IMPORT_AKEEBA="Import from Akeeba"
|
COM_MOKOBACKUP_TOOLBAR_IMPORT_AKEEBA="Import from Akeeba"
|
||||||
COM_MOKOBACKUP_AKEEBA_NOT_FOUND="Akeeba Backup tables not found. Is Akeeba Backup Pro installed?"
|
COM_MOKOBACKUP_AKEEBA_NOT_FOUND="Akeeba Backup tables not found. Is Akeeba Backup Pro installed?"
|
||||||
|
|
||||||
|
; Update site notice
|
||||||
|
COM_MOKOBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key."
|
||||||
|
COM_MOKOBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server."
|
||||||
|
COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||||
|
|
||||||
|
; Component Options (config.xml)
|
||||||
|
COM_MOKOBACKUP_CONFIG_GENERAL="General"
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR="Default Backup Directory"
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC="Default directory for backup archives, relative to Joomla root. Can be overridden per profile."
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE="Default Profile"
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE_DESC="Default backup profile used by quick actions and CLI when no profile is specified."
|
||||||
|
COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE="Show Update Site Notice"
|
||||||
|
COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site configuration notice on the Backup Records view."
|
||||||
|
COM_MOKOBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_AGE_DESC="Default maximum age for backup records. Used by the system plugin and CLI cleanup command."
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_BACKUPS="Max Backup Count"
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_BACKUPS_DESC="Default maximum number of completed backups to retain."
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFICATIONS="Notifications"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL="Global Notification Email(s)"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses for global backup notifications. Per-profile settings override this."
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS="Notify on Success"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS_DESC="Send email when any backup completes successfully (unless overridden by profile)."
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE="Notify on Failure"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE_DESC="Send email when any backup fails (unless overridden by profile)."
|
||||||
|
|
||||||
; Errors
|
; Errors
|
||||||
COM_MOKOBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted."
|
COM_MOKOBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted."
|
||||||
COM_MOKOBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore."
|
COM_MOKOBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore."
|
||||||
|
|||||||
@@ -6,10 +6,45 @@
|
|||||||
|
|
||||||
COM_MOKOBACKUP="MokoJoomBackup"
|
COM_MOKOBACKUP="MokoJoomBackup"
|
||||||
COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
COM_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
||||||
|
COM_MOKOBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||||
COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records"
|
COM_MOKOBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||||
COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
COM_MOKOBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_STORAGE="Storage Used"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
||||||
|
COM_MOKOBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
||||||
COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records"
|
COM_MOKOBACKUP_BACKUPS_TITLE="Backup Records"
|
||||||
COM_MOKOBACKUP_PROFILES_TITLE="Backup Profiles"
|
COM_MOKOBACKUP_PROFILES_TITLE="Backup Profiles"
|
||||||
COM_MOKOBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
|
COM_MOKOBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
|
||||||
COM_MOKOBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
COM_MOKOBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
||||||
COM_MOKOBACKUP_NO_PROFILES="No backup profiles found."
|
COM_MOKOBACKUP_NO_PROFILES="No backup profiles found."
|
||||||
|
COM_MOKOBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key."
|
||||||
|
COM_MOKOBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server."
|
||||||
|
COM_MOKOBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||||
|
COM_MOKOBACKUP_CONFIG_GENERAL="General"
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR="Default Backup Directory"
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC="Default directory for backup archives, relative to Joomla root. Can be overridden per profile."
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE="Default Profile"
|
||||||
|
COM_MOKOBACKUP_CONFIG_DEFAULT_PROFILE_DESC="Default backup profile used by quick actions and CLI when no profile is specified."
|
||||||
|
COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE="Show Update Site Notice"
|
||||||
|
COM_MOKOBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site configuration notice on the Backup Records view."
|
||||||
|
COM_MOKOBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_AGE_DESC="Default maximum age for backup records. Used by the system plugin and CLI cleanup command."
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_BACKUPS="Max Backup Count"
|
||||||
|
COM_MOKOBACKUP_CONFIG_MAX_BACKUPS_DESC="Default maximum number of completed backups to retain."
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFICATIONS="Notifications"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL="Global Notification Email(s)"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses for global backup notifications. Per-profile settings override this."
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS="Notify on Success"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_SUCCESS_DESC="Send email when any backup completes successfully (unless overridden by profile)."
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE="Notify on Failure"
|
||||||
|
COM_MOKOBACKUP_CONFIG_NOTIFY_FAILURE_DESC="Send email when any backup fails (unless overridden by profile)."
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokobackup</name>
|
<name>com_mokobackup</name>
|
||||||
<version>01.00.00</version>
|
<version>01.01.07-dev</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>
|
||||||
@@ -19,8 +19,6 @@
|
|||||||
|
|
||||||
<namespace path="src">Joomla\Component\MokoBackup</namespace>
|
<namespace path="src">Joomla\Component\MokoBackup</namespace>
|
||||||
|
|
||||||
<scriptfile>script.php</scriptfile>
|
|
||||||
|
|
||||||
<install>
|
<install>
|
||||||
<sql>
|
<sql>
|
||||||
<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
|
<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
|
||||||
@@ -40,45 +38,24 @@
|
|||||||
</update>
|
</update>
|
||||||
|
|
||||||
<administration>
|
<administration>
|
||||||
<files folder="services">
|
<menu img="class:archive">COM_MOKOBACKUP</menu>
|
||||||
<filename>provider.php</filename>
|
<submenu>
|
||||||
</files>
|
<menu link="option=com_mokobackup&view=dashboard" img="class:archive">COM_MOKOBACKUP_SUBMENU_DASHBOARD</menu>
|
||||||
<files folder="src">
|
<menu link="option=com_mokobackup&view=backups" img="class:database">COM_MOKOBACKUP_SUBMENU_BACKUPS</menu>
|
||||||
<folder>Controller</folder>
|
<menu link="option=com_mokobackup&view=profiles" img="class:cog">COM_MOKOBACKUP_SUBMENU_PROFILES</menu>
|
||||||
<folder>Engine</folder>
|
</submenu>
|
||||||
<folder>Extension</folder>
|
<files folder=".">
|
||||||
<folder>Model</folder>
|
<folder>cli</folder>
|
||||||
<folder>Table</folder>
|
<folder>forms</folder>
|
||||||
<folder>View</folder>
|
<folder>services</folder>
|
||||||
</files>
|
<folder>sql</folder>
|
||||||
<files folder="forms">
|
<folder>src</folder>
|
||||||
<filename>backup.xml</filename>
|
<folder>tmpl</folder>
|
||||||
<filename>profile.xml</filename>
|
|
||||||
<filename>filter_backups.xml</filename>
|
|
||||||
<filename>filter_profiles.xml</filename>
|
|
||||||
</files>
|
|
||||||
<files folder="tmpl">
|
|
||||||
<folder>backups</folder>
|
|
||||||
<folder>backup</folder>
|
|
||||||
<folder>profiles</folder>
|
|
||||||
<folder>profile</folder>
|
|
||||||
</files>
|
|
||||||
<files folder="sql">
|
|
||||||
<folder>mysql</folder>
|
|
||||||
<folder>updates</folder>
|
|
||||||
</files>
|
|
||||||
<files folder="cli">
|
|
||||||
<filename>mokobackup.php</filename>
|
|
||||||
</files>
|
</files>
|
||||||
<languages folder="language">
|
<languages folder="language">
|
||||||
<language tag="en-GB">en-GB/com_mokobackup.ini</language>
|
<language tag="en-GB">en-GB/com_mokobackup.ini</language>
|
||||||
<language tag="en-GB">en-GB/com_mokobackup.sys.ini</language>
|
<language tag="en-GB">en-GB/com_mokobackup.sys.ini</language>
|
||||||
</languages>
|
</languages>
|
||||||
<menu img="class:archive">COM_MOKOBACKUP</menu>
|
|
||||||
<submenu>
|
|
||||||
<menu link="option=com_mokobackup&view=backups" img="class:database">COM_MOKOBACKUP_SUBMENU_BACKUPS</menu>
|
|
||||||
<menu link="option=com_mokobackup&view=profiles" img="class:cog">COM_MOKOBACKUP_SUBMENU_PROFILES</menu>
|
|
||||||
</submenu>
|
|
||||||
</administration>
|
</administration>
|
||||||
|
|
||||||
<api>
|
<api>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS `#__mokobackup_profiles` (
|
|||||||
`s3_path` VARCHAR(512) NOT NULL DEFAULT '/backups',
|
`s3_path` VARCHAR(512) NOT NULL DEFAULT '/backups',
|
||||||
`remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
`remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
||||||
`encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)',
|
`encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)',
|
||||||
`include_kickstart` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include standalone restore.php in archive',
|
`include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive',
|
||||||
`notify_email` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Comma-separated notification emails',
|
`notify_email` VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Comma-separated notification emails',
|
||||||
`notify_on_success` TINYINT(1) NOT NULL DEFAULT 0,
|
`notify_on_success` TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
`notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1,
|
`notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `#__mokobackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive';
|
||||||
@@ -16,5 +16,5 @@ use Joomla\CMS\MVC\Controller\BaseController;
|
|||||||
|
|
||||||
class DisplayController extends BaseController
|
class DisplayController extends BaseController
|
||||||
{
|
{
|
||||||
protected $default_view = 'backups';
|
protected $default_view = 'dashboard';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class AkeebaImporter
|
|||||||
's3_bucket' => $config['engine.postproc.s3.bucket'] ?? '',
|
's3_bucket' => $config['engine.postproc.s3.bucket'] ?? '',
|
||||||
's3_path' => $config['engine.postproc.s3.directory'] ?? '/backups',
|
's3_path' => $config['engine.postproc.s3.directory'] ?? '/backups',
|
||||||
'remote_keep_local' => 1,
|
'remote_keep_local' => 1,
|
||||||
'include_kickstart' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'),
|
'include_mokorestore' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'),
|
||||||
'published' => 1,
|
'published' => 1,
|
||||||
'ordering' => (int) $akProfile->id,
|
'ordering' => (int) $akProfile->id,
|
||||||
'created' => $now,
|
'created' => $now,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoBackup\Administrator\Engine;
|
|||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\Event\Event;
|
||||||
|
|
||||||
class BackupEngine
|
class BackupEngine
|
||||||
{
|
{
|
||||||
@@ -187,21 +188,21 @@ class BackupEngine
|
|||||||
$this->log('Archive created: ' . $sizeHuman);
|
$this->log('Archive created: ' . $sizeHuman);
|
||||||
$this->log('SHA-256: ' . ($checksum ?: 'N/A'));
|
$this->log('SHA-256: ' . ($checksum ?: 'N/A'));
|
||||||
|
|
||||||
// Step 2.5: Wrap with Kickstart restore script (if enabled)
|
// Step 2.5: Wrap with MokoRestore script (if enabled)
|
||||||
$includeKickstart = (bool) ($profile->include_kickstart ?? false);
|
$includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
|
||||||
|
|
||||||
if ($includeKickstart) {
|
if ($includeMokoRestore) {
|
||||||
$this->log('Wrapping with Kickstart restore script...');
|
$this->log('Wrapping with MokoRestore script...');
|
||||||
$kickstartName = str_replace('.zip', '-kickstart.zip', $archiveName);
|
$mokoRestoreName = str_replace('.zip', '-mokorestore.zip', $archiveName);
|
||||||
$kickstartPath = $this->backupDir . '/' . $kickstartName;
|
$mokoRestorePath = $this->backupDir . '/' . $mokoRestoreName;
|
||||||
Kickstart::wrap($archivePath, $kickstartPath);
|
MokoRestore::wrap($archivePath, $mokoRestorePath);
|
||||||
|
|
||||||
// Replace the original archive with the wrapped one
|
// Replace the original archive with the wrapped one
|
||||||
@unlink($archivePath);
|
@unlink($archivePath);
|
||||||
rename($kickstartPath, $archivePath);
|
rename($mokoRestorePath, $archivePath);
|
||||||
$totalSize = filesize($archivePath);
|
$totalSize = filesize($archivePath);
|
||||||
$sizeHuman = number_format($totalSize / 1048576, 2) . ' MB';
|
$sizeHuman = number_format($totalSize / 1048576, 2) . ' MB';
|
||||||
$this->log('Kickstart archive created: ' . $sizeHuman);
|
$this->log('MokoRestore archive created: ' . $sizeHuman);
|
||||||
}
|
}
|
||||||
|
|
||||||
$remoteFilename = '';
|
$remoteFilename = '';
|
||||||
@@ -250,6 +251,9 @@ class BackupEngine
|
|||||||
// Send success notification
|
// Send success notification
|
||||||
NotificationSender::send($profile, $update, true, implode("\n", $this->log));
|
NotificationSender::send($profile, $update, true, implode("\n", $this->log));
|
||||||
|
|
||||||
|
// Dispatch event for actionlog and other listeners
|
||||||
|
$this->dispatchAfterRun(true, $recordId, $description, $profileId, $origin);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'Backup complete: ' . $archiveName . ' (' . $sizeHuman . ')',
|
'message' => 'Backup complete: ' . $archiveName . ' (' . $sizeHuman . ')',
|
||||||
@@ -275,6 +279,9 @@ class BackupEngine
|
|||||||
// Send failure notification
|
// Send failure notification
|
||||||
NotificationSender::send($profile, $update, false, implode("\n", $this->log));
|
NotificationSender::send($profile, $update, false, implode("\n", $this->log));
|
||||||
|
|
||||||
|
// Dispatch event for actionlog and other listeners
|
||||||
|
$this->dispatchAfterRun(false, $recordId, $description, $profileId, $origin);
|
||||||
|
|
||||||
return ['success' => false, 'message' => 'Backup failed: ' . $e->getMessage(), 'record_id' => $recordId];
|
return ['success' => false, 'message' => 'Backup failed: ' . $e->getMessage(), 'record_id' => $recordId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,6 +452,28 @@ class BackupEngine
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the onMokoBackupAfterRun event so plugins (actionlog, etc.) can react.
|
||||||
|
*/
|
||||||
|
private function dispatchAfterRun(bool $success, int $recordId, string $description, int $profileId, string $origin): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$app = Factory::getApplication();
|
||||||
|
|
||||||
|
$event = new Event('onMokoBackupAfterRun', [
|
||||||
|
'success' => $success,
|
||||||
|
'record_id' => $recordId,
|
||||||
|
'description' => $description,
|
||||||
|
'profile_id' => $profileId,
|
||||||
|
'origin' => $origin,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$app->getDispatcher()->dispatch('onMokoBackupAfterRun', $event);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Never let a listener failure break the backup result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function log(string $message): void
|
private function log(string $message): void
|
||||||
{
|
{
|
||||||
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
|
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
|
||||||
|
|||||||
+5
-5
@@ -9,7 +9,7 @@
|
|||||||
*
|
*
|
||||||
* Standalone restore script generator.
|
* Standalone restore script generator.
|
||||||
*
|
*
|
||||||
* When "Include Kickstart" is enabled on a profile, the backup archive
|
* When "Include MokoRestore" is enabled on a profile, the backup archive
|
||||||
* is wrapped:
|
* is wrapped:
|
||||||
*
|
*
|
||||||
* outer.zip
|
* outer.zip
|
||||||
@@ -17,14 +17,14 @@
|
|||||||
* └── site-backup.zip ← The actual site backup
|
* └── site-backup.zip ← The actual site backup
|
||||||
*
|
*
|
||||||
* Upload outer.zip to a blank server, extract, open restore.php in a
|
* Upload outer.zip to a blank server, extract, open restore.php in a
|
||||||
* browser, and it handles everything — just like Akeeba Kickstart.
|
* browser, and it handles everything — self-contained site restoration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Joomla\Component\MokoBackup\Administrator\Engine;
|
namespace Joomla\Component\MokoBackup\Administrator\Engine;
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
class Kickstart
|
class MokoRestore
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Wrap a backup archive with the standalone restore script.
|
* Wrap a backup archive with the standalone restore script.
|
||||||
@@ -39,7 +39,7 @@ class Kickstart
|
|||||||
$zip = new \ZipArchive();
|
$zip = new \ZipArchive();
|
||||||
|
|
||||||
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
throw new \RuntimeException('Cannot create kickstart archive: ' . $outputPath);
|
throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the standalone restore script
|
// Add the standalone restore script
|
||||||
@@ -68,7 +68,7 @@ class Kickstart
|
|||||||
return <<<'RESTORE_PHP'
|
return <<<'RESTORE_PHP'
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* MokoJoomBackup — Standalone Restore Script
|
* MokoRestore — Standalone Site Restoration Tool
|
||||||
*
|
*
|
||||||
* Upload this file alongside site-backup.zip to your server.
|
* Upload this file alongside site-backup.zip to your server.
|
||||||
* Open restore.php in your browser and follow the steps.
|
* Open restore.php in your browser and follow the steps.
|
||||||
@@ -57,7 +57,7 @@ class SteppedBackupEngine
|
|||||||
$session->excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
|
$session->excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
|
||||||
$session->backupDir = $profile->backup_dir ?: 'administrator/components/com_mokobackup/backups';
|
$session->backupDir = $profile->backup_dir ?: 'administrator/components/com_mokobackup/backups';
|
||||||
$session->remoteStorage = $profile->remote_storage ?? 'none';
|
$session->remoteStorage = $profile->remote_storage ?? 'none';
|
||||||
$session->includeKickstart = (bool) ($profile->include_kickstart ?? false);
|
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
|
||||||
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
|
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
|
||||||
|
|
||||||
// Build archive path
|
// Build archive path
|
||||||
@@ -288,7 +288,7 @@ class SteppedBackupEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finalize phase: add database.sql to ZIP, apply kickstart wrapper.
|
* Finalize phase: add database.sql to ZIP, apply MokoRestore wrapper.
|
||||||
*/
|
*/
|
||||||
private function stepFinalize(SteppedSession $session): void
|
private function stepFinalize(SteppedSession $session): void
|
||||||
{
|
{
|
||||||
@@ -314,15 +314,15 @@ class SteppedBackupEngine
|
|||||||
|
|
||||||
$totalSize = file_exists($session->archivePath) ? filesize($session->archivePath) : 0;
|
$totalSize = file_exists($session->archivePath) ? filesize($session->archivePath) : 0;
|
||||||
|
|
||||||
// Kickstart wrapper
|
// MokoRestore wrapper
|
||||||
if ($session->includeKickstart) {
|
if ($session->includeMokoRestore) {
|
||||||
$session->log('Wrapping with Kickstart restore script...');
|
$session->log('Wrapping with MokoRestore script...');
|
||||||
$kickstartPath = $session->archivePath . '.kickstart.zip';
|
$mokoRestorePath = $session->archivePath . '.mokorestore.zip';
|
||||||
Kickstart::wrap($session->archivePath, $kickstartPath);
|
MokoRestore::wrap($session->archivePath, $mokoRestorePath);
|
||||||
@unlink($session->archivePath);
|
@unlink($session->archivePath);
|
||||||
rename($kickstartPath, $session->archivePath);
|
rename($mokoRestorePath, $session->archivePath);
|
||||||
$totalSize = filesize($session->archivePath);
|
$totalSize = filesize($session->archivePath);
|
||||||
$session->log('Kickstart archive created');
|
$session->log('MokoRestore archive created');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update record
|
// Update record
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class SteppedSession
|
|||||||
public array $excludeFiles = [];
|
public array $excludeFiles = [];
|
||||||
public array $excludeTables = [];
|
public array $excludeTables = [];
|
||||||
public string $remoteStorage = 'none';
|
public string $remoteStorage = 'none';
|
||||||
public bool $includeKickstart = false;
|
public bool $includeMokoRestore = false;
|
||||||
public bool $remoteKeepLocal = true;
|
public bool $remoteKeepLocal = true;
|
||||||
|
|
||||||
// Progress
|
// Progress
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage com_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Component\MokoBackup\Administrator\Model;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||||
|
|
||||||
|
class DashboardModel extends BaseDatabaseModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the most recent completed backup record.
|
||||||
|
*
|
||||||
|
* @return object|null
|
||||||
|
*/
|
||||||
|
public function getLastBackup(): ?object
|
||||||
|
{
|
||||||
|
$db = $this->getDatabase();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('r.*, p.title AS profile_title')
|
||||||
|
->from($db->quoteName('#__mokobackup_records', 'r'))
|
||||||
|
->join('LEFT', $db->quoteName('#__mokobackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
||||||
|
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
|
||||||
|
->order($db->quoteName('r.backupend') . ' DESC');
|
||||||
|
$db->setQuery($query, 0, 1);
|
||||||
|
|
||||||
|
return $db->loadObject() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query com_scheduler for the next scheduled MokoBackup task.
|
||||||
|
*
|
||||||
|
* @return object|null Object with next_execution and title, or null
|
||||||
|
*/
|
||||||
|
public function getNextScheduled(): ?object
|
||||||
|
{
|
||||||
|
$db = $this->getDatabase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName(['t.next_execution', 't.title']))
|
||||||
|
->from($db->quoteName('#__scheduler_tasks', 't'))
|
||||||
|
->where($db->quoteName('t.type') . ' = ' . $db->quote('mokobackup.run_profile'))
|
||||||
|
->where($db->quoteName('t.state') . ' = 1')
|
||||||
|
->order($db->quoteName('t.next_execution') . ' ASC');
|
||||||
|
$db->setQuery($query, 0, 1);
|
||||||
|
|
||||||
|
return $db->loadObject() ?: null;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backup statistics.
|
||||||
|
*
|
||||||
|
* @return object Object with total_count, total_size, fail_count_7d
|
||||||
|
*/
|
||||||
|
public function getStats(): object
|
||||||
|
{
|
||||||
|
$db = $this->getDatabase();
|
||||||
|
|
||||||
|
// Total completed backups and storage
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('COUNT(*) AS total_count')
|
||||||
|
->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size')
|
||||||
|
->from($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||||
|
$db->setQuery($query);
|
||||||
|
$stats = $db->loadObject();
|
||||||
|
|
||||||
|
// Failures in last 7 days
|
||||||
|
$cutoff = date('Y-m-d H:i:s', strtotime('-7 days'));
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('COUNT(*) AS fail_count')
|
||||||
|
->from($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('status') . ' = ' . $db->quote('fail'))
|
||||||
|
->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff));
|
||||||
|
$db->setQuery($query);
|
||||||
|
$stats->fail_count_7d = (int) $db->loadResult();
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check system health for backup readiness.
|
||||||
|
*
|
||||||
|
* @return array Array of check results [{label, status, detail}]
|
||||||
|
*/
|
||||||
|
public function getSystemHealth(): array
|
||||||
|
{
|
||||||
|
$checks = [];
|
||||||
|
|
||||||
|
// PHP version
|
||||||
|
$checks[] = (object) [
|
||||||
|
'label' => 'PHP Version',
|
||||||
|
'status' => version_compare(PHP_VERSION, '8.1.0', '>='),
|
||||||
|
'detail' => PHP_VERSION,
|
||||||
|
];
|
||||||
|
|
||||||
|
// ZipArchive extension
|
||||||
|
$checks[] = (object) [
|
||||||
|
'label' => 'ZipArchive',
|
||||||
|
'status' => extension_loaded('zip'),
|
||||||
|
'detail' => extension_loaded('zip') ? 'Loaded' : 'Not loaded',
|
||||||
|
];
|
||||||
|
|
||||||
|
// AES-256 encryption support
|
||||||
|
$aesSupport = defined('ZipArchive::EM_AES_256');
|
||||||
|
$checks[] = (object) [
|
||||||
|
'label' => 'AES-256 Encryption',
|
||||||
|
'status' => $aesSupport,
|
||||||
|
'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Backup directory writable
|
||||||
|
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups';
|
||||||
|
$writable = is_dir($backupDir) && is_writable($backupDir);
|
||||||
|
$checks[] = (object) [
|
||||||
|
'label' => 'Backup Directory',
|
||||||
|
'status' => $writable,
|
||||||
|
'detail' => $writable ? 'Writable' : 'Not writable or missing',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Disk space
|
||||||
|
$freeSpace = @disk_free_space($backupDir ?: JPATH_ROOT);
|
||||||
|
$freeGB = $freeSpace ? round($freeSpace / 1073741824, 1) : 0;
|
||||||
|
$checks[] = (object) [
|
||||||
|
'label' => 'Free Disk Space',
|
||||||
|
'status' => $freeGB >= 1.0,
|
||||||
|
'detail' => $freeGB . ' GB free',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get published backup profiles for the quick-action selector.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getProfiles(): array
|
||||||
|
{
|
||||||
|
$db = $this->getDatabase();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName(['id', 'title', 'backup_type']))
|
||||||
|
->from($db->quoteName('#__mokobackup_profiles'))
|
||||||
|
->where($db->quoteName('published') . ' = 1')
|
||||||
|
->order($db->quoteName('ordering') . ' ASC');
|
||||||
|
$db->setQuery($query);
|
||||||
|
|
||||||
|
return $db->loadObjectList() ?: [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ defined('_JEXEC') or die;
|
|||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
class HtmlView extends BaseHtmlView
|
class HtmlView extends BaseHtmlView
|
||||||
@@ -44,11 +45,62 @@ class HtmlView extends BaseHtmlView
|
|||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
$this->profiles = $db->loadObjectList() ?: [];
|
$this->profiles = $db->loadObjectList() ?: [];
|
||||||
|
|
||||||
|
$this->checkUpdateSite();
|
||||||
$this->addToolbar();
|
$this->addToolbar();
|
||||||
|
|
||||||
parent::display($tpl);
|
parent::display($tpl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an info notice linking to the update site record so the user
|
||||||
|
* can configure their download key for automatic updates.
|
||||||
|
*/
|
||||||
|
protected function checkUpdateSite(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Find the update site ID linked to pkg_mokobackup
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('us.update_site_id'))
|
||||||
|
->from($db->quoteName('#__update_sites', 'us'))
|
||||||
|
->join(
|
||||||
|
'INNER',
|
||||||
|
$db->quoteName('#__update_sites_extensions', 'use')
|
||||||
|
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||||
|
)
|
||||||
|
->join(
|
||||||
|
'INNER',
|
||||||
|
$db->quoteName('#__extensions', 'e')
|
||||||
|
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||||
|
)
|
||||||
|
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokobackup'))
|
||||||
|
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
||||||
|
->setLimit(1);
|
||||||
|
|
||||||
|
$db->setQuery($query);
|
||||||
|
$updateSiteId = (int) $db->loadResult();
|
||||||
|
|
||||||
|
if ($updateSiteId > 0) {
|
||||||
|
$editUrl = Route::_(
|
||||||
|
'index.php?option=com_installer&view=updatesites&task=updatesite.edit&id=' . $updateSiteId
|
||||||
|
);
|
||||||
|
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
Text::sprintf('COM_MOKOBACKUP_UPDATE_SITE_NOTICE', $editUrl),
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
Text::_('COM_MOKOBACKUP_UPDATE_SITE_MISSING'),
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Non-critical — silently ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
ToolbarHelper::title(Text::_('COM_MOKOBACKUP_BACKUPS_TITLE'), 'database');
|
ToolbarHelper::title(Text::_('COM_MOKOBACKUP_BACKUPS_TITLE'), 'database');
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage com_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Component\MokoBackup\Administrator\View\Dashboard;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public ?object $lastBackup = null;
|
||||||
|
public ?object $nextScheduled = null;
|
||||||
|
public object $stats;
|
||||||
|
public array $systemHealth = [];
|
||||||
|
public array $profiles = [];
|
||||||
|
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
/** @var \Joomla\Component\MokoBackup\Administrator\Model\DashboardModel $model */
|
||||||
|
$model = $this->getModel();
|
||||||
|
|
||||||
|
$this->lastBackup = $model->getLastBackup();
|
||||||
|
$this->nextScheduled = $model->getNextScheduled();
|
||||||
|
$this->stats = $model->getStats();
|
||||||
|
$this->systemHealth = $model->getSystemHealth();
|
||||||
|
$this->profiles = $model->getProfiles();
|
||||||
|
|
||||||
|
$this->addToolbar();
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function addToolbar(): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title(Text::_('COM_MOKOBACKUP_DASHBOARD_TITLE'), 'archive');
|
||||||
|
ToolbarHelper::preferences('com_mokobackup');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage com_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @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\HTML\HTMLHelper;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
use Joomla\CMS\Router\Route;
|
||||||
|
use Joomla\CMS\Session\Session;
|
||||||
|
|
||||||
|
$ajaxToken = Session::getFormToken();
|
||||||
|
$ajaxUrl = Route::_('index.php?option=com_mokobackup&format=json', false);
|
||||||
|
?>
|
||||||
|
<div class="row">
|
||||||
|
<!-- Row 1: Status Cards -->
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="icon-database fs-1 text-primary" aria-hidden="true"></span>
|
||||||
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP'); ?></h5>
|
||||||
|
<?php if ($this->lastBackup) : ?>
|
||||||
|
<p class="card-text text-success fw-bold">
|
||||||
|
<?php echo HTMLHelper::_('date', $this->lastBackup->backupend, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
|
</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<?php echo $this->escape($this->lastBackup->profile_title); ?>
|
||||||
|
—
|
||||||
|
<?php echo HTMLHelper::_('number.bytes', $this->lastBackup->total_size); ?>
|
||||||
|
</small>
|
||||||
|
<?php else : ?>
|
||||||
|
<p class="card-text text-warning"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="icon-calendar fs-1 text-info" aria-hidden="true"></span>
|
||||||
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED'); ?></h5>
|
||||||
|
<?php if ($this->nextScheduled) : ?>
|
||||||
|
<p class="card-text fw-bold">
|
||||||
|
<?php echo HTMLHelper::_('date', $this->nextScheduled->next_execution, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
|
</p>
|
||||||
|
<small class="text-muted"><?php echo $this->escape($this->nextScheduled->title); ?></small>
|
||||||
|
<?php else : ?>
|
||||||
|
<p class="card-text text-muted"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="icon-copy fs-1 text-secondary" aria-hidden="true"></span>
|
||||||
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS'); ?></h5>
|
||||||
|
<p class="card-text fw-bold fs-3"><?php echo (int) $this->stats->total_count; ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="icon-folder-open fs-1 text-warning" aria-hidden="true"></span>
|
||||||
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_STORAGE'); ?></h5>
|
||||||
|
<p class="card-text fw-bold fs-3">
|
||||||
|
<?php echo HTMLHelper::_('number.bytes', (int) $this->stats->total_size); ?>
|
||||||
|
</p>
|
||||||
|
<?php if ($this->stats->fail_count_7d > 0) : ?>
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<?php echo Text::sprintf('COM_MOKOBACKUP_DASHBOARD_FAILURES_7D', $this->stats->fail_count_7d); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Quick Actions -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_QUICK_ACTIONS'); ?></h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (!empty($this->profiles)) : ?>
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-3">
|
||||||
|
<select id="mb-profile-select" class="form-select" style="max-width:250px;">
|
||||||
|
<?php foreach ($this->profiles as $profile) : ?>
|
||||||
|
<option value="<?php echo (int) $profile->id; ?>">
|
||||||
|
<?php echo $this->escape($profile->title); ?>
|
||||||
|
(<?php echo $this->escape($profile->backup_type); ?>)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="window.mokobackupStart()">
|
||||||
|
<span class="icon-download" aria-hidden="true"></span>
|
||||||
|
<?php echo Text::_('COM_MOKOBACKUP_TOOLBAR_BACKUP_NOW'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="<?php echo Route::_('index.php?option=com_mokobackup&view=backups'); ?>" class="list-group-item list-group-item-action">
|
||||||
|
<span class="icon-database" aria-hidden="true"></span>
|
||||||
|
<?php echo Text::_('COM_MOKOBACKUP_SUBMENU_BACKUPS'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Route::_('index.php?option=com_mokobackup&view=profiles'); ?>" class="list-group-item list-group-item-action">
|
||||||
|
<span class="icon-cog" aria-hidden="true"></span>
|
||||||
|
<?php echo Text::_('COM_MOKOBACKUP_SUBMENU_PROFILES'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>" class="list-group-item list-group-item-action">
|
||||||
|
<span class="icon-calendar" aria-hidden="true"></span>
|
||||||
|
<?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_SCHEDULED_TASKS'); ?>
|
||||||
|
</a>
|
||||||
|
<a href="<?php echo Route::_('index.php?option=com_installer&view=updatesites'); ?>" class="list-group-item list-group-item-action">
|
||||||
|
<span class="icon-refresh" aria-hidden="true"></span>
|
||||||
|
<?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_UPDATE_SITE'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2 right: System Health -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_SYSTEM_HEALTH'); ?></h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($this->systemHealth as $check) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="w-1 text-center">
|
||||||
|
<?php if ($check->status) : ?>
|
||||||
|
<span class="icon-publish text-success" aria-hidden="true"></span>
|
||||||
|
<?php else : ?>
|
||||||
|
<span class="icon-unpublish text-danger" aria-hidden="true"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?php echo $this->escape($check->label); ?></td>
|
||||||
|
<td class="text-muted"><?php echo $this->escape($check->detail); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stepped Backup Modal (reused from backups view) -->
|
||||||
|
<div id="mokobackup-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
||||||
|
<div style="max-width:500px; margin:10% auto; background:#fff; border-radius:8px; padding:2rem; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
||||||
|
<h3 id="mb-modal-title" style="margin:0 0 1rem;">Backup in Progress</h3>
|
||||||
|
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
|
||||||
|
<div id="mb-progress-bar" style="height:100%; background:#0d6efd; transition:width 0.3s; width:0%; display:flex; align-items:center; justify-content:center; color:#fff; font-size:0.8rem; font-weight:bold;">0%</div>
|
||||||
|
</div>
|
||||||
|
<p id="mb-status" style="color:#666; font-size:0.9rem; margin:0.5rem 0;">Initializing...</p>
|
||||||
|
<p id="mb-phase" style="color:#999; font-size:0.8rem; margin:0;">Phase: init</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
||||||
|
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
|
||||||
|
|
||||||
|
function showModal() {
|
||||||
|
document.getElementById('mokobackup-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal() {
|
||||||
|
document.getElementById('mokobackup-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(progress, message, phase) {
|
||||||
|
const bar = document.getElementById('mb-progress-bar');
|
||||||
|
bar.style.width = progress + '%';
|
||||||
|
bar.textContent = progress + '%';
|
||||||
|
document.getElementById('mb-status').textContent = message;
|
||||||
|
document.getElementById('mb-phase').textContent = 'Phase: ' + phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postAjax(params) {
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.append(TOKEN_NAME, '1');
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
form.append(k, v);
|
||||||
|
}
|
||||||
|
const res = await fetch(AJAX_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: form,
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
});
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startSteppedBackup() {
|
||||||
|
const profileSelect = document.getElementById('mb-profile-select');
|
||||||
|
const profileId = profileSelect ? profileSelect.value : '1';
|
||||||
|
|
||||||
|
showModal();
|
||||||
|
updateProgress(0, 'Initializing backup...', 'init');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const initResult = await postAjax({
|
||||||
|
task: 'ajax.init',
|
||||||
|
profile_id: profileId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initResult.error) {
|
||||||
|
updateProgress(0, 'ERROR: ' + initResult.message, 'failed');
|
||||||
|
setTimeout(hideModal, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = initResult.session_id;
|
||||||
|
updateProgress(initResult.progress, initResult.message, initResult.phase);
|
||||||
|
|
||||||
|
let done = false;
|
||||||
|
while (!done) {
|
||||||
|
const stepResult = await postAjax({
|
||||||
|
task: 'ajax.step',
|
||||||
|
session_id: sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stepResult.error) {
|
||||||
|
updateProgress(0, 'ERROR: ' + stepResult.message, 'failed');
|
||||||
|
setTimeout(hideModal, 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(stepResult.progress, stepResult.message, stepResult.phase);
|
||||||
|
done = stepResult.done || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('mb-modal-title').textContent = 'Backup Complete';
|
||||||
|
setTimeout(function() {
|
||||||
|
hideModal();
|
||||||
|
location.reload();
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
updateProgress(0, 'ERROR: ' + err.message, 'failed');
|
||||||
|
setTimeout(hideModal, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mokobackupStart = startSteppedBackup;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
; MokoJoomBackup — Actionlog Plugin language file (en-GB)
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Actionlog Plugin system language file (en-GB)
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
; MokoJoomBackup — Actionlog Plugin language file (en-US)
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Actionlog Plugin system language file (en-US)
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP="Action Log - MokoJoomBackup"
|
||||||
|
PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_actionlog_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_actionlog_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
-->
|
||||||
|
<extension type="plugin" group="actionlog" method="upgrade">
|
||||||
|
<name>plg_actionlog_mokobackup</name>
|
||||||
|
<version>01.01.07-dev</version>
|
||||||
|
<creationDate>2026-06-04</creationDate>
|
||||||
|
<author>Moko Consulting</author>
|
||||||
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
|
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<description>PLG_ACTIONLOG_MOKOBACKUP_DESCRIPTION</description>
|
||||||
|
|
||||||
|
<namespace path="src">Joomla\Plugin\Actionlog\MokoBackup</namespace>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
<filename plugin="mokobackup">mokobackup.php</filename>
|
||||||
|
<folder>services</folder>
|
||||||
|
<folder>src</folder>
|
||||||
|
</files>
|
||||||
|
|
||||||
|
<languages>
|
||||||
|
<language tag="en-GB">language/en-GB/plg_actionlog_mokobackup.ini</language>
|
||||||
|
<language tag="en-GB">language/en-GB/plg_actionlog_mokobackup.sys.ini</language>
|
||||||
|
</languages>
|
||||||
|
</extension>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_actionlog_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Extension\PluginInterface;
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Plugin\PluginHelper;
|
||||||
|
use Joomla\DI\Container;
|
||||||
|
use Joomla\DI\ServiceProviderInterface;
|
||||||
|
use Joomla\Event\DispatcherInterface;
|
||||||
|
use Joomla\Plugin\Actionlog\MokoBackup\Extension\MokoBackupActionlog;
|
||||||
|
|
||||||
|
return new class () implements ServiceProviderInterface {
|
||||||
|
public function register(Container $container): void
|
||||||
|
{
|
||||||
|
$container->set(
|
||||||
|
PluginInterface::class,
|
||||||
|
function (Container $container) {
|
||||||
|
$plugin = new MokoBackupActionlog(
|
||||||
|
$container->get(DispatcherInterface::class),
|
||||||
|
(array) PluginHelper::getPlugin('actionlog', 'mokobackup')
|
||||||
|
);
|
||||||
|
$plugin->setApplication(Factory::getApplication());
|
||||||
|
|
||||||
|
return $plugin;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_actionlog_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Actionlog\MokoBackup\Extension;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Event\Model;
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
|
use Joomla\Component\Actionlogs\Administrator\Helper\ActionlogsHelper;
|
||||||
|
use Joomla\Event\Event;
|
||||||
|
use Joomla\Event\SubscriberInterface;
|
||||||
|
|
||||||
|
final class MokoBackupActionlog extends CMSPlugin implements SubscriberInterface
|
||||||
|
{
|
||||||
|
protected $autoloadLanguage = true;
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'onContentAfterSave' => 'onContentAfterSave',
|
||||||
|
'onContentAfterDelete' => 'onContentAfterDelete',
|
||||||
|
'onMokoBackupAfterRun' => 'onMokoBackupAfterRun',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log when a backup profile is saved (created or updated).
|
||||||
|
*/
|
||||||
|
public function onContentAfterSave(Event $event): void
|
||||||
|
{
|
||||||
|
[$context, $table, $isNew] = array_values($event->getArguments());
|
||||||
|
|
||||||
|
if ($context !== 'com_mokobackup.profile') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$messageKey = $isNew
|
||||||
|
? 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_CREATED'
|
||||||
|
: 'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_UPDATED';
|
||||||
|
|
||||||
|
$this->addLog(
|
||||||
|
[
|
||||||
|
$messageKey,
|
||||||
|
'id' => $table->id,
|
||||||
|
'title' => $table->title,
|
||||||
|
'userid' => $this->getCurrentUserId(),
|
||||||
|
'username' => $this->getCurrentUserName(),
|
||||||
|
],
|
||||||
|
$messageKey,
|
||||||
|
'com_mokobackup.profile',
|
||||||
|
$this->getCurrentUserId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log when a backup profile or record is deleted.
|
||||||
|
*/
|
||||||
|
public function onContentAfterDelete(Event $event): void
|
||||||
|
{
|
||||||
|
[$context, $table] = array_values($event->getArguments());
|
||||||
|
|
||||||
|
if ($context === 'com_mokobackup.profile') {
|
||||||
|
$this->addLog(
|
||||||
|
[
|
||||||
|
'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED',
|
||||||
|
'id' => $table->id,
|
||||||
|
'title' => $table->title ?? '',
|
||||||
|
'userid' => $this->getCurrentUserId(),
|
||||||
|
'username' => $this->getCurrentUserName(),
|
||||||
|
],
|
||||||
|
'PLG_ACTIONLOG_MOKOBACKUP_PROFILE_DELETED',
|
||||||
|
'com_mokobackup.profile',
|
||||||
|
$this->getCurrentUserId()
|
||||||
|
);
|
||||||
|
} elseif ($context === 'com_mokobackup.backup') {
|
||||||
|
$this->addLog(
|
||||||
|
[
|
||||||
|
'PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED',
|
||||||
|
'id' => $table->id,
|
||||||
|
'title' => $table->description ?? 'Backup #' . $table->id,
|
||||||
|
'userid' => $this->getCurrentUserId(),
|
||||||
|
'username' => $this->getCurrentUserName(),
|
||||||
|
],
|
||||||
|
'PLG_ACTIONLOG_MOKOBACKUP_RECORD_DELETED',
|
||||||
|
'com_mokobackup.backup',
|
||||||
|
$this->getCurrentUserId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log when a backup completes or fails.
|
||||||
|
* This event should be dispatched from BackupEngine.
|
||||||
|
*/
|
||||||
|
public function onMokoBackupAfterRun(Event $event): void
|
||||||
|
{
|
||||||
|
$args = $event->getArguments();
|
||||||
|
|
||||||
|
$success = $args['success'] ?? false;
|
||||||
|
$recordId = $args['record_id'] ?? 0;
|
||||||
|
$description = $args['description'] ?? '';
|
||||||
|
$profileId = $args['profile_id'] ?? 0;
|
||||||
|
$origin = $args['origin'] ?? 'backend';
|
||||||
|
|
||||||
|
$messageKey = $success
|
||||||
|
? 'PLG_ACTIONLOG_MOKOBACKUP_BACKUP_COMPLETE'
|
||||||
|
: 'PLG_ACTIONLOG_MOKOBACKUP_BACKUP_FAILED';
|
||||||
|
|
||||||
|
$this->addLog(
|
||||||
|
[
|
||||||
|
$messageKey,
|
||||||
|
'id' => $recordId,
|
||||||
|
'title' => $description ?: 'Backup #' . $recordId,
|
||||||
|
'profile_id' => $profileId,
|
||||||
|
'origin' => $origin,
|
||||||
|
'userid' => $this->getCurrentUserId(),
|
||||||
|
'username' => $this->getCurrentUserName(),
|
||||||
|
],
|
||||||
|
$messageKey,
|
||||||
|
'com_mokobackup.backup',
|
||||||
|
$this->getCurrentUserId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an action log entry.
|
||||||
|
*/
|
||||||
|
private function addLog(array $message, string $messageLanguageKey, string $context, int $userId): void
|
||||||
|
{
|
||||||
|
$params = [
|
||||||
|
'message_language_key' => $messageLanguageKey,
|
||||||
|
'message' => json_encode($message),
|
||||||
|
'date' => date('Y-m-d H:i:s'),
|
||||||
|
'extension' => 'com_mokobackup',
|
||||||
|
'user_id' => $userId,
|
||||||
|
'ip_address' => ActionlogsHelper::getIp(),
|
||||||
|
'item_id' => $message['id'] ?? 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$db->insertObject('#__action_logs', (object) $params);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Non-critical — don't break the operation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentUserId(): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (int) Factory::getApplication()->getIdentity()->id;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentUserName(): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return Factory::getApplication()->getIdentity()->username ?: 'system';
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Console Plugin language file (en-GB)
|
||||||
|
PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup"
|
||||||
|
PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Console Plugin system language file (en-GB)
|
||||||
|
PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup"
|
||||||
|
PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Console Plugin language file (en-US)
|
||||||
|
PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup"
|
||||||
|
PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Console Plugin system language file (en-US)
|
||||||
|
PLG_CONSOLE_MOKOBACKUP="Console - MokoJoomBackup"
|
||||||
|
PLG_CONSOLE_MOKOBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
-->
|
||||||
|
<extension type="plugin" group="console" method="upgrade">
|
||||||
|
<name>plg_console_mokobackup</name>
|
||||||
|
<version>01.01.07-dev</version>
|
||||||
|
<creationDate>2026-06-04</creationDate>
|
||||||
|
<author>Moko Consulting</author>
|
||||||
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
|
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<description>PLG_CONSOLE_MOKOBACKUP_DESCRIPTION</description>
|
||||||
|
|
||||||
|
<namespace path="src">Joomla\Plugin\Console\MokoBackup</namespace>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
<filename plugin="mokobackup">mokobackup.php</filename>
|
||||||
|
<folder>services</folder>
|
||||||
|
<folder>src</folder>
|
||||||
|
</files>
|
||||||
|
|
||||||
|
<languages>
|
||||||
|
<language tag="en-GB">language/en-GB/plg_console_mokobackup.ini</language>
|
||||||
|
<language tag="en-GB">language/en-GB/plg_console_mokobackup.sys.ini</language>
|
||||||
|
</languages>
|
||||||
|
</extension>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Extension\PluginInterface;
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Plugin\PluginHelper;
|
||||||
|
use Joomla\DI\Container;
|
||||||
|
use Joomla\DI\ServiceProviderInterface;
|
||||||
|
use Joomla\Event\DispatcherInterface;
|
||||||
|
use Joomla\Plugin\Console\MokoBackup\Extension\MokoBackupConsole;
|
||||||
|
|
||||||
|
return new class () implements ServiceProviderInterface {
|
||||||
|
public function register(Container $container): void
|
||||||
|
{
|
||||||
|
$container->set(
|
||||||
|
PluginInterface::class,
|
||||||
|
function (Container $container) {
|
||||||
|
$plugin = new MokoBackupConsole(
|
||||||
|
$container->get(DispatcherInterface::class),
|
||||||
|
(array) PluginHelper::getPlugin('console', 'mokobackup')
|
||||||
|
);
|
||||||
|
$plugin->setApplication(Factory::getApplication());
|
||||||
|
|
||||||
|
return $plugin;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Console\MokoBackup\Command;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\Console\Command\AbstractCommand;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
class CleanupCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'mokobackup:cleanup';
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Clean up old backup records and archive files');
|
||||||
|
$this->addOption('max-age', null, InputOption::VALUE_REQUIRED, 'Max age in days', '30');
|
||||||
|
$this->addOption('max-count', null, InputOption::VALUE_REQUIRED, 'Max number of backups to keep', '10');
|
||||||
|
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be deleted without deleting');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$maxAge = (int) $input->getOption('max-age');
|
||||||
|
$maxCount = (int) $input->getOption('max-count');
|
||||||
|
$dryRun = $input->getOption('dry-run');
|
||||||
|
|
||||||
|
$io->title('MokoJoomBackup — Cleanup');
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$io->note('Dry run — no files will be deleted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
// Delete by age
|
||||||
|
$cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days"));
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('id, absolute_path, description, backupstart')
|
||||||
|
->from($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
|
||||||
|
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||||
|
$db->setQuery($query);
|
||||||
|
$expired = $db->loadObjectList();
|
||||||
|
|
||||||
|
foreach ($expired as $record) {
|
||||||
|
$io->text('Expired: #' . $record->id . ' — ' . $record->backupstart . ' — ' . ($record->description ?: 'no description'));
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
||||||
|
@unlink($record->absolute_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
||||||
|
);
|
||||||
|
$db->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce max count
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
||||||
|
$db->setQuery($query);
|
||||||
|
$totalCount = (int) $db->loadResult();
|
||||||
|
|
||||||
|
if ($totalCount > $maxCount) {
|
||||||
|
$excess = $totalCount - $maxCount;
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('id, absolute_path, description, backupstart')
|
||||||
|
->from($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
||||||
|
->order($db->quoteName('backupstart') . ' ASC');
|
||||||
|
$db->setQuery($query, 0, $excess);
|
||||||
|
$oldest = $db->loadObjectList();
|
||||||
|
|
||||||
|
foreach ($oldest as $record) {
|
||||||
|
$io->text('Over limit: #' . $record->id . ' — ' . $record->backupstart);
|
||||||
|
|
||||||
|
if (!$dryRun) {
|
||||||
|
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
||||||
|
@unlink($record->absolute_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
||||||
|
);
|
||||||
|
$db->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deleted === 0) {
|
||||||
|
$io->success('No backups to clean up.');
|
||||||
|
} else {
|
||||||
|
$io->success(($dryRun ? 'Would delete ' : 'Deleted ') . $deleted . ' backup record(s).');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Console\MokoBackup\Command;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\Console\Command\AbstractCommand;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
class ListCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'mokobackup:list';
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('List backup records');
|
||||||
|
$this->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Number of records to show', '20');
|
||||||
|
$this->addOption('status', 's', InputOption::VALUE_OPTIONAL, 'Filter by status (complete, fail, running)');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$limit = (int) $input->getOption('limit');
|
||||||
|
$status = $input->getOption('status');
|
||||||
|
|
||||||
|
$io->title('MokoJoomBackup — Backup Records');
|
||||||
|
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('r.id, r.description, r.status, r.origin, r.backup_type, r.total_size, r.backupstart, r.backupend')
|
||||||
|
->select($db->quoteName('p.title', 'profile_title'))
|
||||||
|
->from($db->quoteName('#__mokobackup_records', 'r'))
|
||||||
|
->join('LEFT', $db->quoteName('#__mokobackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
||||||
|
->order($db->quoteName('r.backupstart') . ' DESC');
|
||||||
|
|
||||||
|
if ($status) {
|
||||||
|
$query->where($db->quoteName('r.status') . ' = ' . $db->quote($status));
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->setQuery($query, 0, $limit);
|
||||||
|
$records = $db->loadObjectList();
|
||||||
|
|
||||||
|
if (empty($records)) {
|
||||||
|
$io->info('No backup records found.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($records as $record) {
|
||||||
|
$size = $record->total_size > 0
|
||||||
|
? round($record->total_size / 1048576, 2) . ' MB'
|
||||||
|
: '—';
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
$record->id,
|
||||||
|
$record->profile_title ?: '—',
|
||||||
|
$record->status,
|
||||||
|
$record->backup_type,
|
||||||
|
$size,
|
||||||
|
$record->origin,
|
||||||
|
$record->backupstart,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->table(
|
||||||
|
['ID', 'Profile', 'Status', 'Type', 'Size', 'Origin', 'Started'],
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Console\MokoBackup\Command;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\Console\Command\AbstractCommand;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
class ProfilesCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'mokobackup:profiles';
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('List available backup profiles');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$io->title('MokoJoomBackup — Backup Profiles');
|
||||||
|
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('id, title, backup_type, published, ordering')
|
||||||
|
->from($db->quoteName('#__mokobackup_profiles'))
|
||||||
|
->order($db->quoteName('ordering') . ' ASC');
|
||||||
|
$db->setQuery($query);
|
||||||
|
$profiles = $db->loadObjectList();
|
||||||
|
|
||||||
|
if (empty($profiles)) {
|
||||||
|
$io->info('No backup profiles found.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$rows[] = [
|
||||||
|
$profile->id,
|
||||||
|
$profile->title,
|
||||||
|
$profile->backup_type,
|
||||||
|
$profile->published ? 'Yes' : 'No',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->table(
|
||||||
|
['ID', 'Title', 'Type', 'Published'],
|
||||||
|
$rows
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Console\MokoBackup\Command;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\Component\MokoBackup\Administrator\Engine\RestoreEngine;
|
||||||
|
use Joomla\Console\Command\AbstractCommand;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
class RestoreCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'mokobackup:restore';
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Restore a backup by record ID');
|
||||||
|
$this->addArgument('id', InputArgument::REQUIRED, 'Backup record ID to restore');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$recordId = (int) $input->getArgument('id');
|
||||||
|
|
||||||
|
$io->title('MokoJoomBackup — Restore Backup');
|
||||||
|
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('*')
|
||||||
|
->from($db->quoteName('#__mokobackup_records'))
|
||||||
|
->where($db->quoteName('id') . ' = ' . $recordId);
|
||||||
|
$db->setQuery($query);
|
||||||
|
$record = $db->loadObject();
|
||||||
|
|
||||||
|
if (!$record) {
|
||||||
|
$io->error('Backup record not found: ' . $recordId);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($record->status !== 'complete') {
|
||||||
|
$io->error('Cannot restore — backup status is: ' . $record->status);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($record->absolute_path) || !is_file($record->absolute_path)) {
|
||||||
|
$io->error('Backup archive not found: ' . ($record->absolute_path ?: 'no path'));
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->warning('This will overwrite the current site files and/or database.');
|
||||||
|
$io->text('Archive: ' . $record->absolute_path);
|
||||||
|
$io->text('Type: ' . $record->backup_type);
|
||||||
|
|
||||||
|
if (!$io->confirm('Are you sure you want to continue?', false)) {
|
||||||
|
$io->info('Restore cancelled.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/RestoreEngine.php';
|
||||||
|
|
||||||
|
if (!file_exists($engineFile)) {
|
||||||
|
$io->error('RestoreEngine not found. Is the component fully installed?');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists(RestoreEngine::class)) {
|
||||||
|
require_once $engineFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = new RestoreEngine();
|
||||||
|
$result = $engine->restore($record->absolute_path, $record->backup_type);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$io->success($result['message']);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->error($result['message']);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Console\MokoBackup\Command;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\Component\MokoBackup\Administrator\Engine\BackupEngine;
|
||||||
|
use Joomla\Console\Command\AbstractCommand;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
class RunCommand extends AbstractCommand
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'mokobackup:run';
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->setDescription('Run a backup using a specified profile');
|
||||||
|
$this->addOption('profile', 'p', InputOption::VALUE_REQUIRED, 'Profile ID to use', '1');
|
||||||
|
$this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'Backup description', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$profileId = (int) $input->getOption('profile');
|
||||||
|
$desc = $input->getOption('description') ?: '';
|
||||||
|
|
||||||
|
$io->title('MokoJoomBackup — Run Backup');
|
||||||
|
$io->text('Profile ID: ' . $profileId);
|
||||||
|
|
||||||
|
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/BackupEngine.php';
|
||||||
|
|
||||||
|
if (!file_exists($engineFile)) {
|
||||||
|
$io->error('MokoJoomBackup component not installed.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists(BackupEngine::class)) {
|
||||||
|
require_once $engineFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
$engine = new BackupEngine();
|
||||||
|
$result = $engine->run($profileId, $desc ?: 'CLI backup', 'cli');
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$io->success($result['message']);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io->error($result['message']);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_console_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Console\MokoBackup\Extension;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
|
use Joomla\Event\Event;
|
||||||
|
use Joomla\Event\SubscriberInterface;
|
||||||
|
use Joomla\Plugin\Console\MokoBackup\Command\CleanupCommand;
|
||||||
|
use Joomla\Plugin\Console\MokoBackup\Command\ListCommand;
|
||||||
|
use Joomla\Plugin\Console\MokoBackup\Command\ProfilesCommand;
|
||||||
|
use Joomla\Plugin\Console\MokoBackup\Command\RestoreCommand;
|
||||||
|
use Joomla\Plugin\Console\MokoBackup\Command\RunCommand;
|
||||||
|
|
||||||
|
final class MokoBackupConsole extends CMSPlugin implements SubscriberInterface
|
||||||
|
{
|
||||||
|
protected $autoloadLanguage = true;
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
\Joomla\Application\Event\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerCommands(Event $event): void
|
||||||
|
{
|
||||||
|
$app = $this->getApplication();
|
||||||
|
|
||||||
|
$app->addCommand(new RunCommand());
|
||||||
|
$app->addCommand(new ListCommand());
|
||||||
|
$app->addCommand(new ProfilesCommand());
|
||||||
|
$app->addCommand(new RestoreCommand());
|
||||||
|
$app->addCommand(new CleanupCommand());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
; MokoJoomBackup — Content Plugin language file (en-GB)
|
||||||
|
PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed."
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated."
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE="Backup Profile"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups."
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Content Plugin system language file (en-GB)
|
||||||
|
PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
; MokoJoomBackup — Content Plugin language file (en-US)
|
||||||
|
PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed."
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated."
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE="Backup Profile"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups."
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
; MokoJoomBackup — Content Plugin system language file (en-US)
|
||||||
|
PLG_CONTENT_MOKOBACKUP="Content - MokoJoomBackup"
|
||||||
|
PLG_CONTENT_MOKOBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_content_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_content_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
-->
|
||||||
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
|
<name>plg_content_mokobackup</name>
|
||||||
|
<version>01.01.07-dev</version>
|
||||||
|
<creationDate>2026-06-04</creationDate>
|
||||||
|
<author>Moko Consulting</author>
|
||||||
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
|
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<description>PLG_CONTENT_MOKOBACKUP_DESCRIPTION</description>
|
||||||
|
|
||||||
|
<namespace path="src">Joomla\Plugin\Content\MokoBackup</namespace>
|
||||||
|
|
||||||
|
<files>
|
||||||
|
<filename plugin="mokobackup">mokobackup.php</filename>
|
||||||
|
<folder>services</folder>
|
||||||
|
<folder>src</folder>
|
||||||
|
</files>
|
||||||
|
|
||||||
|
<languages>
|
||||||
|
<language tag="en-GB">language/en-GB/plg_content_mokobackup.ini</language>
|
||||||
|
<language tag="en-GB">language/en-GB/plg_content_mokobackup.sys.ini</language>
|
||||||
|
</languages>
|
||||||
|
|
||||||
|
<config>
|
||||||
|
<fields name="params">
|
||||||
|
<fieldset name="basic">
|
||||||
|
<field
|
||||||
|
name="backup_before_install"
|
||||||
|
type="radio"
|
||||||
|
label="PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL"
|
||||||
|
description="PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_INSTALL_DESC"
|
||||||
|
default="0"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="backup_before_update"
|
||||||
|
type="radio"
|
||||||
|
label="PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE"
|
||||||
|
description="PLG_CONTENT_MOKOBACKUP_FIELD_BEFORE_UPDATE_DESC"
|
||||||
|
default="1"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field
|
||||||
|
name="profile_id"
|
||||||
|
type="sql"
|
||||||
|
label="PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE"
|
||||||
|
description="PLG_CONTENT_MOKOBACKUP_FIELD_PROFILE_DESC"
|
||||||
|
query="SELECT id AS value, title AS text FROM #__mokobackup_profiles WHERE published = 1 ORDER BY ordering ASC"
|
||||||
|
default="1"
|
||||||
|
>
|
||||||
|
<option value="1">Default Backup Profile</option>
|
||||||
|
</field>
|
||||||
|
</fieldset>
|
||||||
|
</fields>
|
||||||
|
</config>
|
||||||
|
</extension>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_content_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Extension\PluginInterface;
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Plugin\PluginHelper;
|
||||||
|
use Joomla\DI\Container;
|
||||||
|
use Joomla\DI\ServiceProviderInterface;
|
||||||
|
use Joomla\Event\DispatcherInterface;
|
||||||
|
use Joomla\Plugin\Content\MokoBackup\Extension\MokoBackupContent;
|
||||||
|
|
||||||
|
return new class () implements ServiceProviderInterface {
|
||||||
|
public function register(Container $container): void
|
||||||
|
{
|
||||||
|
$container->set(
|
||||||
|
PluginInterface::class,
|
||||||
|
function (Container $container) {
|
||||||
|
$plugin = new MokoBackupContent(
|
||||||
|
$container->get(DispatcherInterface::class),
|
||||||
|
(array) PluginHelper::getPlugin('content', 'mokobackup')
|
||||||
|
);
|
||||||
|
$plugin->setApplication(Factory::getApplication());
|
||||||
|
|
||||||
|
return $plugin;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage plg_content_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Plugin\Content\MokoBackup\Extension;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Plugin\CMSPlugin;
|
||||||
|
use Joomla\Component\MokoBackup\Administrator\Engine\BackupEngine;
|
||||||
|
use Joomla\Event\Event;
|
||||||
|
use Joomla\Event\SubscriberInterface;
|
||||||
|
|
||||||
|
final class MokoBackupContent extends CMSPlugin implements SubscriberInterface
|
||||||
|
{
|
||||||
|
protected $autoloadLanguage = true;
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'onExtensionBeforeInstall' => 'onExtensionBeforeInstall',
|
||||||
|
'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a backup before a new extension is installed.
|
||||||
|
*/
|
||||||
|
public function onExtensionBeforeInstall(Event $event): void
|
||||||
|
{
|
||||||
|
if (!(int) $this->params->get('backup_before_install', 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->triggerAutoBackup('Pre-install backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a backup before an extension is updated.
|
||||||
|
*/
|
||||||
|
public function onExtensionBeforeUpdate(Event $event): void
|
||||||
|
{
|
||||||
|
if (!(int) $this->params->get('backup_before_update', 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->triggerAutoBackup('Pre-update backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a backup using the configured profile.
|
||||||
|
*/
|
||||||
|
private function triggerAutoBackup(string $description): void
|
||||||
|
{
|
||||||
|
$profileId = (int) $this->params->get('profile_id', 1);
|
||||||
|
|
||||||
|
// Throttle: only one auto-backup per hour via session
|
||||||
|
$session = Factory::getSession();
|
||||||
|
$lastRun = $session->get('mokobackup.content_last_autobackup', 0);
|
||||||
|
|
||||||
|
if (time() - $lastRun < 3600) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$session->set('mokobackup.content_last_autobackup', time());
|
||||||
|
|
||||||
|
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokobackup/src/Engine/BackupEngine.php';
|
||||||
|
|
||||||
|
if (!file_exists($engineFile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!class_exists(BackupEngine::class)) {
|
||||||
|
require_once $engineFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$engine = new BackupEngine();
|
||||||
|
$engine->run($profileId, $description, 'backend');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Non-fatal — log and continue with the install/update
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
'MokoJoomBackup auto-backup failed: ' . $e->getMessage(),
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="quickicon" method="upgrade">
|
<extension type="plugin" group="quickicon" method="upgrade">
|
||||||
<name>plg_quickicon_mokobackup</name>
|
<name>plg_quickicon_mokobackup</name>
|
||||||
<version>01.00.00</version>
|
<version>01.01.07-dev</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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>plg_system_mokobackup</name>
|
<name>plg_system_mokobackup</name>
|
||||||
<version>01.00.00</version>
|
<version>01.01.07-dev</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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="task" method="upgrade">
|
<extension type="plugin" group="task" method="upgrade">
|
||||||
<name>plg_task_mokobackup</name>
|
<name>plg_task_mokobackup</name>
|
||||||
<version>01.00.00</version>
|
<version>01.01.07-dev</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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>plg_webservices_mokobackup</name>
|
<name>plg_webservices_mokobackup</name>
|
||||||
<version>01.00.00</version>
|
<version>01.01.07-dev</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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoJoomBackup</name>
|
<name>Package - MokoJoomBackup</name>
|
||||||
<packagename>mokobackup</packagename>
|
<packagename>mokobackup</packagename>
|
||||||
<version>01.00.00</version>
|
<version>01.01.07-dev</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,6 +25,9 @@
|
|||||||
<file type="plugin" id="mokobackup" group="task">plg_task_mokobackup.zip</file>
|
<file type="plugin" id="mokobackup" group="task">plg_task_mokobackup.zip</file>
|
||||||
<file type="plugin" id="mokobackup" group="quickicon">plg_quickicon_mokobackup.zip</file>
|
<file type="plugin" id="mokobackup" group="quickicon">plg_quickicon_mokobackup.zip</file>
|
||||||
<file type="plugin" id="mokobackup" group="webservices">plg_webservices_mokobackup.zip</file>
|
<file type="plugin" id="mokobackup" group="webservices">plg_webservices_mokobackup.zip</file>
|
||||||
|
<file type="plugin" id="mokobackup" group="console">plg_console_mokobackup.zip</file>
|
||||||
|
<file type="plugin" id="mokobackup" group="content">plg_content_mokobackup.zip</file>
|
||||||
|
<file type="plugin" id="mokobackup" group="actionlog">plg_actionlog_mokobackup.zip</file>
|
||||||
</files>
|
</files>
|
||||||
|
|
||||||
<languages>
|
<languages>
|
||||||
@@ -32,6 +35,8 @@
|
|||||||
</languages>
|
</languages>
|
||||||
|
|
||||||
<updateservers>
|
<updateservers>
|
||||||
<server type="extension" name="MokoJoomBackup Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/raw/branch/main/updates.xml</server>
|
<server type="extension" name="MokoJoomBackup Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/updates.xml</server>
|
||||||
</updateservers>
|
</updateservers>
|
||||||
|
<dlid prefix="dlid=" suffix=""/>
|
||||||
|
<blockChildUninstall>true</blockChildUninstall>
|
||||||
</extension>
|
</extension>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ defined('_JEXEC') or die;
|
|||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Installer\InstallerAdapter;
|
use Joomla\CMS\Installer\InstallerAdapter;
|
||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
|
use Joomla\CMS\Router\Route;
|
||||||
|
|
||||||
class Pkg_MokoBackupInstallerScript
|
class Pkg_MokoBackupInstallerScript
|
||||||
{
|
{
|
||||||
@@ -107,6 +108,39 @@ class Pkg_MokoBackupInstallerScript
|
|||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
$db->execute();
|
$db->execute();
|
||||||
|
|
||||||
|
// Enable the console plugin automatically
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->update($db->quoteName('#__extensions'))
|
||||||
|
->set($db->quoteName('enabled') . ' = 1')
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||||
|
->where($db->quoteName('folder') . ' = ' . $db->quote('console'))
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup'));
|
||||||
|
|
||||||
|
$db->setQuery($query);
|
||||||
|
$db->execute();
|
||||||
|
|
||||||
|
// Enable the content plugin automatically
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->update($db->quoteName('#__extensions'))
|
||||||
|
->set($db->quoteName('enabled') . ' = 1')
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||||
|
->where($db->quoteName('folder') . ' = ' . $db->quote('content'))
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup'));
|
||||||
|
|
||||||
|
$db->setQuery($query);
|
||||||
|
$db->execute();
|
||||||
|
|
||||||
|
// Enable the actionlog plugin automatically
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->update($db->quoteName('#__extensions'))
|
||||||
|
->set($db->quoteName('enabled') . ' = 1')
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||||
|
->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog'))
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('mokobackup'));
|
||||||
|
|
||||||
|
$db->setQuery($query);
|
||||||
|
$db->execute();
|
||||||
|
|
||||||
// Create default backup directory
|
// Create default backup directory
|
||||||
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups';
|
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokobackup/backups';
|
||||||
|
|
||||||
@@ -118,5 +152,54 @@ class Pkg_MokoBackupInstallerScript
|
|||||||
file_put_contents($backupDir . '/index.html', '<!DOCTYPE html><title></title>');
|
file_put_contents($backupDir . '/index.html', '<!DOCTYPE html><title></title>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show update site link after install or update
|
||||||
|
$this->showUpdateSiteNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an info message linking directly to the update site record
|
||||||
|
* so the user can configure their download key.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function showUpdateSiteNotice(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('us.update_site_id'))
|
||||||
|
->from($db->quoteName('#__update_sites', 'us'))
|
||||||
|
->join(
|
||||||
|
'INNER',
|
||||||
|
$db->quoteName('#__update_sites_extensions', 'use')
|
||||||
|
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
||||||
|
)
|
||||||
|
->join(
|
||||||
|
'INNER',
|
||||||
|
$db->quoteName('#__extensions', 'e')
|
||||||
|
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
||||||
|
)
|
||||||
|
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokobackup'))
|
||||||
|
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
||||||
|
->setLimit(1);
|
||||||
|
|
||||||
|
$db->setQuery($query);
|
||||||
|
$updateSiteId = (int) $db->loadResult();
|
||||||
|
|
||||||
|
if ($updateSiteId > 0) {
|
||||||
|
$editUrl = Route::_(
|
||||||
|
'index.php?option=com_installer&view=updatesites&task=updatesite.edit&id=' . $updateSiteId
|
||||||
|
);
|
||||||
|
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
Text::sprintf('PKG_MOKOBACKUP_POSTINSTALL_UPDATE_SITE', $editUrl),
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Non-critical — silently ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<updates>
|
|
||||||
<update>
|
|
||||||
<name>Package - MokoJoomBackup</name>
|
|
||||||
<description>Full-site backup and restore for Joomla</description>
|
|
||||||
<element>mokobackup</element>
|
|
||||||
<type>package</type>
|
|
||||||
<version>01.00.00</version>
|
|
||||||
<infourl title="Changelog">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/tag/v01.00.00</infourl>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases/download/v01.00.00/pkg_mokobackup-01.00.00.zip</downloadurl>
|
|
||||||
<sha256></sha256>
|
|
||||||
<targetplatform name="joomla" version="[45]\.\d+"/>
|
|
||||||
<php_minimum>8.1.0</php_minimum>
|
|
||||||
</update>
|
|
||||||
</updates>
|
|
||||||
Reference in New Issue
Block a user