Compare commits
19 Commits
development
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cc92e5ddd6 | |||
| 9210e17498 | |||
| e38607b7e6 | |||
| 026b72deed | |||
| f604def173 | |||
| af82b46fe0 | |||
| 02d8bfb089 | |||
| 6aebfc1953 | |||
| 8fb3262eb3 | |||
| 03b53d937a | |||
| eb5513f4af | |||
| 003e9617a0 | |||
| 01139c6fd4 | |||
| a6d843fd9b | |||
| b40482d8a5 | |||
| 8ebbfa7aed | |||
| 83b47ce849 | |||
| d383d1fc09 | |||
| 31941e80c3 |
@@ -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.01.21-dev</version>
|
<version>01.02.00-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>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, type-prefixed packages |
|
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
@@ -71,25 +71,20 @@ jobs:
|
|||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
if ! command -v composer &> /dev/null; then
|
||||||
echo Using pre-installed /opt/moko-platform
|
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
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
|
||||||
if ! command -v composer > /dev/null 2>&1; 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
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/moko-platform-api
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
git clone --depth 1 --branch main --quiet \
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
cd /tmp/moko-platform-api
|
cd /tmp/moko-platform-api
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/branch_rename.php \
|
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
@@ -105,15 +100,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_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 release built" >> $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:
|
||||||
@@ -155,60 +151,25 @@ jobs:
|
|||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
# Ensure PHP + Composer are available
|
||||||
echo Using pre-installed /opt/moko-platform
|
if ! command -v composer &> /dev/null; then
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
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
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
|
||||||
if ! command -v composer > /dev/null 2>&1; 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
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/moko-platform-api
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
git clone --depth 1 --branch main --quiet \
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
cd /tmp/moko-platform-api
|
cd /tmp/moko-platform-api
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_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
|
||||||
- name: Update release notes from CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
||||||
else
|
|
||||||
NOTES="Stable release"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update release body via API
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = open('/dev/stdin').read()
|
|
||||||
payload = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=payload, method='PATCH',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" <<< "$NOTES"
|
|
||||||
echo "Release notes updated from CHANGELOG.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
@@ -221,7 +182,7 @@ jobs:
|
|||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
@@ -295,7 +256,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 01.01.21
|
# VERSION: 01.02.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -159,11 +159,11 @@ jobs:
|
|||||||
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
done < <(find . -name "*.php" \( -path "*/source/*" -o -path "*/src/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${ERRORS} file(s) in source/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "JEXEC guard: OK"
|
echo "JEXEC guard: OK"
|
||||||
@@ -451,11 +451,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify package source
|
- name: Verify package source
|
||||||
run: |
|
run: |
|
||||||
SOURCE_DIR="source"
|
SOURCE_DIR="src"
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
echo "::warning::No source/, src/, or htdocs/ directory"
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
name: "Generic: Repo Health"
|
name: "Generic: Repo Health"
|
||||||
@@ -24,13 +24,12 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
profile:
|
profile:
|
||||||
description: 'Validation profile: all, release, scripts, or repo'
|
description: 'Validation profile: all, scripts, or repo'
|
||||||
required: true
|
required: true
|
||||||
default: all
|
default: all
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- all
|
- all
|
||||||
- release
|
|
||||||
- scripts
|
- scripts
|
||||||
- repo
|
- repo
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -40,10 +39,6 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Release policy - Repository Variables Only
|
|
||||||
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
|
|
||||||
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
|
||||||
|
|
||||||
# Scripts governance policy
|
# Scripts governance policy
|
||||||
SCRIPTS_REQUIRED_DIRS:
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
@@ -138,101 +133,6 @@ jobs:
|
|||||||
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
release_config:
|
|
||||||
name: Release configuration
|
|
||||||
needs: access_check
|
|
||||||
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Guardrails release vars
|
|
||||||
env:
|
|
||||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
|
||||||
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
|
|
||||||
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
|
||||||
case "${profile}" in
|
|
||||||
all|release|scripts|repo) ;;
|
|
||||||
*)
|
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
|
||||||
printf '%s\n' "Profile: ${profile}"
|
|
||||||
printf '%s\n' 'Status: SKIPPED'
|
|
||||||
printf '%s\n' 'Reason: profile excludes release validation'
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
|
|
||||||
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
|
|
||||||
|
|
||||||
missing=()
|
|
||||||
missing_optional=()
|
|
||||||
|
|
||||||
for k in "${required[@]}"; do
|
|
||||||
v="${!k:-}"
|
|
||||||
[ -z "${v}" ] && missing+=("${k}")
|
|
||||||
done
|
|
||||||
|
|
||||||
for k in "${optional[@]}"; do
|
|
||||||
v="${!k:-}"
|
|
||||||
[ -z "${v}" ] && missing_optional+=("${k}")
|
|
||||||
done
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
|
||||||
printf '%s\n' "Profile: ${profile}"
|
|
||||||
printf '%s\n' '| Variable | Status |'
|
|
||||||
printf '%s\n' '|---|---|'
|
|
||||||
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
|
|
||||||
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Missing optional repository variables'
|
|
||||||
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#missing[@]}" -gt 0 ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Missing required repository variables'
|
|
||||||
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
|
|
||||||
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Repository variables validation result'
|
|
||||||
printf '%s\n' 'Status: OK'
|
|
||||||
printf '%s\n' 'All required repository variables present.'
|
|
||||||
printf '%s\n' ''
|
|
||||||
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
scripts_governance:
|
scripts_governance:
|
||||||
name: Scripts governance
|
name: Scripts governance
|
||||||
needs: access_check
|
needs: access_check
|
||||||
@@ -256,14 +156,14 @@ jobs:
|
|||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
profile="${PROFILE_RAW:-all}"
|
||||||
case "${profile}" in
|
case "${profile}" in
|
||||||
all|release|scripts|repo) ;;
|
all|scripts|repo) ;;
|
||||||
*)
|
*)
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
|
if [ "${profile}" = 'repo' ]; then
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Scripts governance'
|
printf '%s\n' '### Scripts governance'
|
||||||
printf '%s\n' "Profile: ${profile}"
|
printf '%s\n' "Profile: ${profile}"
|
||||||
@@ -370,14 +270,14 @@ jobs:
|
|||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
profile="${PROFILE_RAW:-all}"
|
||||||
case "${profile}" in
|
case "${profile}" in
|
||||||
all|release|scripts|repo) ;;
|
all|scripts|repo) ;;
|
||||||
*)
|
*)
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
|
if [ "${profile}" = 'scripts' ]; then
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Repository health'
|
printf '%s\n' '### Repository health'
|
||||||
printf '%s\n' "Profile: ${profile}"
|
printf '%s\n' "Profile: ${profile}"
|
||||||
@@ -396,19 +296,17 @@ jobs:
|
|||||||
missing_required=()
|
missing_required=()
|
||||||
missing_optional=()
|
missing_optional=()
|
||||||
|
|
||||||
# Source directory: source/, src/, or htdocs/ (any is valid for extension repos)
|
# Source directory: src/ or htdocs/ (either is valid for extension repos)
|
||||||
SOURCE_DIR=""
|
SOURCE_DIR=""
|
||||||
if [ -d "source" ]; then
|
if [ -d "src" ]; then
|
||||||
SOURCE_DIR="source"
|
|
||||||
elif [ -d "src" ]; then
|
|
||||||
SOURCE_DIR="src"
|
SOURCE_DIR="src"
|
||||||
elif [ -d "htdocs" ]; then
|
elif [ -d "htdocs" ]; then
|
||||||
SOURCE_DIR="htdocs"
|
SOURCE_DIR="htdocs"
|
||||||
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
|
||||||
# Platform/tooling repos don't need source/
|
# Platform/tooling repos don't need src/
|
||||||
SOURCE_DIR=""
|
SOURCE_DIR=""
|
||||||
else
|
else
|
||||||
missing_required+=("source/ or htdocs/ (source directory required)")
|
missing_required+=("src/ or htdocs/ (source directory required)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for item in "${required_artifacts[@]}"; do
|
for item in "${required_artifacts[@]}"; do
|
||||||
@@ -706,7 +604,7 @@ jobs:
|
|||||||
printf '%s\n' '| Domain | Status | Notes |'
|
printf '%s\n' '| Domain | Status | Notes |'
|
||||||
printf '%s\n' '|---|---|---|'
|
printf '%s\n' '|---|---|---|'
|
||||||
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||||
printf '%s\n' '| Release variables | OK | Repository variables validation |'
|
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
|
||||||
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||||
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||||
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||||
@@ -775,11 +673,10 @@ jobs:
|
|||||||
report-issues:
|
report-issues:
|
||||||
name: "Report Issues"
|
name: "Report Issues"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [access_check, release_config, scripts_governance, repo_health]
|
needs: [access_check, scripts_governance, repo_health]
|
||||||
if: >-
|
if: >-
|
||||||
always() &&
|
always() &&
|
||||||
(needs.release_config.result == 'failure' ||
|
(needs.scripts_governance.result == 'failure' ||
|
||||||
needs.scripts_governance.result == 'failure' ||
|
|
||||||
needs.repo_health.result == 'failure')
|
needs.repo_health.result == 'failure')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -805,10 +702,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
report_gate "Release Configuration" \
|
|
||||||
"${{ needs.release_config.result }}" \
|
|
||||||
"Required repository variables are missing (RS_FTP_PATH_SUFFIX). Check repository settings."
|
|
||||||
|
|
||||||
report_gate "Scripts Governance" \
|
report_gate "Scripts Governance" \
|
||||||
"${{ needs.scripts_governance.result }}" \
|
"${{ needs.scripts_governance.result }}" \
|
||||||
"Scripts directory policy violations detected. Review required and allowed directories."
|
"Scripts directory policy violations detected. Review required and allowed directories."
|
||||||
@@ -816,4 +709,3 @@ jobs:
|
|||||||
report_gate "Repository Health" \
|
report_gate "Repository Health" \
|
||||||
"${{ needs.repo_health.result }}" \
|
"${{ needs.repo_health.result }}" \
|
||||||
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -2,9 +2,14 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
|
## [01.02.00] --- 2026-06-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Joomla-styled standalone installer (MokoRestore) with 7-step wizard, admin password reset, and client provisioning
|
- Joomla-styled standalone installer (MokoRestore) with 7-step wizard, admin password reset, and client provisioning
|
||||||
- Placeholder support for backup directories and archive filenames ([host], [date], [profile_name], etc.)
|
- Web cron trigger for shared hosting without crontab — URL-based backup with secret word, IP whitelist
|
||||||
|
- Placeholder support for backup directories and archive filenames ([host], [date], [site_name], [profile_name], etc.)
|
||||||
|
- FolderPicker JS placeholder resolution — resolves [site_name]/[host] when browsing, reverse-replaces on selection for portable profiles
|
||||||
- Archive Name Format field on backup profiles with customizable filename templates
|
- Archive Name Format field on backup profiles with customizable filename templates
|
||||||
- Interactive directory tree browser for exclude filters (replaces plain text input)
|
- Interactive directory tree browser for exclude filters (replaces plain text input)
|
||||||
- Backup log viewer modal in backup records list and inline in detail view
|
- Backup log viewer modal in backup records list and inline in detail view
|
||||||
@@ -16,19 +21,28 @@
|
|||||||
- Default directory dashboard warning when backups are stored inside web root
|
- Default directory dashboard warning when backups are stored inside web root
|
||||||
- Backup log files written alongside archives (.log)
|
- Backup log files written alongside archives (.log)
|
||||||
- Backup detail view with checksum, file path, DB size, and embedded log
|
- Backup detail view with checksum, file path, DB size, and embedded log
|
||||||
|
- Browser beforeunload warning during backup progress
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Renamed all extension elements from mokobackup to mokojoombackup (pkg, com, all plugins, DB tables, namespaces, language keys)
|
||||||
- Renamed source directory from src/ to source/ per MokoStandards convention
|
- Renamed source directory from src/ to source/ per MokoStandards convention
|
||||||
- Dashboard health check shows actual resolved backup directory path from profiles
|
- Dashboard health check shows actual resolved backup directory path from profiles
|
||||||
- Update site post-install notice links to filtered list view (avoids Joomla core bug)
|
- Update site post-install notice links to filtered list view (avoids Joomla core bug)
|
||||||
|
- License warning suppressed when download key is already configured
|
||||||
|
- Download key preserved across package updates via preflight/postflight backup
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Download ERR_INVALID_RESPONSE — flush output buffers before sending file headers
|
- Download ERR_INVALID_RESPONSE — flush output buffers before sending file headers
|
||||||
- Backup directory path resolution for absolute paths outside web root
|
- Backup directory path resolution for absolute paths outside web root
|
||||||
- Schema migrations consolidated to version 01.01.02 (within extension version range)
|
- Schema migrations consolidated to version within extension range
|
||||||
|
- PSR-4 class file naming (MokoBackup*.php → MokoJoomBackup*.php)
|
||||||
|
- Nested package directories from rename flattened
|
||||||
|
- INSERT IGNORE for default profile prevents duplicate key on update
|
||||||
|
- ActionlogsHelper::getIp() replaced — method does not exist in Joomla 5
|
||||||
- Console plugin namespace and quickicon translation keys
|
- Console plugin namespace and quickicon translation keys
|
||||||
- CLI exit codes and SQL schema defaults
|
- CLI exit codes and SQL schema defaults
|
||||||
- Component Options page (added config.xml)
|
- Component Options page (added config.xml)
|
||||||
|
- Placeholder-aware directory checks in FolderPicker and dashboard health
|
||||||
|
|
||||||
## 01.01 — 2026-06-04
|
## 01.01 — 2026-06-04
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoJoomBackup
|
# MokoJoomBackup
|
||||||
|
|
||||||
<!-- VERSION: 01.01.21 -->
|
<!-- VERSION: 01.02.00 -->
|
||||||
|
|
||||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokojoombackup</name>
|
<name>com_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</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>
|
||||||
|
|||||||
@@ -80,8 +80,24 @@ class AjaxController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$path = $this->input->getString('path', JPATH_ROOT);
|
$requestPath = $this->input->getString('path', JPATH_ROOT);
|
||||||
$path = realpath($path) ?: $path;
|
$path = realpath($requestPath) ?: $requestPath;
|
||||||
|
|
||||||
|
// Security: restrict browsing to site root and current user's home
|
||||||
|
$jRoot = realpath(JPATH_ROOT);
|
||||||
|
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: '');
|
||||||
|
$allowed = false;
|
||||||
|
|
||||||
|
if ($jRoot !== false && strpos($path, $jRoot) === 0) {
|
||||||
|
$allowed = true;
|
||||||
|
} elseif ($homeDir !== '' && strpos($path, $homeDir) === 0) {
|
||||||
|
$allowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$allowed) {
|
||||||
|
$this->sendJson(['error' => true, 'message' => 'Access denied: path outside allowed directories']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!is_dir($path)) {
|
if (!is_dir($path)) {
|
||||||
$this->sendJson(['error' => true, 'message' => 'Directory not found: ' . $path]);
|
$this->sendJson(['error' => true, 'message' => 'Directory not found: ' . $path]);
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ class BackupEngine
|
|||||||
$this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir));
|
$this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir));
|
||||||
|
|
||||||
if (!is_dir($this->backupDir)) {
|
if (!is_dir($this->backupDir)) {
|
||||||
mkdir($this->backupDir, 0755, true);
|
if (!mkdir($this->backupDir, 0755, true)) {
|
||||||
|
return ['success' => false, 'message' => 'Cannot create backup directory: ' . $this->backupDir, 'record_id' => 0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create backup record
|
// Create backup record
|
||||||
@@ -155,14 +157,22 @@ class BackupEngine
|
|||||||
$filesCount = count($filesToBackup);
|
$filesCount = count($filesToBackup);
|
||||||
$this->log('Backing up ' . $filesCount . ' files');
|
$this->log('Backing up ' . $filesCount . ' files');
|
||||||
|
|
||||||
|
$skippedFiles = 0;
|
||||||
|
|
||||||
foreach ($filesToBackup as $relativePath) {
|
foreach ($filesToBackup as $relativePath) {
|
||||||
$fullPath = JPATH_ROOT . '/' . $relativePath;
|
$fullPath = JPATH_ROOT . '/' . $relativePath;
|
||||||
|
|
||||||
if (is_file($fullPath) && is_readable($fullPath)) {
|
if (is_file($fullPath) && is_readable($fullPath)) {
|
||||||
$archiver->addFile($fullPath, $relativePath);
|
$archiver->addFile($fullPath, $relativePath);
|
||||||
|
} else {
|
||||||
|
$skippedFiles++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($skippedFiles > 0) {
|
||||||
|
$this->log('WARNING: ' . $skippedFiles . ' files skipped (not readable or missing)');
|
||||||
|
}
|
||||||
|
|
||||||
$this->log('Files added to archive');
|
$this->log('Files added to archive');
|
||||||
|
|
||||||
// Build manifest for full/differential backups (used by future differentials)
|
// Build manifest for full/differential backups (used by future differentials)
|
||||||
@@ -239,7 +249,9 @@ class BackupEngine
|
|||||||
// Write log file alongside the archive
|
// Write log file alongside the archive
|
||||||
$logContent = implode("\n", $this->log);
|
$logContent = implode("\n", $this->log);
|
||||||
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
|
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
|
||||||
@file_put_contents($logPath, $logContent);
|
if (@file_put_contents($logPath, $logContent) === false) {
|
||||||
|
error_log('MokoJoomBackup: Could not write log file: ' . $logPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Final record update
|
// Final record update
|
||||||
$update = (object) [
|
$update = (object) [
|
||||||
@@ -493,7 +505,8 @@ class BackupEngine
|
|||||||
|
|
||||||
$app->getDispatcher()->dispatch('onMokoJoomBackupAfterRun', $event);
|
$app->getDispatcher()->dispatch('onMokoJoomBackupAfterRun', $event);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Never let a listener failure break the backup result
|
// Never let a listener failure break the backup result, but log it
|
||||||
|
error_log('MokoJoomBackup: onAfterRun listener error: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ function actionDatabase(array $data): array
|
|||||||
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
|
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'success' => true,
|
'success' => ($statements > 0 || $errors === 0),
|
||||||
'message' => "Executed {$statements} statements" . ($errors ? " ({$errors} warnings)" : ''),
|
'message' => "Executed {$statements} statements" . ($errors ? " ({$errors} warnings)" : ''),
|
||||||
'statements' => $statements,
|
'statements' => $statements,
|
||||||
'errors' => $errors,
|
'errors' => $errors,
|
||||||
@@ -625,9 +625,20 @@ function actionCleanup(): array
|
|||||||
function getDbConnection(array $data): PDO
|
function getDbConnection(array $data): PDO
|
||||||
{
|
{
|
||||||
$host = $data['db_host'] ?? 'localhost';
|
$host = $data['db_host'] ?? 'localhost';
|
||||||
|
// Validate db_prefix to prevent SQL injection $prefix = $data['db_prefix'] ?? 'moko_'; if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) { throw new RuntimeException('Invalid table prefix format'); }
|
||||||
$name = $data['db_name'] ?? '';
|
$name = $data['db_name'] ?? '';
|
||||||
|
// Validate db_prefix to prevent SQL injection $prefix = $data['db_prefix'] ?? 'moko_'; if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) { throw new RuntimeException('Invalid table prefix format'); }
|
||||||
$user = $data['db_user'] ?? '';
|
$user = $data['db_user'] ?? '';
|
||||||
|
// Validate db_prefix to prevent SQL injection $prefix = $data['db_prefix'] ?? 'moko_'; if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) { throw new RuntimeException('Invalid table prefix format'); }
|
||||||
$pass = $data['db_pass'] ?? '';
|
$pass = $data['db_pass'] ?? '';
|
||||||
|
// Validate db_prefix to prevent SQL injection $prefix = $data['db_prefix'] ?? 'moko_'; if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) { throw new RuntimeException('Invalid table prefix format'); }
|
||||||
|
|
||||||
|
// Validate db_prefix to prevent SQL injection
|
||||||
|
$prefix = $data['db_prefix'] ?? 'moko_';
|
||||||
|
// Validate db_prefix to prevent SQL injection $prefix = $data['db_prefix'] ?? 'moko_'; if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) { throw new RuntimeException('Invalid table prefix format'); }
|
||||||
|
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$\/', $prefix)) {
|
||||||
|
throw new RuntimeException('Invalid table prefix format');
|
||||||
|
}
|
||||||
|
|
||||||
return new PDO(
|
return new PDO(
|
||||||
"mysql:host={$host};dbname={$name};charset=utf8mb4",
|
"mysql:host={$host};dbname={$name};charset=utf8mb4",
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ class NotificationSender
|
|||||||
|
|
||||||
return $db->loadColumn() ?: [];
|
return $db->loadColumn() ?: [];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
error_log('MokoJoomBackup: Could not resolve user group emails: ' . $e->getMessage());
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ class SteppedBackupEngine
|
|||||||
$backupDir = $this->resolveBackupDir($resolver->resolve($session->backupDir));
|
$backupDir = $this->resolveBackupDir($resolver->resolve($session->backupDir));
|
||||||
|
|
||||||
if (!is_dir($backupDir)) {
|
if (!is_dir($backupDir)) {
|
||||||
mkdir($backupDir, 0755, true);
|
if (!mkdir($backupDir, 0755, true)) {
|
||||||
|
return ['error' => true, 'message' => 'Cannot create backup directory: ' . $backupDir];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
@@ -232,11 +234,15 @@ class SteppedBackupEngine
|
|||||||
. "-- Prefix: " . $db->getPrefix() . "\n\n"
|
. "-- Prefix: " . $db->getPrefix() . "\n\n"
|
||||||
. "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n"
|
. "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n"
|
||||||
. "SET time_zone = \"+00:00\";\n\n";
|
. "SET time_zone = \"+00:00\";\n\n";
|
||||||
file_put_contents($sqlFile, $header);
|
if (file_put_contents($sqlFile, $header) === false) {
|
||||||
|
throw new \RuntimeException('Cannot write SQL dump: ' . $sqlFile);
|
||||||
|
}
|
||||||
$flags = FILE_APPEND;
|
$flags = FILE_APPEND;
|
||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents($sqlFile, $sql, $flags);
|
if (file_put_contents($sqlFile, $sql, $flags) === false) {
|
||||||
|
throw new \RuntimeException('Cannot write SQL dump: ' . $sqlFile);
|
||||||
|
}
|
||||||
$session->dbSize += strlen($sql);
|
$session->dbSize += strlen($sql);
|
||||||
|
|
||||||
$session->tableIndex++;
|
$session->tableIndex++;
|
||||||
@@ -369,6 +375,7 @@ class SteppedBackupEngine
|
|||||||
$uploader = match ($session->remoteStorage) {
|
$uploader = match ($session->remoteStorage) {
|
||||||
'ftp' => new FtpUploader($profile),
|
'ftp' => new FtpUploader($profile),
|
||||||
'google_drive' => new GoogleDriveUploader($profile),
|
'google_drive' => new GoogleDriveUploader($profile),
|
||||||
|
's3' => new S3Uploader($profile),
|
||||||
default => throw new \InvalidArgumentException('Unknown storage: ' . $session->remoteStorage),
|
default => throw new \InvalidArgumentException('Unknown storage: ' . $session->remoteStorage),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -414,7 +421,9 @@ class SteppedBackupEngine
|
|||||||
|
|
||||||
// Write log file alongside the archive
|
// Write log file alongside the archive
|
||||||
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $session->archivePath);
|
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $session->archivePath);
|
||||||
@file_put_contents($logPath, $logContent);
|
if (@file_put_contents($logPath, $logContent) === false) {
|
||||||
|
error_log('MokoJoomBackup: Could not write log file: ' . $logPath);
|
||||||
|
}
|
||||||
|
|
||||||
$update = (object) [
|
$update = (object) [
|
||||||
'id' => $session->recordId,
|
'id' => $session->recordId,
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ class SteppedSession
|
|||||||
$dir = JPATH_ROOT . '/tmp/mokojoombackup-sessions';
|
$dir = JPATH_ROOT . '/tmp/mokojoombackup-sessions';
|
||||||
|
|
||||||
if (!is_dir($dir)) {
|
if (!is_dir($dir)) {
|
||||||
mkdir($dir, 0755, true);
|
if (!mkdir($dir, 0755, true)) {
|
||||||
|
throw new \RuntimeException('Cannot create session directory: ' . $dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $dir;
|
return $dir;
|
||||||
@@ -124,7 +126,9 @@ class SteppedSession
|
|||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$path = self::getSessionPath($this->sessionId);
|
$path = self::getSessionPath($this->sessionId);
|
||||||
file_put_contents($path, json_encode(get_object_vars($this), JSON_PRETTY_PRINT));
|
if (file_put_contents($path, json_encode(get_object_vars($this), JSON_PRETTY_PRINT)) === false) {
|
||||||
|
throw new \RuntimeException('Cannot save backup session: ' . $path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="actionlog" method="upgrade">
|
<extension type="plugin" group="actionlog" method="upgrade">
|
||||||
<name>plg_actionlog_mokojoombackup</name>
|
<name>plg_actionlog_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</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="console" method="upgrade">
|
<extension type="plugin" group="console" method="upgrade">
|
||||||
<name>plg_console_mokojoombackup</name>
|
<name>plg_console_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</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="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>plg_content_mokojoombackup</name>
|
<name>plg_content_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -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_mokojoombackup</name>
|
<name>plg_quickicon_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</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_mokojoombackup</name>
|
<name>plg_system_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</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>
|
||||||
|
|||||||
@@ -151,7 +151,9 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface
|
|||||||
|
|
||||||
foreach ($expired as $record) {
|
foreach ($expired as $record) {
|
||||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
||||||
@unlink($record->absolute_path);
|
if (!@unlink($record->absolute_path)) {
|
||||||
|
continue; // Don't delete DB record if file can't be removed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$db->setQuery(
|
$db->setQuery(
|
||||||
@@ -182,7 +184,9 @@ final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface
|
|||||||
|
|
||||||
foreach ($oldest as $record) {
|
foreach ($oldest as $record) {
|
||||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
||||||
@unlink($record->absolute_path);
|
if (!@unlink($record->absolute_path)) {
|
||||||
|
continue; // Do not delete DB record if file cannot be removed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$db->setQuery(
|
$db->setQuery(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="task" method="upgrade">
|
<extension type="plugin" group="task" method="upgrade">
|
||||||
<name>plg_task_mokojoombackup</name>
|
<name>plg_task_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</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_mokojoombackup</name>
|
<name>plg_webservices_mokojoombackup</name>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</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>mokojoombackup</packagename>
|
<packagename>mokojoombackup</packagename>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.02.00</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>
|
||||||
|
|||||||
+5
-3
@@ -101,7 +101,7 @@ class Pkg_MokoJoomBackupInstallerScript
|
|||||||
$this->savedDownloadKey = $key;
|
$this->savedDownloadKey = $key;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Not critical
|
error_log('MokoJoomBackup: Could not save download key: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ class Pkg_MokoJoomBackupInstallerScript
|
|||||||
$db->execute();
|
$db->execute();
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Not critical
|
error_log('MokoJoomBackup: Could not restore download key: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +278,8 @@ class Pkg_MokoJoomBackupInstallerScript
|
|||||||
'warning'
|
'warning'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch (\Throwable $e) {}
|
catch (\Throwable $e) {
|
||||||
|
error_log('MokoJoomBackup: License key check failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user