19 Commits

Author SHA1 Message Date
gitea-actions[bot] cc92e5ddd6 chore(release): build 01.02.00 [skip ci] 2026-06-07 02:21:55 +00:00
jmiller 9210e17498 Merge pull request 'v01.02 — Full rename, installer, web cron, portable profiles' (#35) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-07 02:21:42 +00:00
Jonathan Miller e38607b7e6 Merge remote-tracking branch 'origin/main' into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 5s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Generic: Repo Health / Access control (pull_request) Successful in 3s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 7s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 14s
# Conflicts:
#	.mokogitea/workflows/auto-release.yml
#	.mokogitea/workflows/pr-check.yml
#	.mokogitea/workflows/repo-health.yml
#	src/pkg_mokobackup.xml
2026-06-06 21:21:09 -05:00
Jonathan Miller 026b72deed fix: address all PR review findings — error handling, security, validation
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 15s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Release configuration (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Security:
- browseDir restricted to JPATH_ROOT and current user $HOME (not all /home/)
- MokoRestore db_prefix validated with regex to prevent SQL injection
- MokoRestore DB import returns failure when zero statements succeed

Error handling (fatal — would produce corrupt backups):
- BackupEngine/SteppedEngine mkdir() checked, returns error on failure
- SteppedSession save() checked, throws on write failure
- SteppedEngine SQL dump file_put_contents checked, throws on failure
- MokoRestore configuration.php write checked, throws on failure

Error handling (logged — secondary operations):
- BackupEngine dispatchAfterRun catch block logs to error_log
- BackupEngine/SteppedEngine log file write failures logged
- NotificationSender user group email resolution logged
- script.php download key save/restore logged

Operational fixes:
- Cleanup plugin: don't delete DB record if file unlink fails (prevents orphans)
- BackupEngine: count and log skipped unreadable files
- BackupEngine: handle MokoRestore rename failure gracefully
- SteppedEngine: add S3Uploader to stepUpload match (feature parity)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 21:10:11 -05:00
Jonathan Miller f604def173 docs: update changelog with all dev changes for merge to main
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Release configuration (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Generic: Repo Health / Site Health (push) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 8s
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Successful in 11s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Universal: PR Check / Validate PR (pull_request) Failing after 30s
Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 20:25:56 -05:00
jmiller af82b46fe0 chore: add dlid and blockChildUninstall to package manifest [skip ci] 2026-06-04 22:02:32 +00:00
jmiller 02d8bfb089 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:58:52 +00:00
jmiller 6aebfc1953 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:41:45 +00:00
jmiller 8fb3262eb3 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:32:55 +00:00
jmiller 03b53d937a chore: remove updates.xml [skip ci] 2026-06-04 15:27:06 +00:00
jmiller eb5513f4af chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:19:46 +00:00
jmiller 003e9617a0 feat(update): migrate update server URL to Gitea Pages [skip ci] 2026-06-04 14:34:14 +00:00
jmiller 01139c6fd4 chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:24:01 +00:00
jmiller a6d843fd9b chore: sync updates.xml from development [skip ci] 2026-06-04 13:57:48 +00:00
jmiller b40482d8a5 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-04 13:47:45 +00:00
jmiller 8ebbfa7aed chore: sync updates.xml from development [skip ci] 2026-06-04 13:16:03 +00:00
jmiller 83b47ce849 chore: sync updates.xml from development [skip ci] 2026-06-04 13:02:06 +00:00
jmiller d383d1fc09 chore: sync updates.xml from development [skip ci] 2026-06-04 12:41:04 +00:00
jmiller 31941e80c3 chore: sync updates.xml 01.02.00-rc from rc [skip ci] 2026-06-04 12:13:17 +00:00
24 changed files with 154 additions and 227 deletions
+1 -1
View File
@@ -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>
+31 -70
View File
@@ -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
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 $CLONE_URL /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 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
- 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
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 $CLONE_URL /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 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
- 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
+1 -1
View File
@@ -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"
+4 -5
View File
@@ -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)
+13 -121
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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());
}
} }
} }