fix: admin submenu items and icons for Joomla 6 #45
+16
-16
@@ -1,4 +1,4 @@
|
||||
# MokoJoomBackup
|
||||
# MokoSuiteBackup
|
||||
|
||||
Full-site backup and restore for Joomla — database, files, and configuration. Replaces Akeeba Backup Pro.
|
||||
|
||||
@@ -6,10 +6,10 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Package** | `pkg_mokojoombackup` |
|
||||
| **Package** | `pkg_mokosuitebackup` |
|
||||
| **Language** | PHP 8.1+ |
|
||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||
| **Wiki** | [MokoJoomBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/wiki) |
|
||||
| **Wiki** | [MokoSuiteBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/wiki) |
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -26,32 +26,32 @@ composer install # Install PHP dependencies
|
||||
|
||||
Joomla **package** with four sub-extensions:
|
||||
|
||||
### com_mokojoombackup (Component)
|
||||
### com_mokosuitebackup (Component)
|
||||
- Admin backend for managing backup profiles and records
|
||||
- Backup engine: `Engine/BackupEngine`, `Engine/DatabaseDumper`, `Engine/FileScanner`, `Engine/Archiver`
|
||||
- Joomla 4/5 MVC: Controllers, Models, Views, Tables
|
||||
- Namespace: `Joomla\Component\MokoJoomBackup\Administrator`
|
||||
- DB tables: `#__mokojoombackup_profiles`, `#__mokojoombackup_records`
|
||||
- CLI: `cli/mokojoombackup.php` for cron-based backups
|
||||
- Namespace: `Joomla\Component\MokoSuiteBackup\Administrator`
|
||||
- DB tables: `#__mokosuitebackup_profiles`, `#__mokosuitebackup_records`
|
||||
- CLI: `cli/mokosuitebackup.php` for cron-based backups
|
||||
|
||||
### plg_system_mokojoombackup (System Plugin)
|
||||
### plg_system_mokosuitebackup (System Plugin)
|
||||
- Cleanup of expired backup archives (age + count limits)
|
||||
- Namespace: `Joomla\Plugin\System\MokoJoomBackup`
|
||||
- Namespace: `Joomla\Plugin\System\MokoSuiteBackup`
|
||||
|
||||
### plg_task_mokojoombackup (Task Plugin)
|
||||
### plg_task_mokosuitebackup (Task Plugin)
|
||||
- Integrates with Joomla's Scheduled Tasks (com_scheduler)
|
||||
- Registers "Run Backup Profile" task type
|
||||
- Namespace: `Joomla\Plugin\Task\MokoJoomBackup`
|
||||
- Namespace: `Joomla\Plugin\Task\MokoSuiteBackup`
|
||||
|
||||
### plg_webservices_mokojoombackup (WebServices Plugin)
|
||||
- REST API for remote backup management (wire-compatible with mcp_mokojoombackup)
|
||||
### plg_webservices_mokosuitebackup (WebServices Plugin)
|
||||
- REST API for remote backup management (wire-compatible with mcp_mokosuitebackup)
|
||||
- Endpoints: backup, backups, profiles, download, delete
|
||||
- Namespace: `Joomla\Plugin\WebServices\MokoJoomBackup`
|
||||
- Namespace: `Joomla\Plugin\WebServices\MokoSuiteBackup`
|
||||
|
||||
### Database Schema
|
||||
|
||||
- `#__mokojoombackup_profiles` — backup profiles (name, description, config JSON, filters JSON)
|
||||
- `#__mokojoombackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps)
|
||||
- `#__mokosuitebackup_profiles` — backup profiles (name, description, config JSON, filters JSON)
|
||||
- `#__mokosuitebackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps)
|
||||
|
||||
## Rules
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
|
||||
<identity>
|
||||
<name>MokoJoomBackup</name>
|
||||
<display-name>Package - MokoJoomBackup</display-name>
|
||||
<name>MokoSuiteBackup</name>
|
||||
<display-name>Package - MokoSuiteBackup</display-name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
|
||||
<version>01.08.00-dev</version>
|
||||
<version>01.20.00-dev</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/moko-platform/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||
/tmp/moko-platform-api
|
||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 01.00.00
|
||||
# INGROUP: mokoplatform.Automation
|
||||
# VERSION: 01.20.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- name: Create branch and comment
|
||||
run: |
|
||||
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||
|
||||
+508
-508
File diff suppressed because it is too large
Load Diff
@@ -8,4 +8,245 @@
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- 'fix/**'
|
||||
- 'patch/**'
|
||||
- 'hotfix/**'
|
||||
- 'bugfix/**'
|
||||
- 'chore/**'
|
||||
- alpha
|
||||
- beta
|
||||
- rc
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stability:
|
||||
description: 'Pre-release channel'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- development
|
||||
- alpha
|
||||
- beta
|
||||
- release-candidate
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||
runs-on: release
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Setup moko-platform tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/moko-platform
|
||||
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
|
||||
|
||||
- name: Detect platform
|
||||
id: platform
|
||||
run: |
|
||||
# Auto-detect and update platform if not set in manifest
|
||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
run: |
|
||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
case "${{ github.ref_name }}" in
|
||||
rc) STABILITY="release-candidate" ;;
|
||||
alpha) STABILITY="alpha" ;;
|
||||
beta) STABILITY="beta" ;;
|
||||
*) STABILITY="development" ;;
|
||||
esac
|
||||
else
|
||||
STABILITY="${{ inputs.stability || 'development' }}"
|
||||
fi
|
||||
|
||||
case "$STABILITY" in
|
||||
development) SUFFIX="-dev"; TAG="development" ;;
|
||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||
esac
|
||||
|
||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||
case "$STABILITY" in
|
||||
release-candidate) BUMP="minor" ;;
|
||||
*) BUMP="patch" ;;
|
||||
esac
|
||||
|
||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||
|
||||
# Set stability suffix and verify consistency
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
|
||||
php ${MOKO_CLI}/version_set_platform.php \
|
||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||
|
||||
# Append suffix for output
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
VERSION="${VERSION}${SUFFIX}"
|
||||
fi
|
||||
|
||||
# Commit version bump
|
||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||
git config --local user.name "gitea-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
git add -A
|
||||
git diff --cached --quiet || {
|
||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||
git push origin HEAD 2>&1
|
||||
}
|
||||
|
||||
# Auto-detect element via manifest_element.php
|
||||
php ${MOKO_CLI}/manifest_element.php \
|
||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||
--repo "${GITEA_REPO}" --github-output
|
||||
|
||||
# Read back element outputs
|
||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_create.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||
else
|
||||
NOTES="Release ${VERSION}"
|
||||
fi
|
||||
|
||||
# Update release body via API
|
||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
"${API_BASE}/releases/tags/${TAG}" | 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
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_package.php \
|
||||
--path . --version "$VERSION" --tag "$TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
--repo "${GITEA_REPO}" --output /tmp || true
|
||||
|
||||
# updates.xml is generated dynamically by MokoGitea license server
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
php ${MOKO_CLI}/release_cascade.php \
|
||||
--stability "${{ steps.meta.outputs.stability }}" \
|
||||
--token "${TOKEN}" \
|
||||
--api-base "${API_BASE}"
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoPlatform.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
|
||||
name: "RC Revert"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
name: Rename rc/ back to dev/
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == false &&
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
run: |
|
||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||
SUFFIX="${BRANCH#rc/}"
|
||||
DEV_BRANCH="dev/${SUFFIX}"
|
||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Create dev/ branch from rc/ branch
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||
"${API}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "201" ]; then
|
||||
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delete rc/ branch
|
||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||
fi
|
||||
|
||||
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||
File diff suppressed because it is too large
Load Diff
+9
-2
@@ -1,6 +1,13 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Admin submenu items (Dashboard, Backups, Profiles) not created on install — `<submenu>` block in manifest was empty
|
||||
- Submenu items not created on update — added `ensureSubmenuItems()` using Joomla's `MenuTable` API with proper nested set positioning
|
||||
- Submenu icons not rendering in Joomla 6 — set `menu_icon` param for level 2+ items (Atum only renders `img` column icons for level 1)
|
||||
- CSS selector `#menu` → `.main-nav` for icon injection (Joomla 6 uses dynamic `id="menu{moduleId}"`)
|
||||
- Use `margin-inline-end` instead of `margin-right` for RTL layout support
|
||||
|
||||
## [01.08.00] --- 2026-06-07
|
||||
|
||||
## [01.07.00] --- 2026-06-07
|
||||
@@ -12,13 +19,13 @@
|
||||
|
||||
### Added
|
||||
- Dashboard submenu entry as default landing page with `class:home` icon
|
||||
- `[DEFAULT_DIR]` placeholder for portable backup directory configuration — resolves to `administrator/components/com_mokojoombackup/backups` at runtime
|
||||
- `[DEFAULT_DIR]` placeholder for portable backup directory configuration — resolves to `administrator/components/com_mokosuitebackup/backups` at runtime
|
||||
- Live AJAX directory validation on backup_dir field — checks existence, writability, and placeholder resolution as user types (debounced 400ms)
|
||||
- `checkDir` AJAX endpoint for real-time directory permission checking
|
||||
- Web-accessible warning badge on backup download buttons when archive is inside web root
|
||||
- Inline security warning in FolderPicker when default directory is selected
|
||||
- Auto `.htaccess` and `index.html` protection for web-accessible backup directories on profile save and at backup time
|
||||
- Font Awesome 6 submenu icons via CSS injection in `MokoJoomBackupComponent::boot()`
|
||||
- Font Awesome 6 submenu icons via CSS injection in `MokoSuiteBackupComponent::boot()`
|
||||
- `syncMenuIcons()` installer postflight — syncs icon classes to `#__menu` on install and update
|
||||
- `encryptionPassword` property on `SteppedSession` for upcoming stepped backup encryption support
|
||||
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Contributing to MokoJoomBackup
|
||||
# Contributing to MokoSuiteBackup
|
||||
|
||||
Thank you for your interest in contributing to MokoJoomBackup.
|
||||
Thank you for your interest in contributing to MokoSuiteBackup.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -27,7 +27,7 @@ Thank you for your interest in contributing to MokoJoomBackup.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/issues).
|
||||
Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/issues).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# MokoJoomBackup — Full-site backup and restore for Joomla
|
||||
# MokoSuiteBackup — Full-site backup and restore for Joomla
|
||||
#
|
||||
# Builds and releases are handled by CI workflows (pre-release.yml,
|
||||
# auto-release.yml). This Makefile provides local validation helpers
|
||||
@@ -12,7 +12,7 @@
|
||||
# CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
EXTENSION_NAME := mokojoombackup
|
||||
EXTENSION_NAME := mokosuitebackup
|
||||
EXTENSION_TYPE := package
|
||||
|
||||
SRC_DIR := source
|
||||
@@ -20,7 +20,7 @@ SRC_DIR := source
|
||||
# Gitea
|
||||
GITEA_URL := https://git.mokoconsulting.tech
|
||||
GITEA_ORG := MokoConsulting
|
||||
GITEA_REPO := MokoJoomBackup
|
||||
GITEA_REPO := MokoSuiteBackup
|
||||
|
||||
# Tools
|
||||
PHP := php
|
||||
@@ -44,7 +44,7 @@ COLOR_RED := \033[31m
|
||||
.PHONY: help
|
||||
help: ## Show this help message
|
||||
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
||||
@echo "$(COLOR_BLUE)║ MokoJoomBackup Makefile ║$(COLOR_RESET)"
|
||||
@echo "$(COLOR_BLUE)║ MokoSuiteBackup Makefile ║$(COLOR_RESET)"
|
||||
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
||||
@echo ""
|
||||
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
||||
@@ -158,7 +158,7 @@ release-rc: validate validate-xml ## Trigger release-candidate build via CI work
|
||||
|
||||
.PHONY: version
|
||||
version: ## Display version from package manifest
|
||||
@VERSION=$$(grep '<version>' $(SRC_DIR)/pkg_mokojoombackup.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/'); \
|
||||
@VERSION=$$(grep '<version>' $(SRC_DIR)/pkg_mokosuitebackup.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/'); \
|
||||
echo "$(COLOR_BLUE)$(EXTENSION_NAME)$(COLOR_RESET) v$$VERSION ($(EXTENSION_TYPE))"
|
||||
|
||||
# Default target
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# MokoJoomBackup
|
||||
# MokoSuiteBackup
|
||||
|
||||
<!-- VERSION: 01.08.00 -->
|
||||
<!-- VERSION: 01.20.00 -->
|
||||
|
||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||
|
||||
## Overview
|
||||
|
||||
MokoJoomBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It creates complete site backups including the database, files, and configuration, packaged into downloadable ZIP archives. Supports multiple backup profiles, scheduled backups via CLI/cron, and a REST API for remote management.
|
||||
MokoSuiteBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It creates complete site backups including the database, files, and configuration, packaged into downloadable ZIP archives. Supports multiple backup profiles, scheduled backups via CLI/cron, and a REST API for remote management.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -25,13 +25,13 @@ MokoJoomBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It cre
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download `pkg_mokobackup-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases)
|
||||
1. Download `pkg_mokobackup-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/releases)
|
||||
2. Joomla Administrator > Extensions > Install
|
||||
3. System plugin enabled automatically on install
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Component**: Administrator > Components > MokoJoomBackup
|
||||
- **Component**: Administrator > Components > MokoSuiteBackup
|
||||
- **Profiles**: Create backup profiles with different file/database filters
|
||||
- **System Plugin**: Configure scheduled backup triggers and notifications
|
||||
- **CLI**: `php cli/mokobackup.php --profile=1` for cron-based backups
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage plg_webservices_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
+7
-8
@@ -1,14 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage plg_webservices_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>plg_webservices_mokojoombackup</name>
|
||||
<version>01.08.00</version>
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>01.10.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -17,16 +16,16 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
|
||||
<namespace path="src">Joomla\Plugin\WebServices\MokoJoomBackup</namespace>
|
||||
<namespace path="src">Joomla\Plugin\WebServices\MokoSuiteBackup</namespace>
|
||||
|
||||
<files>
|
||||
<filename plugin="mokojoombackup">mokojoombackup.php</filename>
|
||||
<filename plugin="mokosuitebackup">mokosuitebackup.php</filename>
|
||||
<folder>services</folder>
|
||||
<folder>src</folder>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
<language tag="en-GB">language/en-GB/plg_webservices_mokojoombackup.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_webservices_mokojoombackup.sys.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_webservices_mokosuitebackup.ini</language>
|
||||
<language tag="en-GB">language/en-GB/plg_webservices_mokosuitebackup.sys.ini</language>
|
||||
</languages>
|
||||
</extension>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage plg_webservices_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Joomla\Plugin\WebServices\MokoSuiteBackup\Extension\MokoSuiteBackupWebServices;
|
||||
|
||||
return new class () implements ServiceProviderInterface {
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$plugin = new MokoSuiteBackupWebServices(
|
||||
$container->get(DispatcherInterface::class),
|
||||
(array) PluginHelper::getPlugin('webservices', 'mokosuitebackup')
|
||||
);
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
; MokoJoomBackup — Package language file (en-GB)
|
||||
; @package MokoJoomBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
PKG_MOKOJOOMBACKUP="Package - MokoJoomBackup"
|
||||
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
||||
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
@@ -0,0 +1,10 @@
|
||||
; MokoSuiteBackup — Package language file (en-GB)
|
||||
; @package MokoSuiteBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
PKG_MOKOJOOMBACKUP="Package - MokoSuiteBackup"
|
||||
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoSuiteBackup requires PHP %s or later."
|
||||
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
@@ -1,10 +0,0 @@
|
||||
; MokoJoomBackup — Package language file (en-US)
|
||||
; @package MokoJoomBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
PKG_MOKOJOOMBACKUP="Package - MokoJoomBackup"
|
||||
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
||||
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
@@ -0,0 +1,10 @@
|
||||
; MokoSuiteBackup — Package language file (en-US)
|
||||
; @package MokoSuiteBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
PKG_MOKOJOOMBACKUP="Package - MokoSuiteBackup"
|
||||
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoSuiteBackup requires PHP %s or later."
|
||||
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
@@ -1,11 +0,0 @@
|
||||
; MokoJoomBackup — Component system language file (en-GB)
|
||||
; @package MokoJoomBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoJoomBackup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||
@@ -1,11 +0,0 @@
|
||||
; MokoJoomBackup — Component system language file (en-US)
|
||||
; @package MokoJoomBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoJoomBackup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP TABLE IF EXISTS `#__mokojoombackup_records`;
|
||||
DROP TABLE IF EXISTS `#__mokojoombackup_profiles`;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `#__mokojoombackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive';
|
||||
@@ -1,12 +0,0 @@
|
||||
-- MokoJoomBackup 01.01.02
|
||||
-- Consolidated schema updates: NULL defaults, notifications, archive name format
|
||||
|
||||
-- Fix: allow NULL defaults for manifest and log columns
|
||||
ALTER TABLE `#__mokojoombackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL;
|
||||
ALTER TABLE `#__mokojoombackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
|
||||
|
||||
-- Add user group notifications column to profiles
|
||||
ALTER TABLE `#__mokojoombackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
|
||||
|
||||
-- Add archive_name_format column with placeholder support
|
||||
ALTER TABLE `#__mokojoombackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\MVCComponent;
|
||||
use Joomla\CMS\Factory;
|
||||
|
||||
class MokoJoomBackupComponent extends MVCComponent
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
try {
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if (!$app->isClient('administrator')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wa = $app->getDocument()->getWebAssetManager();
|
||||
$wa->addInlineStyle(
|
||||
'#menu a[href*="com_mokojoombackup"][href*="view=dashboard"] .sidebar-item-title::before,'
|
||||
. ' #menu a[href*="com_mokojoombackup"][href*="view=backups"] .sidebar-item-title::before,'
|
||||
. ' #menu a[href*="com_mokojoombackup"][href*="view=profiles"] .sidebar-item-title::before'
|
||||
. ' { font-family: "Font Awesome 6 Free"; font-weight: 900; margin-right: .5em; }'
|
||||
. ' #menu a[href*="com_mokojoombackup"][href*="view=dashboard"] .sidebar-item-title::before { content: "\f015"; }'
|
||||
. ' #menu a[href*="com_mokojoombackup"][href*="view=backups"] .sidebar-item-title::before { content: "\f1c0"; }'
|
||||
. ' #menu a[href*="com_mokojoombackup"][href*="view=profiles"] .sidebar-item-title::before { content: "\f013"; }'
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoJoomBackup: boot() CSS injection failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Utility;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
class BackupDirectory
|
||||
{
|
||||
public const DEFAULT_RELATIVE = 'administrator/components/com_mokojoombackup/backups';
|
||||
|
||||
public const PLACEHOLDER = '[DEFAULT_DIR]';
|
||||
|
||||
private const HTACCESS_CONTENT = <<<'HTACCESS'
|
||||
# Apache 2.4+
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
# Apache 2.2
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
HTACCESS;
|
||||
|
||||
private const INDEX_CONTENT = '<!DOCTYPE html><title></title>';
|
||||
|
||||
/**
|
||||
* Get the absolute default backup directory path.
|
||||
*/
|
||||
public static function getDefaultAbsolute(): string
|
||||
{
|
||||
return JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a backup directory path. Replaces [DEFAULT_DIR] placeholder,
|
||||
* then resolves relative paths from JPATH_ROOT.
|
||||
*
|
||||
* @param string $dir Raw directory value from profile
|
||||
*
|
||||
* @return string Absolute path (may still contain other placeholders)
|
||||
*/
|
||||
public static function resolve(string $dir): string
|
||||
{
|
||||
if ($dir === '' || $dir === self::PLACEHOLDER) {
|
||||
$dir = self::getDefaultAbsolute();
|
||||
} else {
|
||||
$dir = str_replace(self::PLACEHOLDER, self::getDefaultAbsolute(), $dir);
|
||||
}
|
||||
|
||||
if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
|
||||
return rtrim($dir, '/\\');
|
||||
}
|
||||
|
||||
return JPATH_ROOT . '/' . $dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a resolved path still contains unresolved placeholders.
|
||||
*/
|
||||
public static function hasPlaceholders(string $path): bool
|
||||
{
|
||||
return (bool) preg_match('/\[.+\]/', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a resolved absolute path is inside the web root.
|
||||
*/
|
||||
public static function isWebAccessible(string $absolutePath): bool
|
||||
{
|
||||
$jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
|
||||
$realDir = realpath($absolutePath) ?: $absolutePath;
|
||||
|
||||
return strpos($realDir, $jRoot) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create .htaccess and index.html protection files in a directory.
|
||||
* Only creates files if they don't already exist.
|
||||
*/
|
||||
public static function protect(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$htaccess = $dir . '/.htaccess';
|
||||
|
||||
if (!is_file($htaccess)) {
|
||||
if (@file_put_contents($htaccess, self::HTACCESS_CONTENT . "\n") === false) {
|
||||
error_log('MokoJoomBackup: Could not create .htaccess in: ' . $dir);
|
||||
}
|
||||
}
|
||||
|
||||
$index = $dir . '/index.html';
|
||||
|
||||
if (!is_file($index)) {
|
||||
if (@file_put_contents($index, self::INDEX_CONTENT) === false) {
|
||||
error_log('MokoJoomBackup: Could not create index.html in: ' . $dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the backup directory exists, create it if needed,
|
||||
* and apply web protection if it's inside the web root.
|
||||
*
|
||||
* @return bool True if directory exists and is usable
|
||||
*/
|
||||
public static function ensureReady(string $dir): bool
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
if (!@mkdir($dir, 0755, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
self::protect($dir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a newline-separated text field into an array of trimmed, non-empty strings.
|
||||
*/
|
||||
public static function parseNewlineList(string $text): array
|
||||
{
|
||||
if (empty($text)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(
|
||||
array_map('trim', explode("\n", str_replace("\r", '', $text))),
|
||||
fn($line) => $line !== ''
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the log file path from an archive path.
|
||||
*/
|
||||
public static function logPathFromArchive(string $archivePath): string
|
||||
{
|
||||
return preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<access component="com_mokosuitebackup">
|
||||
<section name="component">
|
||||
<action name="core.admin" title="JACTION_ADMIN" />
|
||||
<action name="core.options" title="JACTION_OPTIONS" />
|
||||
<action name="core.manage" title="JACTION_MANAGE" />
|
||||
<action name="core.create" title="JACTION_CREATE" />
|
||||
<action name="core.delete" title="JACTION_DELETE" />
|
||||
<action name="core.edit" title="JACTION_EDIT" />
|
||||
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
||||
<action name="mokosuitebackup.backup.run" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN" />
|
||||
<action name="mokosuitebackup.backup.download" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD" />
|
||||
<action name="mokosuitebackup.backup.restore" title="COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE" />
|
||||
</section>
|
||||
</access>
|
||||
+48
-10
@@ -1,19 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Api\Controller;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Api\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\ApiController;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
|
||||
|
||||
class BackupsController extends ApiController
|
||||
{
|
||||
@@ -21,10 +21,18 @@ class BackupsController extends ApiController
|
||||
protected $default_view = 'backups';
|
||||
|
||||
/**
|
||||
* Start a new backup (POST /api/index.php/v1/mokojoombackup/backup)
|
||||
* Start a new backup (POST /api/index.php/v1/mokosuitebackup/backup)
|
||||
*/
|
||||
public function backup(): static
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
$this->app->setHeader('status', 403);
|
||||
echo json_encode(['errors' => [['title' => 'Access denied']]]);
|
||||
$this->app->close();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$data = json_decode($this->input->json->getRaw(), true) ?: [];
|
||||
|
||||
$profileId = (int) ($data['profile'] ?? 1);
|
||||
@@ -47,10 +55,18 @@ class BackupsController extends ApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a backup archive (GET /api/index.php/v1/mokojoombackup/backup/:id/download)
|
||||
* Download a backup archive (GET /api/index.php/v1/mokosuitebackup/backup/:id/download)
|
||||
*/
|
||||
public function download(): static
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) {
|
||||
$this->app->setHeader('status', 403);
|
||||
echo json_encode(['errors' => [['title' => 'Access denied']]]);
|
||||
$this->app->close();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$id = $this->input->getInt('id', 0);
|
||||
|
||||
$model = $this->getModel('Backup', 'Administrator');
|
||||
@@ -64,20 +80,42 @@ class BackupsController extends ApiController
|
||||
return $this;
|
||||
}
|
||||
|
||||
$content = base64_encode(file_get_contents($item->absolute_path));
|
||||
// Stream as binary download instead of base64 to avoid memory exhaustion
|
||||
while (@ob_end_clean()) {
|
||||
// clear all buffers
|
||||
}
|
||||
|
||||
$filename = basename($item->archivename ?? $item->absolute_path);
|
||||
$filesize = filesize($item->absolute_path);
|
||||
$contentType = str_ends_with($filename, '.tar.gz')
|
||||
? 'application/gzip'
|
||||
: 'application/zip';
|
||||
|
||||
header('Content-Type: ' . $contentType);
|
||||
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
|
||||
header('Content-Length: ' . $filesize);
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
|
||||
readfile($item->absolute_path);
|
||||
|
||||
$this->app->setHeader('status', 200);
|
||||
echo json_encode(['data' => $content]);
|
||||
$this->app->close();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* List backup profiles (GET /api/index.php/v1/mokojoombackup/profiles)
|
||||
* List backup profiles (GET /api/index.php/v1/mokosuitebackup/profiles)
|
||||
*/
|
||||
public function profiles(): static
|
||||
{
|
||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
|
||||
$this->app->setHeader('status', 403);
|
||||
echo json_encode(['errors' => [['title' => 'Access denied']]]);
|
||||
$this->app->close();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$model = $this->getModel('Profiles', 'Administrator');
|
||||
$items = $model->getItems();
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Api\View\Backups;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Api\View\Backups;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -10,7 +10,7 @@
|
||||
* CLI backup script for cron/scheduled use.
|
||||
*
|
||||
* Usage:
|
||||
* php cli/mokojoombackup.php --profile=1 --description="Scheduled backup"
|
||||
* php cli/mokosuitebackup.php --profile=1 --description="Scheduled backup"
|
||||
*
|
||||
* Must be run from the Joomla root directory.
|
||||
*/
|
||||
@@ -30,7 +30,7 @@ if (!defined('JPATH_BASE')) {
|
||||
require_once JPATH_BASE . '/includes/framework.php';
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
|
||||
|
||||
// Parse CLI arguments
|
||||
$profileId = 1;
|
||||
@@ -51,7 +51,7 @@ if (empty($description)) {
|
||||
// Boot the application
|
||||
$app = Factory::getApplication('administrator');
|
||||
|
||||
echo "MokoJoomBackup CLI\n";
|
||||
echo "MokoSuiteBackup CLI\n";
|
||||
echo "Profile: {$profileId}\n";
|
||||
echo "Description: {$description}\n";
|
||||
echo "Starting backup...\n\n";
|
||||
+31
-6
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -13,15 +13,15 @@
|
||||
type="FolderPicker"
|
||||
label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR"
|
||||
description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC"
|
||||
default="administrator/components/com_mokojoombackup/backups"
|
||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
||||
default="administrator/components/com_mokosuitebackup/backups"
|
||||
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
|
||||
/>
|
||||
<field
|
||||
name="default_profile"
|
||||
type="sql"
|
||||
label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE"
|
||||
description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC"
|
||||
query="SELECT id AS value, title AS text FROM #__mokojoombackup_profiles WHERE published = 1 ORDER BY ordering ASC"
|
||||
query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC"
|
||||
default="1"
|
||||
>
|
||||
<option value="1">Default Backup Profile</option>
|
||||
@@ -71,6 +71,31 @@
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="preaction" label="COM_MOKOJOOMBACKUP_CONFIG_PREACTION">
|
||||
<field
|
||||
name="backup_before_update"
|
||||
type="radio"
|
||||
label="COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE"
|
||||
description="COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE_DESC"
|
||||
default="0"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field
|
||||
name="backup_before_uninstall"
|
||||
type="radio"
|
||||
label="COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL"
|
||||
description="COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC"
|
||||
default="0"
|
||||
class="btn-group"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="cleanup" label="COM_MOKOJOOMBACKUP_CONFIG_CLEANUP">
|
||||
<field
|
||||
name="max_age_days"
|
||||
@@ -133,7 +158,7 @@
|
||||
label="JCONFIG_PERMISSIONS_LABEL"
|
||||
filter="rules"
|
||||
validate="rules"
|
||||
component="com_mokojoombackup"
|
||||
component="com_mokosuitebackup"
|
||||
section="component"
|
||||
/>
|
||||
</fieldset>
|
||||
+35
-4
@@ -68,7 +68,7 @@
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC"
|
||||
default="[DEFAULT_DIR]"
|
||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
||||
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
|
||||
/>
|
||||
<field
|
||||
name="archive_name_format"
|
||||
@@ -129,7 +129,7 @@
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DIRS_DESC"
|
||||
filter="raw"
|
||||
hint="tmp"
|
||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
||||
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
|
||||
/>
|
||||
<field
|
||||
name="exclude_files"
|
||||
@@ -138,7 +138,7 @@
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_FILES_DESC"
|
||||
filter="raw"
|
||||
hint="*.bak"
|
||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
||||
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
|
||||
/>
|
||||
<field
|
||||
name="exclude_tables"
|
||||
@@ -146,7 +146,7 @@
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_DESC"
|
||||
filter="raw"
|
||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
||||
addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
@@ -215,6 +215,37 @@
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field
|
||||
name="ntfy_spacer"
|
||||
type="note"
|
||||
label=""
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC"
|
||||
class="alert alert-light border"
|
||||
/>
|
||||
<field
|
||||
name="ntfy_topic"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC_DESC"
|
||||
maxlength="255"
|
||||
hint="my-backups"
|
||||
/>
|
||||
<field
|
||||
name="ntfy_server"
|
||||
type="url"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER_DESC"
|
||||
maxlength="512"
|
||||
default="https://ntfy.sh"
|
||||
hint="https://ntfy.sh"
|
||||
/>
|
||||
<field
|
||||
name="ntfy_token"
|
||||
type="password"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN_DESC"
|
||||
maxlength="255"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="ftp" label="COM_MOKOJOOMBACKUP_FIELDSET_FTP">
|
||||
+29
-8
@@ -1,10 +1,10 @@
|
||||
; MokoJoomBackup — Component language file (en-GB)
|
||||
; @package MokoJoomBackup
|
||||
; MokoSuiteBackup — Component language file (en-GB)
|
||||
; @package MokoSuiteBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoJoomBackup"
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
||||
|
||||
; Submenu
|
||||
@@ -12,8 +12,16 @@ COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||
|
||||
; ACL Actions
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN="Run Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN_DESC="Allows users in this group to trigger backup operations."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD="Download Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD_DESC="Allows users in this group to download backup archive files. These archives contain the full database and site files."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
|
||||
|
||||
; Dashboard view
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
||||
@@ -94,7 +102,7 @@ COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD_DESC="Set a password to encrypt the
|
||||
COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE="Split Size (MB)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE_DESC="Split archive into parts of this size in MB. 0 = no splitting."
|
||||
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR="Backup Directory"
|
||||
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Directory where backup archives are stored. Supports placeholders: [host], [date], [year], [month], [day], [profile_name], [site_name], [type]. Absolute paths (starting with /) are used as-is; relative paths resolve from the Joomla root."
|
||||
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Directory where backup archives are stored. Supports placeholders: [HOME] (user home directory), [host], [date], [year], [month], [day], [profile_name], [site_name], [type]. Use [HOME]/backups to store outside the web root. Absolute paths (starting with /) are used as-is; relative paths resolve from the Joomla root."
|
||||
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT="Archive Name Format"
|
||||
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC="Filename template for backup archives (without extension). Placeholders: [host] hostname, [date] Ymd, [time] His, [datetime] Ymd_His, [year] [month] [day] [hour] [minute] [second], [profile_id], [profile_name], [site_name], [type], [random]."
|
||||
COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE="Include Restore Script"
|
||||
@@ -189,6 +197,13 @@ COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS="Notify on Success"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS_DESC="Send an email when a backup completes successfully."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE="Notify on Failure"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE_DESC="Send an email when a backup fails. Includes log excerpt for debugging."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_SPACER_DESC="<strong>Push Notifications (ntfy)</strong> — Send instant push notifications to your phone or desktop via <a href='https://ntfy.sh' target='_blank'>ntfy.sh</a> or a self-hosted ntfy server."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC="ntfy Topic"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOPIC_DESC="The ntfy topic to publish notifications to. Leave blank to disable push notifications."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER="ntfy Server"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_SERVER_DESC="URL of the ntfy server. Default is the public ntfy.sh service. Use your own server URL for self-hosted instances."
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN="Access Token"
|
||||
COM_MOKOJOOMBACKUP_FIELD_NTFY_TOKEN_DESC="Optional access token for private ntfy topics. Leave blank for public topics."
|
||||
|
||||
; Integrity verification
|
||||
COM_MOKOJOOMBACKUP_TOOLBAR_VERIFY="Verify Integrity"
|
||||
@@ -216,8 +231,8 @@ COM_MOKOJOOMBACKUP_AKEEBA_NOT_FOUND="Akeeba Backup tables not found. Is Akeeba B
|
||||
|
||||
; Update site notice
|
||||
COM_MOKOJOOMBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key."
|
||||
COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server."
|
||||
COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoSuiteBackup update site not found. Reinstall the package to register the update server."
|
||||
COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
|
||||
; Component Options (config.xml)
|
||||
COM_MOKOJOOMBACKUP_CONFIG_GENERAL="General"
|
||||
@@ -227,6 +242,12 @@ COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE="Default Profile"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC="Default backup profile used by quick actions and CLI when no profile is specified."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE="Show Update Site Notice"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site configuration notice on the Backup Records view."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_PREACTION="Pre-action Backups"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE="Backup Before Extension Update"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE_DESC="Automatically run a full backup before any extension is updated. Uses the default profile. Throttled to once per 10 minutes to prevent duplicate backups during batch updates."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
|
||||
|
||||
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE_DESC="Default maximum age for backup records. Used by the system plugin and CLI cleanup command."
|
||||
@@ -245,7 +266,7 @@ COM_MOKOJOOMBACKUP_CONFIG_WEBCRON="Web Cron"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED="Enable Web Cron"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED_DESC="Allow backups to be triggered via a URL with a secret key. Use this when crontab is not available on shared hosting."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET="Secret Word"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET_DESC="The secret key required in the URL to trigger a backup. Use a long, random string. URL format: index.php?mokojoombackup_cron=YOUR_SECRET&profile_id=1"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET_DESC="The secret key required in the URL to trigger a backup. Use a long, random string. URL format: index.php?mokosuitebackup_cron=YOUR_SECRET&profile_id=1"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP="IP Whitelist"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP_DESC="Comma-separated list of IP addresses allowed to trigger web cron. Leave blank to allow any IP."
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
; MokoSuiteBackup — Component system language file (en-GB)
|
||||
; @package MokoSuiteBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||
|
||||
; ACL Actions
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN="Run Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN_DESC="Allows users in this group to trigger backup operations."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD="Download Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD_DESC="Allows users in this group to download backup archive files. These archives contain the full database and site files."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
|
||||
+21
-6
@@ -1,15 +1,24 @@
|
||||
; MokoJoomBackup — Component language file (en-US)
|
||||
; @package MokoJoomBackup
|
||||
; MokoSuiteBackup — Component language file (en-US)
|
||||
; @package MokoSuiteBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoJoomBackup"
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard"
|
||||
|
||||
; ACL Actions
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN="Run Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN_DESC="Allows users in this group to trigger backup operations."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD="Download Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD_DESC="Allows users in this group to download backup archive files. These archives contain the full database and site files."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
|
||||
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
||||
@@ -27,8 +36,8 @@ COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
|
||||
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
||||
COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found."
|
||||
COM_MOKOJOOMBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key."
|
||||
COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server."
|
||||
COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoSuiteBackup update site not found. Reinstall the package to register the update server."
|
||||
COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_GENERAL="General"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR="Default Backup Directory"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC="Default directory for backup archives, relative to Joomla root. Can be overridden per profile."
|
||||
@@ -36,6 +45,12 @@ COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE="Default Profile"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC="Default backup profile used by quick actions and CLI when no profile is specified."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE="Show Update Site Notice"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC="Display the update site configuration notice on the Backup Records view."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_PREACTION="Pre-action Backups"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE="Backup Before Extension Update"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UPDATE_DESC="Automatically run a full backup before any extension is updated. Uses the default profile. Throttled to once per 10 minutes to prevent duplicate backups during batch updates."
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL="Backup Before Extension Uninstall"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_BACKUP_BEFORE_UNINSTALL_DESC="Automatically run a full backup before any extension is uninstalled. Uses the default profile. Throttled to once per 10 minutes."
|
||||
|
||||
COM_MOKOJOOMBACKUP_CONFIG_CLEANUP="Cleanup Defaults"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE="Max Backup Age (days)"
|
||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE_DESC="Default maximum age for backup records. Used by the system plugin and CLI cleanup command."
|
||||
@@ -0,0 +1,19 @@
|
||||
; MokoSuiteBackup — Component system language file (en-US)
|
||||
; @package MokoSuiteBackup
|
||||
; @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
; @license GPL-3.0-or-later
|
||||
|
||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
||||
|
||||
; ACL Actions
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN="Run Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RUN_DESC="Allows users in this group to trigger backup operations."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD="Download Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_DOWNLOAD_DESC="Allows users in this group to download backup archive files. These archives contain the full database and site files."
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE="Restore Backup"
|
||||
COM_MOKOSUITEBACKUP_ACTION_BACKUP_RESTORE_DESC="Allows users in this group to restore the site from a backup archive. This is a destructive operation that overwrites the current site."
|
||||
+17
-10
@@ -1,14 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>com_mokojoombackup</name>
|
||||
<version>01.08.00</version>
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>01.20.00-rc</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -17,7 +16,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<description>COM_MOKOJOOMBACKUP_DESCRIPTION</description>
|
||||
|
||||
<namespace path="src">Joomla\Component\MokoJoomBackup</namespace>
|
||||
<namespace path="src">Joomla\Component\MokoSuiteBackup</namespace>
|
||||
|
||||
<install>
|
||||
<sql>
|
||||
@@ -40,11 +39,19 @@
|
||||
<administration>
|
||||
<menu img="class:archive">COM_MOKOJOOMBACKUP</menu>
|
||||
<submenu>
|
||||
<menu link="option=com_mokojoombackup&view=dashboard" img="class:home">COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD</menu>
|
||||
<menu link="option=com_mokojoombackup&view=backups" img="class:database">COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS</menu>
|
||||
<menu link="option=com_mokojoombackup&view=profiles" img="class:cog">COM_MOKOJOOMBACKUP_SUBMENU_PROFILES</menu>
|
||||
<menu link="option=com_mokosuitebackup&view=dashboard"
|
||||
img="class:home"
|
||||
alt="Dashboard">COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD</menu>
|
||||
<menu link="option=com_mokosuitebackup&view=backups"
|
||||
img="class:database"
|
||||
alt="Backups">COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS</menu>
|
||||
<menu link="option=com_mokosuitebackup&view=profiles"
|
||||
img="class:cog"
|
||||
alt="Profiles">COM_MOKOJOOMBACKUP_SUBMENU_PROFILES</menu>
|
||||
</submenu>
|
||||
<files folder=".">
|
||||
<filename>access.xml</filename>
|
||||
<filename>config.xml</filename>
|
||||
<folder>cli</folder>
|
||||
<folder>forms</folder>
|
||||
<folder>services</folder>
|
||||
@@ -53,8 +60,8 @@
|
||||
<folder>tmpl</folder>
|
||||
</files>
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/com_mokojoombackup.ini</language>
|
||||
<language tag="en-GB">en-GB/com_mokojoombackup.sys.ini</language>
|
||||
<language tag="en-GB">en-GB/com_mokosuitebackup.ini</language>
|
||||
<language tag="en-GB">en-GB/com_mokosuitebackup.sys.ini</language>
|
||||
</languages>
|
||||
</administration>
|
||||
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -15,20 +15,20 @@ use Joomla\CMS\Extension\ComponentInterface;
|
||||
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
|
||||
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
|
||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Extension\MokoJoomBackupComponent;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Extension\MokoSuiteBackupComponent;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
|
||||
return new class () implements ServiceProviderInterface {
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoJoomBackup'));
|
||||
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoJoomBackup'));
|
||||
$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoSuiteBackup'));
|
||||
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoSuiteBackup'));
|
||||
|
||||
$container->set(
|
||||
ComponentInterface::class,
|
||||
function (Container $container) {
|
||||
$component = new MokoJoomBackupComponent(
|
||||
$component = new MokoSuiteBackupComponent(
|
||||
$container->get(ComponentDispatcherFactoryInterface::class)
|
||||
);
|
||||
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
|
||||
+9
-6
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
||||
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`description` TEXT NOT NULL,
|
||||
@@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` (
|
||||
`archive_format` VARCHAR(10) NOT NULL DEFAULT 'zip',
|
||||
`compression_level` TINYINT(1) UNSIGNED NOT NULL DEFAULT 5 COMMENT '0=none, 9=max',
|
||||
`split_size` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=no split, otherwise MB per part',
|
||||
`backup_dir` VARCHAR(512) NOT NULL DEFAULT 'administrator/components/com_mokojoombackup/backups',
|
||||
`backup_dir` VARCHAR(512) NOT NULL DEFAULT '[DEFAULT_DIR]',
|
||||
`archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders',
|
||||
`exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude',
|
||||
`exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude',
|
||||
@@ -36,6 +36,9 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` (
|
||||
`notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs',
|
||||
`notify_on_success` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`notify_on_failure` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name',
|
||||
`ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL',
|
||||
`ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)',
|
||||
`published` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`ordering` INT(11) NOT NULL DEFAULT 0,
|
||||
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
@@ -44,7 +47,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` (
|
||||
KEY `idx_published` (`published`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `#__mokojoombackup_records` (
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_records` (
|
||||
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1,
|
||||
`description` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
@@ -74,15 +77,15 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_records` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Insert default backup profile (IGNORE prevents duplicate key error on update)
|
||||
INSERT IGNORE INTO `#__mokojoombackup_profiles` (
|
||||
INSERT IGNORE INTO `#__mokosuitebackup_profiles` (
|
||||
`id`, `title`, `description`, `backup_type`,
|
||||
`archive_format`, `compression_level`, `split_size`, `backup_dir`,
|
||||
`exclude_dirs`, `exclude_files`, `exclude_tables`,
|
||||
`published`, `ordering`, `created`, `modified`
|
||||
) VALUES (
|
||||
1, 'Default Backup Profile', 'Full site backup with default settings', 'full',
|
||||
'zip', 5, 0, 'administrator/components/com_mokojoombackup/backups',
|
||||
'administrator/components/com_mokojoombackup/backups\ntmp\ncache\nlogs\nadministrator/logs',
|
||||
'zip', 5, 0, '[DEFAULT_DIR]',
|
||||
'administrator/components/com_mokosuitebackup/backups\ntmp\ncache\nlogs\nadministrator/logs',
|
||||
'.gitignore\n.htaccess.bak',
|
||||
'#__session',
|
||||
1, 1, NOW(), NOW()
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS `#__mokosuitebackup_records`;
|
||||
DROP TABLE IF EXISTS `#__mokosuitebackup_profiles`;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `#__mokosuitebackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive';
|
||||
@@ -0,0 +1,12 @@
|
||||
-- MokoSuiteBackup 01.01.02
|
||||
-- Consolidated schema updates: NULL defaults, notifications, archive name format
|
||||
|
||||
-- Fix: allow NULL defaults for manifest and log columns
|
||||
ALTER TABLE `#__mokosuitebackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL;
|
||||
ALTER TABLE `#__mokosuitebackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
|
||||
|
||||
-- Add user group notifications column to profiles
|
||||
ALTER TABLE `#__mokosuitebackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
|
||||
|
||||
-- Add archive_name_format column with placeholder support
|
||||
ALTER TABLE `#__mokosuitebackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Add ntfy push notification fields to backup profiles
|
||||
ALTER TABLE `#__mokosuitebackup_profiles`
|
||||
ADD COLUMN `ntfy_topic` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy topic name' AFTER `notify_on_failure`,
|
||||
ADD COLUMN `ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL' AFTER `ntfy_topic`,
|
||||
ADD COLUMN `ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional, for private topics)' AFTER `ntfy_server`;
|
||||
+70
-27
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -11,14 +11,14 @@
|
||||
* Handles init and step requests from the admin UI JavaScript.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\SteppedBackupEngine;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\SteppedBackupEngine;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
|
||||
|
||||
class AjaxController extends BaseController
|
||||
{
|
||||
@@ -29,7 +29,13 @@ class AjaxController extends BaseController
|
||||
public function init(): void
|
||||
{
|
||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -50,7 +56,13 @@ class AjaxController extends BaseController
|
||||
public function step(): void
|
||||
{
|
||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -76,17 +88,26 @@ class AjaxController extends BaseController
|
||||
public function browseDir(): void
|
||||
{
|
||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$requestPath = $this->input->getString('path', JPATH_ROOT);
|
||||
$path = realpath($requestPath) ?: $requestPath;
|
||||
|
||||
// Resolve placeholders and relative paths before permission check
|
||||
$resolved = BackupDirectory::resolve($requestPath);
|
||||
$path = realpath($resolved) ?: $resolved;
|
||||
|
||||
// Security: restrict browsing to site root and current user's home
|
||||
$jRoot = realpath(JPATH_ROOT);
|
||||
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: '');
|
||||
$homeDir = BackupDirectory::getHomeDirectory();
|
||||
$allowed = false;
|
||||
|
||||
if ($jRoot !== false && strpos($path, $jRoot) === 0) {
|
||||
@@ -143,7 +164,7 @@ class AjaxController extends BaseController
|
||||
if ($parent !== $path) {
|
||||
if ($jRoot !== false && strpos($parent, $jRoot) === 0) {
|
||||
$parentAllowed = true;
|
||||
} elseif ($homeDir !== '' && strpos($parent, $homeDir) === 0) {
|
||||
} elseif ($homeDir !== '' && (strpos($parent, $homeDir) === 0 || $parent === \dirname($homeDir))) {
|
||||
$parentAllowed = true;
|
||||
}
|
||||
}
|
||||
@@ -169,7 +190,13 @@ class AjaxController extends BaseController
|
||||
public function viewLog(): void
|
||||
{
|
||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -182,16 +209,23 @@ class AjaxController extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
$db = \Joomla\CMS\Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['absolute_path', 'log']))
|
||||
->from($db->quoteName('#__mokojoombackup_records'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $id);
|
||||
$db->setQuery($query);
|
||||
$record = $db->loadObject();
|
||||
try {
|
||||
$db = \Joomla\CMS\Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['absolute_path', 'log']))
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('id') . ' = ' . (int) $id);
|
||||
$db->setQuery($query);
|
||||
$record = $db->loadObject();
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: viewLog() DB error for record ' . $id . ': ' . $e->getMessage());
|
||||
$this->sendJson(['error' => true, 'message' => 'Failed to load backup record'], 500);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$record) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Record not found']);
|
||||
$this->sendJson(['error' => true, 'message' => 'Record not found'], 404);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -199,18 +233,26 @@ class AjaxController extends BaseController
|
||||
// Try to load log from file alongside the archive
|
||||
$logPath = BackupDirectory::logPathFromArchive($record->absolute_path);
|
||||
$logContent = '';
|
||||
$source = 'none';
|
||||
|
||||
if (is_file($logPath)) {
|
||||
$logContent = file_get_contents($logPath);
|
||||
$content = file_get_contents($logPath);
|
||||
|
||||
if ($content !== false) {
|
||||
$logContent = $content;
|
||||
$source = 'file';
|
||||
} else {
|
||||
$source = 'file (read error)';
|
||||
}
|
||||
} elseif (!empty($record->log)) {
|
||||
// Fall back to database-stored log
|
||||
$logContent = $record->log;
|
||||
$source = 'database';
|
||||
}
|
||||
|
||||
$this->sendJson([
|
||||
'error' => false,
|
||||
'log' => $logContent ?: '(no log available)',
|
||||
'source' => is_file($logPath) ? 'file' : 'database',
|
||||
'source' => $source,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -221,13 +263,13 @@ class AjaxController extends BaseController
|
||||
public function checkDir(): void
|
||||
{
|
||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
||||
$this->sendJson(['error' => true, 'message' => 'Invalid token'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokojoombackup')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Access denied']);
|
||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
|
||||
$this->sendJson(['error' => true, 'message' => 'Access denied'], 403);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -269,9 +311,10 @@ class AjaxController extends BaseController
|
||||
/**
|
||||
* Send a JSON response and close the application.
|
||||
*/
|
||||
private function sendJson(array $data): void
|
||||
private function sendJson(array $data, int $status = 200): void
|
||||
{
|
||||
$app = $this->app;
|
||||
$app->setHeader('status', $status);
|
||||
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
$app->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||
$app->sendHeaders();
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+54
-23
@@ -1,21 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\RestoreEngine;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine;
|
||||
|
||||
class BackupsController extends AdminController
|
||||
{
|
||||
@@ -35,6 +36,13 @@ class BackupsController extends AdminController
|
||||
{
|
||||
$this->checkToken();
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$profileId = $this->input->getInt('profile_id', 1);
|
||||
$description = $this->input->getString('description', '');
|
||||
|
||||
@@ -47,7 +55,7 @@ class BackupsController extends AdminController
|
||||
$this->setMessage($result['message'], 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,13 +65,22 @@ class BackupsController extends AdminController
|
||||
*/
|
||||
public function download(): void
|
||||
{
|
||||
$this->checkToken('get');
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) {
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $this->input->getInt('id', 0);
|
||||
$model = $this->getModel('Backup');
|
||||
$item = $model->getItem($id);
|
||||
|
||||
if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +99,7 @@ class BackupsController extends AdminController
|
||||
: 'application/zip';
|
||||
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename));
|
||||
header('Content-Length: ' . $filesize);
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
@@ -101,6 +118,13 @@ class BackupsController extends AdminController
|
||||
{
|
||||
$this->checkToken();
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $this->input->getInt('id', 0);
|
||||
$restoreFiles = (bool) $this->input->getInt('restore_files', 1);
|
||||
$restoreDb = (bool) $this->input->getInt('restore_db', 1);
|
||||
@@ -108,8 +132,8 @@ class BackupsController extends AdminController
|
||||
$password = $this->input->getString('encryption_password', '');
|
||||
|
||||
if (!$id) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -123,7 +147,7 @@ class BackupsController extends AdminController
|
||||
$this->setMessage($result['message'], 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,12 +157,19 @@ class BackupsController extends AdminController
|
||||
{
|
||||
$this->checkToken();
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$cid = $this->input->get('cid', [], 'array');
|
||||
$id = !empty($cid) ? (int) $cid[0] : $this->input->getInt('id', 0);
|
||||
|
||||
if (!$id) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -147,22 +178,22 @@ class BackupsController extends AdminController
|
||||
$item = $model->getItem($id);
|
||||
|
||||
if (!$item || !$item->id) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_file($item->absolute_path)) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($item->checksum)) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM', 'warning');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM'), 'warning');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -170,11 +201,11 @@ class BackupsController extends AdminController
|
||||
$currentHash = hash_file('sha256', $item->absolute_path);
|
||||
|
||||
if ($currentHash === $item->checksum) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_OK');
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_OK'));
|
||||
} else {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_FAILED', 'error');
|
||||
$this->setMessage(Text::_('COM_MOKOJOOMBACKUP_VERIFY_FAILED'), 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false));
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+14
-6
@@ -1,21 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Controller\AdminController;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\AkeebaImporter;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Engine\AkeebaImporter;
|
||||
|
||||
class ProfilesController extends AdminController
|
||||
{
|
||||
@@ -33,6 +34,13 @@ class ProfilesController extends AdminController
|
||||
{
|
||||
$this->checkToken();
|
||||
|
||||
if (!$this->app->getIdentity()->authorise('core.create', 'com_mokosuitebackup')) {
|
||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$importHistory = (bool) $this->input->getInt('import_history', 1);
|
||||
|
||||
$importer = new AkeebaImporter();
|
||||
@@ -40,7 +48,7 @@ class ProfilesController extends AdminController
|
||||
|
||||
if (!$detection['profiles']) {
|
||||
$this->setMessage('COM_MOKOJOOMBACKUP_AKEEBA_NOT_FOUND', 'error');
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=profiles', false));
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -55,7 +63,7 @@ class ProfilesController extends AdminController
|
||||
$this->setMessage($result['message'], 'error');
|
||||
}
|
||||
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=profiles', false));
|
||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false));
|
||||
}
|
||||
|
||||
/**
|
||||
+12
-12
@@ -1,16 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* Imports Akeeba Backup Pro profiles and backup history into MokoJoomBackup.
|
||||
* Imports Akeeba Backup Pro profiles and backup history into MokoSuiteBackup.
|
||||
*
|
||||
* Reads from #__ak_profiles and #__ak_stats, maps Akeeba's configuration
|
||||
* format to MokoJoomBackup's individual column format.
|
||||
* format to MokoSuiteBackup's individual column format.
|
||||
*
|
||||
* Akeeba config format:
|
||||
* INI-style with dot-notation keys, e.g.:
|
||||
@@ -25,12 +25,12 @@
|
||||
* "databases": {"include": {...}, "exclude": {...}}}
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
|
||||
|
||||
class AkeebaImporter
|
||||
{
|
||||
@@ -90,7 +90,7 @@ class AkeebaImporter
|
||||
}
|
||||
|
||||
/**
|
||||
* Import all Akeeba profiles into MokoJoomBackup.
|
||||
* Import all Akeeba profiles into MokoSuiteBackup.
|
||||
*
|
||||
* @param bool $importHistory Also import backup history from #__ak_stats
|
||||
*
|
||||
@@ -120,7 +120,7 @@ class AkeebaImporter
|
||||
$akProfiles = $db->loadObjectList();
|
||||
|
||||
$profilesImported = 0;
|
||||
$profileIdMap = []; // akeeba_id => mokojoombackup_id
|
||||
$profileIdMap = []; // akeeba_id => mokosuitebackup_id
|
||||
|
||||
foreach ($akProfiles as $akProfile) {
|
||||
$config = $this->parseAkeebaConfig($akProfile->configuration ?? '');
|
||||
@@ -128,11 +128,11 @@ class AkeebaImporter
|
||||
|
||||
$mokoProfile = $this->mapToMokoProfile($akProfile, $config, $filters);
|
||||
|
||||
$db->insertObject('#__mokojoombackup_profiles', $mokoProfile, 'id');
|
||||
$db->insertObject('#__mokosuitebackup_profiles', $mokoProfile, 'id');
|
||||
$profileIdMap[$akProfile->id] = $mokoProfile->id;
|
||||
$profilesImported++;
|
||||
|
||||
$this->log('Imported profile: "' . $akProfile->description . '" (Akeeba #' . $akProfile->id . ' → MokoJoomBackup #' . $mokoProfile->id . ')');
|
||||
$this->log('Imported profile: "' . $akProfile->description . '" (Akeeba #' . $akProfile->id . ' → MokoSuiteBackup #' . $mokoProfile->id . ')');
|
||||
}
|
||||
|
||||
// Import backup history
|
||||
@@ -201,7 +201,7 @@ class AkeebaImporter
|
||||
'log' => 'Imported from Akeeba Backup record #' . $stat->id,
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokojoombackup_records', $record, 'id');
|
||||
$db->insertObject('#__mokosuitebackup_records', $record, 'id');
|
||||
$imported++;
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ class AkeebaImporter
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an Akeeba profile to a MokoJoomBackup profile object.
|
||||
* Map an Akeeba profile to a MokoSuiteBackup profile object.
|
||||
*/
|
||||
private function mapToMokoProfile(object $akProfile, array $config, array $filters): object
|
||||
{
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+87
-18
@@ -1,19 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
|
||||
use Joomla\Event\Event;
|
||||
|
||||
class BackupEngine
|
||||
@@ -47,7 +47,7 @@ class BackupEngine
|
||||
// Load profile
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('id') . ' = ' . $profileId);
|
||||
$db->setQuery($query);
|
||||
$profile = $db->loadObject();
|
||||
@@ -105,7 +105,7 @@ class BackupEngine
|
||||
'log' => '',
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokojoombackup_records', $record, 'id');
|
||||
$db->insertObject('#__mokosuitebackup_records', $record, 'id');
|
||||
$recordId = $record->id;
|
||||
|
||||
try {
|
||||
@@ -161,10 +161,20 @@ class BackupEngine
|
||||
foreach ($filesToBackup as $relativePath) {
|
||||
$fullPath = JPATH_ROOT . '/' . $relativePath;
|
||||
|
||||
if (is_file($fullPath) && is_readable($fullPath)) {
|
||||
$archiver->addFile($fullPath, $relativePath);
|
||||
} else {
|
||||
if (!is_file($fullPath) || !is_readable($fullPath)) {
|
||||
$skippedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store configuration.php as .bak with credentials stripped.
|
||||
// The restore process rebuilds a fresh configuration.php
|
||||
// from user input + non-sensitive values from the .bak.
|
||||
if ($relativePath === 'configuration.php') {
|
||||
$sanitized = self::sanitizeConfiguration($fullPath);
|
||||
$archiver->addFromString('configuration.php.bak', $sanitized);
|
||||
$this->log('configuration.php saved as .bak (credentials stripped)');
|
||||
} else {
|
||||
$archiver->addFile($fullPath, $relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,11 +223,16 @@ class BackupEngine
|
||||
MokoRestore::wrap($archivePath, $mokoRestorePath);
|
||||
|
||||
// Replace the original archive with the wrapped one
|
||||
@unlink($archivePath);
|
||||
if (is_file($archivePath) && !unlink($archivePath)) {
|
||||
$this->log('WARNING: Could not remove pre-wrap archive');
|
||||
}
|
||||
rename($mokoRestorePath, $archivePath);
|
||||
$totalSize = filesize($archivePath);
|
||||
$sizeHuman = number_format($totalSize / 1048576, 2) . ' MB';
|
||||
// Recompute checksum for the final wrapped archive
|
||||
$checksum = hash_file('sha256', $archivePath);
|
||||
$this->log('MokoRestore archive created: ' . $sizeHuman);
|
||||
$this->log('SHA-256 (wrapped): ' . $checksum);
|
||||
}
|
||||
|
||||
$remoteFilename = '';
|
||||
@@ -249,7 +264,7 @@ class BackupEngine
|
||||
$logContent = implode("\n", $this->log);
|
||||
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
|
||||
if (@file_put_contents($logPath, $logContent) === false) {
|
||||
error_log('MokoJoomBackup: Could not write log file: ' . $logPath);
|
||||
error_log('MokoSuiteBackup: Could not write log file: ' . $logPath);
|
||||
}
|
||||
|
||||
// Final record update
|
||||
@@ -268,7 +283,7 @@ class BackupEngine
|
||||
'log' => $logContent,
|
||||
];
|
||||
|
||||
$db->updateObject('#__mokojoombackup_records', $update, 'id');
|
||||
$db->updateObject('#__mokosuitebackup_records', $update, 'id');
|
||||
|
||||
// Send success notification
|
||||
NotificationSender::send($profile, $update, true, implode("\n", $this->log));
|
||||
@@ -296,7 +311,7 @@ class BackupEngine
|
||||
'log' => implode("\n", $this->log),
|
||||
];
|
||||
|
||||
$db->updateObject('#__mokojoombackup_records', $update, 'id');
|
||||
$db->updateObject('#__mokosuitebackup_records', $update, 'id');
|
||||
|
||||
// Send failure notification
|
||||
NotificationSender::send($profile, $update, false, implode("\n", $this->log));
|
||||
@@ -416,7 +431,7 @@ class BackupEngine
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('manifest'))
|
||||
->from($db->quoteName('#__mokojoombackup_records'))
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('profile_id') . ' = ' . $profileId)
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
||||
->where($db->quoteName('manifest') . ' != ' . $db->quote(''))
|
||||
@@ -472,14 +487,14 @@ class BackupEngine
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the onMokoJoomBackupAfterRun event so plugins (actionlog, etc.) can react.
|
||||
* Dispatch the onMokoSuiteBackupAfterRun event so plugins (actionlog, etc.) can react.
|
||||
*/
|
||||
private function dispatchAfterRun(bool $success, int $recordId, string $description, int $profileId, string $origin): void
|
||||
{
|
||||
try {
|
||||
$app = Factory::getApplication();
|
||||
|
||||
$event = new Event('onMokoJoomBackupAfterRun', [
|
||||
$event = new Event('onMokoSuiteBackupAfterRun', [
|
||||
'success' => $success,
|
||||
'record_id' => $recordId,
|
||||
'description' => $description,
|
||||
@@ -487,13 +502,67 @@ class BackupEngine
|
||||
'origin' => $origin,
|
||||
]);
|
||||
|
||||
$app->getDispatcher()->dispatch('onMokoJoomBackupAfterRun', $event);
|
||||
$app->getDispatcher()->dispatch('onMokoSuiteBackupAfterRun', $event);
|
||||
} catch (\Throwable $e) {
|
||||
// Never let a listener failure break the backup result, but log it
|
||||
error_log('MokoJoomBackup: onAfterRun listener error: ' . $e->getMessage());
|
||||
error_log('MokoSuiteBackup: onAfterRun listener error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize configuration.php by replacing sensitive field values with
|
||||
* [SANITIZED:fieldname] placeholders. Non-sensitive fields (sitename,
|
||||
* debug, cache, SEF, etc.) are preserved as-is.
|
||||
*
|
||||
* @param string $path Absolute path to configuration.php
|
||||
*
|
||||
* @return string Sanitized file contents
|
||||
*/
|
||||
public static function sanitizeConfiguration(string $path): string
|
||||
{
|
||||
$content = file_get_contents($path);
|
||||
|
||||
if ($content === false) {
|
||||
error_log('MokoSuiteBackup: sanitizeConfiguration() failed to read: ' . $path);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Fields whose values must be replaced with placeholders.
|
||||
// Grouped by category for maintainability.
|
||||
$sensitiveFields = [
|
||||
// Database
|
||||
'host', 'user', 'password', 'db',
|
||||
// Security
|
||||
'secret',
|
||||
// SMTP
|
||||
'smtpuser', 'smtppass', 'smtphost',
|
||||
// Proxy
|
||||
'proxy_user', 'proxy_pass',
|
||||
// Redis
|
||||
'redis_server_auth', 'session_redis_server_auth',
|
||||
// Database TLS
|
||||
'dbsslkey', 'dbsslcert', 'dbsslca',
|
||||
];
|
||||
|
||||
foreach ($sensitiveFields as $field) {
|
||||
// Match: public $field = 'value'; (single-quoted)
|
||||
$content = preg_replace(
|
||||
'/^(\s*public\s+\$' . preg_quote($field, '/') . '\s*=\s*\').*?(\';)/m',
|
||||
'$1[SANITIZED:' . $field . ']$2',
|
||||
$content
|
||||
);
|
||||
// Match: public $field = "value"; (double-quoted)
|
||||
$content = preg_replace(
|
||||
'/^(\s*public\s+\$' . preg_quote($field, '/') . '\s*=\s*").*?("\s*;)/m',
|
||||
'$1[SANITIZED:' . $field . ']$2',
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function log(string $message): void
|
||||
{
|
||||
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
|
||||
+4
-4
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -56,7 +56,7 @@ class DatabaseDumper
|
||||
$prefix = $db->getPrefix();
|
||||
$output = [];
|
||||
|
||||
$output[] = '-- MokoJoomBackup Database Dump';
|
||||
$output[] = '-- MokoSuiteBackup Database Dump';
|
||||
$output[] = '-- Generated: ' . date('Y-m-d H:i:s');
|
||||
$output[] = '-- Server: ' . $db->getServerType();
|
||||
$output[] = '-- Database: ' . $db->getName();
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -12,7 +12,7 @@
|
||||
* and DROP TABLE before CREATE TABLE for clean restores.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -101,7 +101,7 @@ class DatabaseImporter
|
||||
// Log but don't abort — some statements may fail on
|
||||
// different MySQL versions (e.g. charset differences)
|
||||
// but the overall restore should continue.
|
||||
error_log('MokoJoomBackup SQL import warning: ' . $e->getMessage());
|
||||
error_log('MokoSuiteBackup SQL import warning: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class DatabaseImporter
|
||||
$db->execute();
|
||||
$statementsExecuted++;
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoJoomBackup SQL import warning (final): ' . $e->getMessage());
|
||||
error_log('MokoSuiteBackup SQL import warning (final): ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -15,7 +15,7 @@
|
||||
* {"path/to/file": {"size": 1234, "mtime": 1717350000}, ...}
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+5
-4
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -11,7 +11,7 @@
|
||||
* Skips database.sql and sensitive files that should not be overwritten.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -22,10 +22,11 @@ class FileRestorer
|
||||
|
||||
/**
|
||||
* Files that should never be overwritten during restore.
|
||||
* configuration.php is handled separately by the RestoreEngine.
|
||||
* configuration.php is rebuilt from .bak + user input by RestoreEngine.
|
||||
*/
|
||||
private const SKIP_FILES = [
|
||||
'configuration.php',
|
||||
'configuration.php.bak',
|
||||
'.htaccess',
|
||||
'web.config',
|
||||
];
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -12,7 +12,7 @@
|
||||
* No SDK dependency — pure PHP with cURL.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -19,7 +19,7 @@
|
||||
* The RestoreEngine can then restore from the extracted files.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+304
-69
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -21,7 +21,7 @@
|
||||
* with a Joomla-styled wizard interface.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -89,7 +89,7 @@ class MokoRestore
|
||||
*
|
||||
* DELETE THIS FILE AFTER INSTALLATION IS COMPLETE.
|
||||
*
|
||||
* @package MokoJoomBackup
|
||||
* @package MokoSuiteBackup
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GPL-3.0-or-later
|
||||
*/
|
||||
@@ -258,31 +258,34 @@ function actionExtract(array $data): array
|
||||
$count = $zip->numFiles;
|
||||
$zip->close();
|
||||
|
||||
// Try to read existing configuration.php for pre-filling
|
||||
// Pre-fill from configuration.php.bak (sanitized backup) or
|
||||
// configuration.php (legacy/unsanitized backup). Skip [SANITIZED:] values.
|
||||
$existingConfig = [];
|
||||
$configFile = RESTORE_DIR . '/configuration.php';
|
||||
$configFile = RESTORE_DIR . '/configuration.php.bak';
|
||||
|
||||
if (!is_file($configFile)) {
|
||||
$configFile = RESTORE_DIR . '/configuration.php';
|
||||
}
|
||||
|
||||
if (is_file($configFile)) {
|
||||
$content = file_get_contents($configFile);
|
||||
|
||||
if (preg_match('/\$host\s*=\s*\'([^\']*)\'/', $content, $m)) {
|
||||
$existingConfig['db_host'] = $m[1];
|
||||
}
|
||||
$fieldMap = [
|
||||
'host' => 'db_host',
|
||||
'db' => 'db_name',
|
||||
'user' => 'db_user',
|
||||
'dbprefix' => 'db_prefix',
|
||||
'sitename' => 'sitename',
|
||||
'smtphost' => 'smtp_host',
|
||||
'smtpuser' => 'smtp_user',
|
||||
];
|
||||
|
||||
if (preg_match('/\$db\s*=\s*\'([^\']*)\'/', $content, $m)) {
|
||||
$existingConfig['db_name'] = $m[1];
|
||||
}
|
||||
|
||||
if (preg_match('/\$user\s*=\s*\'([^\']*)\'/', $content, $m)) {
|
||||
$existingConfig['db_user'] = $m[1];
|
||||
}
|
||||
|
||||
if (preg_match('/\$dbprefix\s*=\s*\'([^\']*)\'/', $content, $m)) {
|
||||
$existingConfig['db_prefix'] = $m[1];
|
||||
}
|
||||
|
||||
if (preg_match('/\$sitename\s*=\s*\'([^\']*)\'/', $content, $m)) {
|
||||
$existingConfig['sitename'] = $m[1];
|
||||
foreach ($fieldMap as $phpField => $configKey) {
|
||||
if (preg_match('/\$' . preg_quote($phpField, '/') . '\s*=\s*\'([^\']*)\'/', $content, $m)) {
|
||||
if (strpos($m[1], '[SANITIZED:') === false) {
|
||||
$existingConfig[$configKey] = $m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,42 +393,104 @@ function actionConfig(array $data): array
|
||||
$prefix = $data['db_prefix'] ?? 'moko_';
|
||||
$sitename = $data['sitename'] ?? 'Joomla Site';
|
||||
$livesite = $data['live_site'] ?? '';
|
||||
$smtpHost = $data['smtp_host'] ?? '';
|
||||
$smtpUser = $data['smtp_user'] ?? '';
|
||||
$smtpPass = $data['smtp_pass'] ?? '';
|
||||
$tmpPath = RESTORE_DIR . '/tmp';
|
||||
$logPath = RESTORE_DIR . '/administrator/logs';
|
||||
|
||||
$configFile = RESTORE_DIR . '/configuration.php';
|
||||
$configPath = RESTORE_DIR . '/configuration.php';
|
||||
$bakPath = RESTORE_DIR . '/configuration.php.bak';
|
||||
|
||||
if (is_file($configFile)) {
|
||||
// Update existing configuration.php
|
||||
$config = file_get_contents($configFile);
|
||||
// Use .bak as the base template (preserves non-sensitive settings like
|
||||
// debug, cache, SEF, editor, etc.). Fall back to existing config
|
||||
// for legacy/unsanitized backups, or build from scratch if neither exists.
|
||||
$basePath = is_file($bakPath) ? $bakPath : (is_file($configPath) ? $configPath : null);
|
||||
|
||||
if ($basePath !== null) {
|
||||
$config = file_get_contents($basePath);
|
||||
|
||||
// Replace all credential and server-specific fields with user input
|
||||
// Escape all user input for safe interpolation into PHP string literals
|
||||
$eHost = addcslashes($host, "'\\");
|
||||
$eDbName = addcslashes($dbName, "'\\");
|
||||
$eDbUser = addcslashes($dbUser, "'\\");
|
||||
$eDbPass = addcslashes($dbPass, "'\\");
|
||||
$ePrefix = addcslashes($prefix, "'\\");
|
||||
$eSite = addcslashes($sitename, "'\\");
|
||||
$eLive = addcslashes($livesite, "'\\");
|
||||
$eSmtpH = addcslashes($smtpHost, "'\\");
|
||||
$eSmtpU = addcslashes($smtpUser, "'\\");
|
||||
$eSmtpP = addcslashes($smtpPass, "'\\");
|
||||
|
||||
$replacements = [
|
||||
'/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$host}'",
|
||||
'/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$dbName}'",
|
||||
'/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$dbUser}'",
|
||||
'/\$password\s*=\s*\'[^\']*\'/' => "\$password = '" . addcslashes($dbPass, "'\\") . "'",
|
||||
'/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$prefix}'",
|
||||
'/\$host\s*=\s*\'[^\']*\'/' => "\$host = '{$eHost}'",
|
||||
'/\$db\s*=\s*\'[^\']*\'/' => "\$db = '{$eDbName}'",
|
||||
'/\$user\s*=\s*\'[^\']*\'/' => "\$user = '{$eDbUser}'",
|
||||
'/\$password\s*=\s*\'[^\']*\'/' => "\$password = '{$eDbPass}'",
|
||||
'/\$dbprefix\s*=\s*\'[^\']*\'/' => "\$dbprefix = '{$ePrefix}'",
|
||||
'/\$tmp_path\s*=\s*\'[^\']*\'/' => "\$tmp_path = '{$tmpPath}'",
|
||||
'/\$log_path\s*=\s*\'[^\']*\'/' => "\$log_path = '{$logPath}'",
|
||||
'/\$sitename\s*=\s*\'[^\']*\'/' => "\$sitename = '" . addcslashes($sitename, "'\\") . "'",
|
||||
'/\$sitename\s*=\s*\'[^\']*\'/' => "\$sitename = '{$eSite}'",
|
||||
'/\$secret\s*=\s*\'[^\']*\'/' => "\$secret = '" . bin2hex(random_bytes(16)) . "'",
|
||||
];
|
||||
|
||||
if ($livesite !== '') {
|
||||
$replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$livesite}'";
|
||||
$replacements['/\$live_site\s*=\s*\'[^\']*\'/'] = "\$live_site = '{$eLive}'";
|
||||
}
|
||||
|
||||
// SMTP — always replace (clears sanitized placeholders even if blank)
|
||||
$replacements['/\$smtphost\s*=\s*\'[^\']*\'/'] = "\$smtphost = '{$eSmtpH}'";
|
||||
$replacements['/\$smtpuser\s*=\s*\'[^\']*\'/'] = "\$smtpuser = '{$eSmtpU}'";
|
||||
$replacements['/\$smtppass\s*=\s*\'[^\']*\'/'] = "\$smtppass = '{$eSmtpP}'";
|
||||
|
||||
// Clear remaining sanitized placeholders (proxy, Redis, DB TLS)
|
||||
$replacements['/\$proxy_user\s*=\s*\'[^\']*\'/'] = "\$proxy_user = ''";
|
||||
$replacements['/\$proxy_pass\s*=\s*\'[^\']*\'/'] = "\$proxy_pass = ''";
|
||||
$replacements['/\$redis_server_auth\s*=\s*\'[^\']*\'/'] = "\$redis_server_auth = ''";
|
||||
$replacements['/\$session_redis_server_auth\s*=\s*\'[^\']*\'/'] = "\$session_redis_server_auth = ''";
|
||||
$replacements['/\$dbsslkey\s*=\s*\'[^\']*\'/'] = "\$dbsslkey = ''";
|
||||
$replacements['/\$dbsslcert\s*=\s*\'[^\']*\'/'] = "\$dbsslcert = ''";
|
||||
$replacements['/\$dbsslca\s*=\s*\'[^\']*\'/'] = "\$dbsslca = ''";
|
||||
|
||||
foreach ($replacements as $pattern => $replacement) {
|
||||
$config = preg_replace($pattern, $replacement, $config);
|
||||
}
|
||||
|
||||
file_put_contents($configFile, $config);
|
||||
if (file_put_contents($configPath, $config) === false) {
|
||||
return ['success' => false, 'message' => 'Failed to write Joomla config file — check directory permissions'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'configuration.php updated with new settings and fresh secret'];
|
||||
// Remove .bak after successful rebuild
|
||||
if (is_file($bakPath)) {
|
||||
@unlink($bakPath);
|
||||
}
|
||||
|
||||
// Reset .htaccess to Joomla defaults if requested
|
||||
$htWarn = '';
|
||||
|
||||
if (($data['reset_htaccess'] ?? '0') === '1') {
|
||||
$htWarn = writeDefaultHtaccess(RESTORE_DIR);
|
||||
}
|
||||
|
||||
$msg = 'Joomla configuration rebuilt with fresh credentials and secret';
|
||||
|
||||
if ($htWarn !== '') {
|
||||
$msg .= ' (Warning: ' . $htWarn . ')';
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => $msg];
|
||||
}
|
||||
|
||||
// Create new configuration.php from scratch
|
||||
$secret = bin2hex(random_bytes(16));
|
||||
// Create new configuration.php from scratch — use escaped values
|
||||
$eHost = addcslashes($host, "'\\");
|
||||
$eDbName = addcslashes($dbName, "'\\");
|
||||
$eDbUser = addcslashes($dbUser, "'\\");
|
||||
$eDbPass = addcslashes($dbPass, "'\\");
|
||||
$ePrefix = addcslashes($prefix, "'\\");
|
||||
$eSite = addcslashes($sitename, "'\\");
|
||||
$eLive = addcslashes($livesite, "'\\");
|
||||
$secret = bin2hex(random_bytes(16));
|
||||
$newConfig = <<<JCONFIG
|
||||
<?php
|
||||
class JConfig {
|
||||
@@ -433,7 +498,7 @@ class JConfig {
|
||||
public \$offline_message = 'This site is down for maintenance.<br>Please check back again soon.';
|
||||
public \$display_offline_message = 1;
|
||||
public \$offline_image = '';
|
||||
public \$sitename = '{$sitename}';
|
||||
public \$sitename = '{$eSite}';
|
||||
public \$editor = 'tinymce';
|
||||
public \$captcha = '0';
|
||||
public \$list_limit = 20;
|
||||
@@ -442,11 +507,11 @@ class JConfig {
|
||||
public \$debug_lang = false;
|
||||
public \$debug_lang_const = true;
|
||||
public \$dbtype = 'mysqli';
|
||||
public \$host = '{$host}';
|
||||
public \$user = '{$dbUser}';
|
||||
public \$password = '{$dbPass}';
|
||||
public \$db = '{$dbName}';
|
||||
public \$dbprefix = '{$prefix}';
|
||||
public \$host = '{$eHost}';
|
||||
public \$user = '{$eDbUser}';
|
||||
public \$password = '{$eDbPass}';
|
||||
public \$db = '{$eDbName}';
|
||||
public \$dbprefix = '{$ePrefix}';
|
||||
public \$dbencryption = 0;
|
||||
public \$dbsslverifyservercert = false;
|
||||
public \$dbsslkey = '';
|
||||
@@ -454,7 +519,7 @@ class JConfig {
|
||||
public \$dbsslca = '';
|
||||
public \$dbsslcipher = '';
|
||||
public \$force_ssl = 0;
|
||||
public \$live_site = '{$livesite}';
|
||||
public \$live_site = '{$eLive}';
|
||||
public \$secret = '{$secret}';
|
||||
public \$gzip = false;
|
||||
public \$error_reporting = 'default';
|
||||
@@ -468,19 +533,161 @@ class JConfig {
|
||||
}
|
||||
JCONFIG;
|
||||
|
||||
file_put_contents($configFile, $newConfig);
|
||||
if (file_put_contents($configPath, $newConfig) === false) {
|
||||
return ['success' => false, 'message' => 'Failed to write Joomla config file — check directory permissions'];
|
||||
}
|
||||
|
||||
// Ensure directories exist
|
||||
@mkdir($tmpPath, 0755, true);
|
||||
@mkdir($logPath, 0755, true);
|
||||
|
||||
return ['success' => true, 'message' => 'configuration.php created from scratch with fresh secret'];
|
||||
// Reset .htaccess to Joomla defaults if requested
|
||||
$htWarn = '';
|
||||
|
||||
if (($data['reset_htaccess'] ?? '0') === '1') {
|
||||
$htWarn = writeDefaultHtaccess(RESTORE_DIR);
|
||||
}
|
||||
|
||||
$msg = 'Joomla configuration created from scratch with fresh secret';
|
||||
|
||||
if ($htWarn !== '') {
|
||||
$msg .= ' (Warning: ' . $htWarn . ')';
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => $msg];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a clean Joomla default .htaccess file.
|
||||
* Backs up the existing one as .htaccess.bak first.
|
||||
*/
|
||||
function writeDefaultHtaccess(string $siteRoot): string
|
||||
{
|
||||
$htaccess = $siteRoot . '/.htaccess';
|
||||
|
||||
// Backup existing .htaccess before overwriting
|
||||
if (is_file($htaccess)) {
|
||||
if (!copy($htaccess, $htaccess . '.bak')) {
|
||||
return 'Could not back up existing .htaccess — reset skipped for safety';
|
||||
}
|
||||
}
|
||||
|
||||
$default = <<<'HTACCESS'
|
||||
##
|
||||
# @package Joomla
|
||||
# @copyright (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
# @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
##
|
||||
|
||||
##
|
||||
# READ THIS COMPLETELY IF YOU CHOOSE TO USE THIS FILE!
|
||||
#
|
||||
# The line 'Options +FollowSymLinks' may cause problems with some server
|
||||
# configurations. It is required for the use of Apache mod_rewrite, but
|
||||
# it may have already been set by your server administrator in a way that
|
||||
# disallows changing it in this .htaccess file. If using it causes your
|
||||
# server to report an error, comment it out, reload your site in your
|
||||
# browser and test your SEF URLs. If they work, then it has been set by
|
||||
# your server administrator and you do not need to set it here.
|
||||
##
|
||||
|
||||
## No directory listings
|
||||
<IfModule autoindex>
|
||||
IndexIgnore *
|
||||
</IfModule>
|
||||
|
||||
## Suppress mime type detection in browsers for unknown types
|
||||
<IfModule mod_headers.c>
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
</IfModule>
|
||||
|
||||
## Can be commented out if causes errors, see notes above.
|
||||
Options +FollowSymLinks
|
||||
Options -Indexes
|
||||
|
||||
## Disable inline JavaScript when directly opening SVG files or embedding them with the object-tag
|
||||
<FilesMatch "\.svg$">
|
||||
<IfModule mod_headers.c>
|
||||
Header always set Content-Security-Policy "script-src 'none'"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
## Mod_rewrite in use.
|
||||
RewriteEngine On
|
||||
|
||||
## Begin - Rewrite rules to block out some common exploits.
|
||||
# If you experience problems on your site then comment out the operations listed
|
||||
# below by adding a # to the beginning of the line.
|
||||
# This attempts to block the most common type of exploit `attempts` on Joomla!
|
||||
#
|
||||
# Block any script trying to base64_encode data within the URL.
|
||||
RewriteCond %{QUERY_STRING} base64_encode[^(]*\([^)]*\) [OR]
|
||||
# Block any script that includes a <script> tag in URL.
|
||||
RewriteCond %{QUERY_STRING} (<|%3C)([^s]*s)+cript.*(>|%3E) [NC,OR]
|
||||
# Block any script trying to set a PHP GLOBALS variable via URL.
|
||||
RewriteCond %{QUERY_STRING} GLOBALS(=|\[|\%[0-9A-Z]{0,2}) [OR]
|
||||
# Block any script trying to modify a _REQUEST variable via URL.
|
||||
RewriteCond %{QUERY_STRING} _REQUEST(=|\[|\%[0-9A-Z]{0,2})
|
||||
# Return 403 Forbidden header and show the content of the root home page
|
||||
RewriteRule .* index.php [F]
|
||||
#
|
||||
## End - Rewrite rules to block out some common exploits.
|
||||
|
||||
## Begin - Custom redirects
|
||||
#
|
||||
# If you need to redirect some pages, or set a canonical non-www to
|
||||
# www redirect (or vice versa), place that code here. Ensure those
|
||||
# redirects use the correct RewriteRule syntax and the [R=301,L] flags.
|
||||
#
|
||||
## End - Custom redirects
|
||||
|
||||
##
|
||||
# Uncomment the following line if your webserver's URL
|
||||
# is not directly related to physical file paths.
|
||||
# Update Your Joomla! Directory (just / for root).
|
||||
##
|
||||
|
||||
# RewriteBase /
|
||||
|
||||
## Begin - Joomla! core SEF Section.
|
||||
#
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
#
|
||||
# If the requested path and file is not /index.php and the request
|
||||
# has not already been internally rewritten to the index.php script
|
||||
RewriteCond %{REQUEST_URI} !^/index\.php
|
||||
# and the requested path and file matches no existing file
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
# and the requested path and file matches no existing directory
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
# internally rewrite the request to the index.php script
|
||||
RewriteRule .* index.php [L]
|
||||
#
|
||||
## End - Joomla! core SEF Section.
|
||||
HTACCESS;
|
||||
|
||||
if (file_put_contents($htaccess, $default) === false) {
|
||||
return 'Could not write .htaccess — check directory permissions';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function getValidatedPrefix(array $data): string
|
||||
{
|
||||
$prefix = getValidatedPrefix($data);
|
||||
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,20}$/', $prefix)) {
|
||||
throw new RuntimeException('Invalid table prefix format');
|
||||
}
|
||||
|
||||
return $prefix;
|
||||
}
|
||||
|
||||
function actionListAdmins(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = $data['db_prefix'] ?? 'moko_';
|
||||
$prefix = getValidatedPrefix($data);
|
||||
|
||||
// Find super admin users (group 8 = Super Users in Joomla)
|
||||
$stmt = $pdo->prepare(
|
||||
@@ -526,7 +733,7 @@ function actionResetAdmin(array $data): array
|
||||
function actionProvision(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = $data['db_prefix'] ?? 'moko_';
|
||||
$prefix = getValidatedPrefix($data);
|
||||
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
|
||||
$results = [];
|
||||
|
||||
@@ -625,20 +832,12 @@ function actionCleanup(): array
|
||||
function getDbConnection(array $data): PDO
|
||||
{
|
||||
$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'] ?? '';
|
||||
// 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'] ?? '';
|
||||
// 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'] ?? '';
|
||||
// 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');
|
||||
}
|
||||
// Validate db_prefix to prevent SQL injection (used by callers for table names)
|
||||
getValidatedPrefix($data);
|
||||
|
||||
return new PDO(
|
||||
"mysql:host={$host};dbname={$name};charset=utf8mb4",
|
||||
@@ -763,7 +962,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<body>
|
||||
<div class="mr-header">
|
||||
<h1>MokoRestore</h1>
|
||||
<p>Standalone Site Installer — MokoJoomBackup</p>
|
||||
<p>Standalone Site Installer — MokoSuiteBackup</p>
|
||||
</div>
|
||||
|
||||
<div class="mr-container">
|
||||
@@ -837,15 +1036,44 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<!-- Step 4: Site Configuration -->
|
||||
<div class="mr-panel" id="panel4">
|
||||
<h2>Site Configuration</h2>
|
||||
<p class="mr-desc">Update or create configuration.php with the correct settings for this server.</p>
|
||||
<div class="mr-field"><label>Site Name</label><input type="text" id="siteName" value="Joomla Site"></div>
|
||||
<div class="mr-field">
|
||||
<label>Live Site URL <span style="font-weight:normal;color:#94a3b8">(optional)</span></label>
|
||||
<input type="text" id="liveSite" placeholder="https://example.com">
|
||||
<div class="mr-hint">Leave blank to auto-detect. Set this if using a reverse proxy or custom domain.</div>
|
||||
<p class="mr-desc">Configure your site settings. Credentials were removed from the backup for security — enter the correct values for this server.</p>
|
||||
|
||||
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#f8fafc">
|
||||
<div style="font-weight:600;font-size:0.9rem;color:#334155;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
|
||||
<span style="font-size:1.1rem">🌐</span> General
|
||||
</div>
|
||||
<div class="mr-field"><label>Site Name</label><input type="text" id="siteName" value="Joomla Site"></div>
|
||||
<div class="mr-field">
|
||||
<label>Live Site URL <span style="font-weight:normal;color:#94a3b8">(optional)</span></label>
|
||||
<input type="text" id="liveSite" placeholder="https://example.com">
|
||||
<div class="mr-hint">Leave blank to auto-detect. Set this if using a reverse proxy or custom domain.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#f8fafc">
|
||||
<div style="font-weight:600;font-size:0.9rem;color:#334155;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
|
||||
<span style="font-size:1.1rem">✉</span> Mail / SMTP <span style="font-weight:normal;font-size:0.8rem;color:#94a3b8">— leave blank if using PHP mail()</span>
|
||||
</div>
|
||||
<div class="mr-field"><label>SMTP Host</label><input type="text" id="smtpHost" placeholder="smtp.example.com"></div>
|
||||
<div class="mr-row">
|
||||
<div class="mr-field"><label>SMTP User</label><input type="text" id="smtpUser" placeholder="user@example.com"></div>
|
||||
<div class="mr-field"><label>SMTP Password</label><input type="password" id="smtpPass" placeholder=""></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.25rem;background:#f8fafc">
|
||||
<div style="font-weight:600;font-size:0.9rem;color:#334155;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem">
|
||||
<span style="font-size:1.1rem">🛠</span> Server
|
||||
</div>
|
||||
<div class="mr-field" style="display:flex;align-items:center;gap:0.5rem">
|
||||
<input type="checkbox" id="resetHtaccess" style="width:auto">
|
||||
<label for="resetHtaccess" style="margin:0;cursor:pointer">Reset .htaccess to Joomla defaults</label>
|
||||
</div>
|
||||
<div class="mr-hint">Check this if restoring to a different server. The backup's .htaccess may contain server-specific rewrite rules that won't work here.</div>
|
||||
</div>
|
||||
|
||||
<div class="mr-alert mr-alert-info">
|
||||
A new Joomla secret will be generated automatically for security.
|
||||
<span>🔒</span> A new Joomla secret key will be generated automatically. This invalidates active sessions (users will need to log in again) but does not affect passwords or user accounts.
|
||||
</div>
|
||||
<div class="mr-status" id="configStatus"></div>
|
||||
<div class="mr-actions">
|
||||
@@ -927,7 +1155,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
|
||||
<div class="mr-footer">
|
||||
MokoRestore — <a href="https://mokoconsulting.tech" target="_blank">Moko Consulting</a>
|
||||
— Part of MokoJoomBackup
|
||||
— Part of MokoSuiteBackup
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -1057,13 +1285,16 @@ async function runExtract() {
|
||||
setStatus('extractStatus', r.message, 'success');
|
||||
log(r.message);
|
||||
|
||||
// Pre-fill DB config from extracted configuration.php
|
||||
// Pre-fill config from extracted configuration.php
|
||||
// (sanitized fields will be absent — those form fields stay empty)
|
||||
if (r.config) {
|
||||
if (r.config.db_host) document.getElementById('dbHost').value = r.config.db_host;
|
||||
if (r.config.db_name) document.getElementById('dbName').value = r.config.db_name;
|
||||
if (r.config.db_user) document.getElementById('dbUser').value = r.config.db_user;
|
||||
if (r.config.db_prefix) document.getElementById('dbPrefix').value = r.config.db_prefix;
|
||||
if (r.config.sitename) document.getElementById('siteName').value = r.config.sitename;
|
||||
if (r.config.smtp_host) document.getElementById('smtpHost').value = r.config.smtp_host;
|
||||
if (r.config.smtp_user) document.getElementById('smtpUser').value = r.config.smtp_user;
|
||||
}
|
||||
|
||||
if (!r.has_db) {
|
||||
@@ -1130,6 +1361,10 @@ async function runConfig() {
|
||||
const params = Object.assign({}, dbConfig, {
|
||||
sitename: document.getElementById('siteName').value,
|
||||
live_site: document.getElementById('liveSite').value,
|
||||
smtp_host: document.getElementById('smtpHost').value,
|
||||
smtp_user: document.getElementById('smtpUser').value,
|
||||
smtp_pass: document.getElementById('smtpPass').value,
|
||||
reset_htaccess: document.getElementById('resetHtaccess').checked ? '1' : '0',
|
||||
});
|
||||
|
||||
const r = await post('config', params);
|
||||
+99
-8
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -11,7 +11,7 @@
|
||||
* Uses Joomla's built-in mail system (Factory::getMailer()).
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -32,6 +32,14 @@ class NotificationSender
|
||||
* @return bool True if email was sent
|
||||
*/
|
||||
public static function send(object $profile, object $record, bool $success, string $logText = ''): bool
|
||||
{
|
||||
$emailSent = self::sendEmail($profile, $record, $success, $logText);
|
||||
$ntfySent = self::sendNtfy($profile, $record, $success);
|
||||
|
||||
return $emailSent || $ntfySent;
|
||||
}
|
||||
|
||||
private static function sendEmail(object $profile, object $record, bool $success, string $logText = ''): bool
|
||||
{
|
||||
$notifyEmail = trim($profile->notify_email ?? '');
|
||||
$notifyUserGroups = $profile->notify_user_groups ?? '';
|
||||
@@ -73,7 +81,7 @@ class NotificationSender
|
||||
|
||||
// Build subject
|
||||
$statusLabel = $success ? 'SUCCESS' : 'FAILED';
|
||||
$mailer->setSubject("[MokoJoomBackup] {$statusLabel}: {$record->description} — {$siteName}");
|
||||
$mailer->setSubject("[MokoSuiteBackup] {$statusLabel}: {$record->description} — {$siteName}");
|
||||
|
||||
// Build body
|
||||
$duration = '';
|
||||
@@ -92,7 +100,7 @@ class NotificationSender
|
||||
? number_format($record->total_size / 1048576, 2) . ' MB'
|
||||
: 'N/A';
|
||||
|
||||
$body = "MokoJoomBackup Notification\n"
|
||||
$body = "MokoSuiteBackup Notification\n"
|
||||
. "============================\n\n"
|
||||
. "Site: {$siteName}\n"
|
||||
. "URL: {$siteUrl}\n"
|
||||
@@ -125,7 +133,7 @@ class NotificationSender
|
||||
}
|
||||
|
||||
$body .= "\n--\n"
|
||||
. "MokoJoomBackup — https://mokoconsulting.tech\n";
|
||||
. "MokoSuiteBackup — https://mokoconsulting.tech\n";
|
||||
|
||||
$mailer->setBody($body);
|
||||
$mailer->isHtml(false);
|
||||
@@ -133,12 +141,95 @@ class NotificationSender
|
||||
return $mailer->Send();
|
||||
} catch (\Throwable $e) {
|
||||
// Don't let notification failure break the backup flow
|
||||
error_log('MokoJoomBackup notification error: ' . $e->getMessage());
|
||||
error_log('MokoSuiteBackup notification error: ' . $e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a push notification via ntfy.
|
||||
*/
|
||||
private static function sendNtfy(object $profile, object $record, bool $success): bool
|
||||
{
|
||||
$topic = trim($profile->ntfy_topic ?? '');
|
||||
$server = trim($profile->ntfy_server ?? 'https://ntfy.sh');
|
||||
$token = trim($profile->ntfy_token ?? '');
|
||||
|
||||
if ($topic === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Respect the same success/failure preferences as email
|
||||
if ($success && empty($profile->notify_on_success)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$success && empty($profile->notify_on_failure)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$config = Factory::getApplication()->getConfig();
|
||||
$siteName = $config->get('sitename', 'Joomla Site');
|
||||
|
||||
$statusLabel = $success ? 'SUCCESS' : 'FAILED';
|
||||
$statusEmoji = $success ? "\xE2\x9C\x85" : "\xE2\x9D\x8C";
|
||||
|
||||
$sizeHuman = $record->total_size > 0
|
||||
? number_format($record->total_size / 1048576, 2) . ' MB'
|
||||
: 'N/A';
|
||||
|
||||
$title = "{$statusEmoji} Backup {$statusLabel}: {$siteName}";
|
||||
$body = "Profile: {$profile->title}\n"
|
||||
. "Type: {$record->backup_type}\n"
|
||||
. "Archive: {$record->archivename}\n"
|
||||
. "Size: {$sizeHuman}";
|
||||
|
||||
$url = rtrim($server, '/') . '/' . rawurlencode($topic);
|
||||
|
||||
$headers = [
|
||||
'Title: ' . $title,
|
||||
'Priority: ' . ($success ? '3' : '5'),
|
||||
'Tags: ' . ($success ? 'white_check_mark' : 'rotating_light'),
|
||||
];
|
||||
|
||||
if ($token !== '') {
|
||||
$headers[] = 'Authorization: Bearer ' . $token;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error !== '') {
|
||||
error_log('MokoSuiteBackup: ntfy error: ' . $error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($httpCode < 200 || $httpCode >= 300) {
|
||||
error_log('MokoSuiteBackup: ntfy returned HTTP ' . $httpCode . ': ' . $response);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: ntfy notification error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve user group IDs to email addresses of group members.
|
||||
*
|
||||
@@ -172,7 +263,7 @@ class NotificationSender
|
||||
|
||||
return $db->loadColumn() ?: [];
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoJoomBackup: Could not resolve user group emails: ' . $e->getMessage());
|
||||
error_log('MokoSuiteBackup: Could not resolve user group emails: ' . $e->getMessage());
|
||||
|
||||
return [];
|
||||
}
|
||||
+6
-4
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -11,12 +11,12 @@
|
||||
* directory paths and archive filename formats.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
|
||||
|
||||
class PlaceholderResolver
|
||||
{
|
||||
@@ -40,6 +40,7 @@ class PlaceholderResolver
|
||||
'[type]' => 'Backup type (full, database, files, differential)',
|
||||
'[random]' => 'Random 6-character hex string',
|
||||
'[DEFAULT_DIR]' => 'Default backup directory',
|
||||
'[HOME]' => 'Home directory of the PHP process owner',
|
||||
];
|
||||
|
||||
private array $replacements;
|
||||
@@ -77,6 +78,7 @@ class PlaceholderResolver
|
||||
'[type]' => $profile->backup_type ?? 'full',
|
||||
'[random]' => bin2hex(random_bytes(3)),
|
||||
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
|
||||
'[HOME]' => BackupDirectory::getHomeDirectory(),
|
||||
];
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -18,7 +18,7 @@
|
||||
* 6. Clean up staging directory
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -57,7 +57,7 @@ class RestoreEngine
|
||||
// Load backup record
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokojoombackup_records'))
|
||||
->from($db->quoteName('#__mokosuitebackup_records'))
|
||||
->where($db->quoteName('id') . ' = ' . $recordId);
|
||||
$db->setQuery($query);
|
||||
$record = $db->loadObject();
|
||||
@@ -77,7 +77,7 @@ class RestoreEngine
|
||||
}
|
||||
|
||||
// Create staging directory
|
||||
$this->stagingDir = JPATH_ROOT . '/tmp/mokojoombackup-restore-' . $record->tag;
|
||||
$this->stagingDir = JPATH_ROOT . '/tmp/mokosuitebackup-restore-' . $record->tag;
|
||||
|
||||
if (is_dir($this->stagingDir)) {
|
||||
$this->recursiveDelete($this->stagingDir);
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -12,7 +12,7 @@
|
||||
* No SDK dependency — pure PHP with cURL.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+26
-17
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -16,12 +16,12 @@
|
||||
* where ini_set() and set_time_limit() are disabled.
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
|
||||
|
||||
class SteppedBackupEngine
|
||||
{
|
||||
@@ -37,7 +37,7 @@ class SteppedBackupEngine
|
||||
// Load profile
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('id') . ' = ' . $profileId);
|
||||
$db->setQuery($query);
|
||||
$profile = $db->loadObject();
|
||||
@@ -100,7 +100,7 @@ class SteppedBackupEngine
|
||||
'log' => '',
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokojoombackup_records', $record, 'id');
|
||||
$db->insertObject('#__mokosuitebackup_records', $record, 'id');
|
||||
$session->recordId = $record->id;
|
||||
|
||||
// Determine what work needs to be done and estimate steps
|
||||
@@ -228,7 +228,7 @@ class SteppedBackupEngine
|
||||
$flags = $session->tableIndex === 0 ? 0 : FILE_APPEND;
|
||||
|
||||
if ($session->tableIndex === 0) {
|
||||
$header = "-- MokoJoomBackup Database Dump\n"
|
||||
$header = "-- MokoSuiteBackup Database Dump\n"
|
||||
. "-- Generated: " . date('Y-m-d H:i:s') . "\n"
|
||||
. "-- Prefix: " . $db->getPrefix() . "\n\n"
|
||||
. "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n"
|
||||
@@ -277,10 +277,19 @@ class SteppedBackupEngine
|
||||
foreach ($batch as $relativePath) {
|
||||
$fullPath = JPATH_ROOT . '/' . $relativePath;
|
||||
|
||||
if (is_file($fullPath) && is_readable($fullPath)) {
|
||||
$zip->addFile($fullPath, $relativePath);
|
||||
$added++;
|
||||
if (!is_file($fullPath) || !is_readable($fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store config as .bak with credentials stripped — restore rebuilds it
|
||||
if (basename($relativePath) === 'configuration.php' && dirname($relativePath) === '.') {
|
||||
$sanitized = BackupEngine::sanitizeConfiguration($fullPath);
|
||||
$zip->addFromString('configuration.php.bak', $sanitized);
|
||||
} else {
|
||||
$zip->addFile($fullPath, $relativePath);
|
||||
}
|
||||
|
||||
$added++;
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
@@ -315,7 +324,7 @@ class SteppedBackupEngine
|
||||
|
||||
// Clean up temp SQL file
|
||||
if (is_file($sqlFile) && !@unlink($sqlFile)) {
|
||||
error_log('MokoJoomBackup: Could not delete temp SQL file: ' . $sqlFile);
|
||||
error_log('MokoSuiteBackup: Could not delete temp SQL file: ' . $sqlFile);
|
||||
}
|
||||
|
||||
$totalSize = file_exists($session->archivePath) ? filesize($session->archivePath) : 0;
|
||||
@@ -344,7 +353,7 @@ class SteppedBackupEngine
|
||||
'filesexist' => 1,
|
||||
];
|
||||
|
||||
$db->updateObject('#__mokojoombackup_records', $update, 'id');
|
||||
$db->updateObject('#__mokosuitebackup_records', $update, 'id');
|
||||
|
||||
$session->currentStep++;
|
||||
$session->phase = ($session->remoteStorage !== 'none') ? 'upload' : 'complete';
|
||||
@@ -366,7 +375,7 @@ class SteppedBackupEngine
|
||||
// Reload profile for remote settings
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('id') . ' = ' . $session->profileId);
|
||||
$db->setQuery($query);
|
||||
$profile = $db->loadObject();
|
||||
@@ -402,7 +411,7 @@ class SteppedBackupEngine
|
||||
'filesexist' => is_file($session->archivePath) ? 1 : 0,
|
||||
];
|
||||
|
||||
$db->updateObject('#__mokojoombackup_records', $update, 'id');
|
||||
$db->updateObject('#__mokosuitebackup_records', $update, 'id');
|
||||
|
||||
$session->currentStep++;
|
||||
$session->phase = 'complete';
|
||||
@@ -421,7 +430,7 @@ class SteppedBackupEngine
|
||||
// Write log file alongside the archive
|
||||
$logPath = BackupDirectory::logPathFromArchive($session->archivePath);
|
||||
if (@file_put_contents($logPath, $logContent) === false) {
|
||||
error_log('MokoJoomBackup: Could not write log file: ' . $logPath);
|
||||
error_log('MokoSuiteBackup: Could not write log file: ' . $logPath);
|
||||
}
|
||||
|
||||
$update = (object) [
|
||||
@@ -431,7 +440,7 @@ class SteppedBackupEngine
|
||||
'log' => $logContent,
|
||||
];
|
||||
|
||||
$db->updateObject('#__mokojoombackup_records', $update, 'id');
|
||||
$db->updateObject('#__mokosuitebackup_records', $update, 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,7 +456,7 @@ class SteppedBackupEngine
|
||||
'log' => implode("\n", $session->log),
|
||||
];
|
||||
|
||||
$db->updateObject('#__mokojoombackup_records', $update, 'id');
|
||||
$db->updateObject('#__mokosuitebackup_records', $update, 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -17,7 +17,7 @@
|
||||
* Phases: init → database → files → finalize → upload → complete
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -63,7 +63,7 @@ class SteppedSession
|
||||
|
||||
private static function getSessionDir(): string
|
||||
{
|
||||
$dir = JPATH_ROOT . '/tmp/mokojoombackup-sessions';
|
||||
$dir = JPATH_ROOT . '/tmp/mokosuitebackup-sessions';
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
if (!mkdir($dir, 0755, true)) {
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Document\HtmlDocument;
|
||||
use Joomla\CMS\Extension\MVCComponent;
|
||||
use Joomla\CMS\Factory;
|
||||
|
||||
class MokoSuiteBackupComponent extends MVCComponent
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
try {
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if (!$app->isClient('administrator')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$doc = $app->getDocument();
|
||||
|
||||
if (!($doc instanceof HtmlDocument)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wa = $doc->getWebAssetManager();
|
||||
$wa->addInlineStyle(
|
||||
'.main-nav a[href*="com_mokosuitebackup"][href*="view=dashboard"] .sidebar-item-title::before,'
|
||||
. ' .main-nav a[href*="com_mokosuitebackup"][href*="view=backups"] .sidebar-item-title::before,'
|
||||
. ' .main-nav a[href*="com_mokosuitebackup"][href*="view=profiles"] .sidebar-item-title::before'
|
||||
. ' { font-family: "Font Awesome 6 Free"; font-weight: 900; margin-inline-end: .5em; }'
|
||||
. ' .main-nav a[href*="com_mokosuitebackup"][href*="view=dashboard"] .sidebar-item-title::before { content: "\f015"; }'
|
||||
. ' .main-nav a[href*="com_mokosuitebackup"][href*="view=backups"] .sidebar-item-title::before { content: "\f1c0"; }'
|
||||
. ' .main-nav a[href*="com_mokosuitebackup"][href*="view=profiles"] .sidebar-item-title::before { content: "\f013"; }'
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: boot() CSS injection failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Field;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
@@ -11,7 +11,7 @@
|
||||
* Loads the directory tree from the server via AJAX (browseDir endpoint).
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Field;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -163,7 +163,7 @@ class DirectoryFilterField extends FormField
|
||||
const tokenName = Joomla.getOptions('csrf.token') || '';
|
||||
if (tokenName) form.append(tokenName, '1');
|
||||
|
||||
fetch('index.php?option=com_mokojoombackup&format=json', {
|
||||
fetch('index.php?option=com_mokosuitebackup&format=json', {
|
||||
method: 'POST', body: form,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Field;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
+55
-9
@@ -1,21 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Field;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Form\FormField;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
||||
use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
|
||||
|
||||
class FolderPickerField extends FormField
|
||||
{
|
||||
@@ -51,6 +51,7 @@ class FolderPickerField extends FormField
|
||||
|
||||
$placeholders = [
|
||||
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
|
||||
'[HOME]' => BackupDirectory::getHomeDirectory(),
|
||||
'[host]' => $hostname,
|
||||
'[site_name]' => $sanitizedSiteName ?: 'joomla',
|
||||
'[profile_id]' => '1',
|
||||
@@ -90,11 +91,14 @@ class FolderPickerField extends FormField
|
||||
<div class="input-group">
|
||||
<input type="text" name="{$name}" id="{$id}" value="{$value}"
|
||||
class="form-control" maxlength="512"
|
||||
placeholder="[DEFAULT_DIR] or /home/user/backups/[host]" />
|
||||
placeholder="[HOME]/backups or [DEFAULT_DIR]" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="{$id}_btn">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
Browse
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#{$id}_helpModal" title="Available placeholders">
|
||||
<span class="icon-question-circle" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-1" id="{$id}_status">
|
||||
<small class="{$statusClass}">
|
||||
@@ -106,6 +110,44 @@ class FolderPickerField extends FormField
|
||||
<span class="icon-warning-circle" aria-hidden="true"></span>
|
||||
The default backup directory is inside the web root. Backup archives may be directly downloadable if <code>.htaccess</code> is not supported. For better security, use a path outside the web root.
|
||||
</div>
|
||||
<div class="modal fade" id="{$id}_helpModal" tabindex="-1" aria-labelledby="{$id}_helpLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="{$id}_helpLabel">Backup Directory Placeholders</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Use these placeholders in the backup directory path. They are resolved at backup time.</p>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead><tr><th>Placeholder</th><th>Description</th><th>Example</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>[HOME]</code></td><td>Home directory of the server user</td><td><code>{$placeholders['[HOME]']}</code></td></tr>
|
||||
<tr><td><code>[DEFAULT_DIR]</code></td><td>Default backup directory (inside web root)</td><td><code>{$placeholders['[DEFAULT_DIR]']}</code></td></tr>
|
||||
<tr><td><code>[host]</code></td><td>Server hostname</td><td><code>{$placeholders['[host]']}</code></td></tr>
|
||||
<tr><td><code>[site_name]</code></td><td>Joomla site name</td><td><code>{$placeholders['[site_name]']}</code></td></tr>
|
||||
<tr><td><code>[date]</code></td><td>Date (Ymd)</td><td><code>{$placeholders['[date]']}</code></td></tr>
|
||||
<tr><td><code>[year]</code></td><td>Four-digit year</td><td><code>{$placeholders['[year]']}</code></td></tr>
|
||||
<tr><td><code>[month]</code></td><td>Two-digit month</td><td><code>{$placeholders['[month]']}</code></td></tr>
|
||||
<tr><td><code>[day]</code></td><td>Two-digit day</td><td><code>{$placeholders['[day]']}</code></td></tr>
|
||||
<tr><td><code>[profile_id]</code></td><td>Backup profile ID</td><td><code>1</code></td></tr>
|
||||
<tr><td><code>[profile_name]</code></td><td>Profile title</td><td><code>default</code></td></tr>
|
||||
<tr><td><code>[type]</code></td><td>Backup type</td><td><code>full</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h6>Recommended Paths</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><code>[HOME]/backups</code> — Outside web root (recommended)</li>
|
||||
<li><code>[HOME]/backups/[host]</code> — Per-site subdirectory</li>
|
||||
<li><code>[DEFAULT_DIR]</code> — Inside web root (protected by .htaccess)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;">
|
||||
<div class="card-body p-2">
|
||||
<div id="{$id}_tree"></div>
|
||||
@@ -162,8 +204,12 @@ class FolderPickerField extends FormField
|
||||
function setDefaultDirWarning() {
|
||||
var warn = document.getElementById(fieldId + '_defaultwarn');
|
||||
var val = input.value.trim();
|
||||
var isDefault = (!val || val === '[DEFAULT_DIR]' || val === 'administrator/components/com_mokojoombackup/backups');
|
||||
if (warn) warn.style.display = isDefault ? 'block' : 'none';
|
||||
var resolved = resolve(val);
|
||||
var jRoot = placeholders['[DEFAULT_DIR]'].replace(/\/administrator\/components\/com_mokosuitebackup\/backups$/, '');
|
||||
var isInsideWebRoot = resolved && resolved.indexOf(jRoot) === 0;
|
||||
var isOldDefault = val === 'administrator/components/com_mokosuitebackup/backups' || val === 'administrator/components/com_mokojoombackup/backups' || val === '[DEFAULT_DIR]';
|
||||
var showWarning = isOldDefault || isInsideWebRoot;
|
||||
if (warn) warn.style.display = showWarning ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function checkDirPermissions() {
|
||||
@@ -179,7 +225,7 @@ class FolderPickerField extends FormField
|
||||
var tokenName = Joomla.getOptions('csrf.token') || '';
|
||||
if (tokenName) form.append(tokenName, '1');
|
||||
|
||||
fetch('index.php?option=com_mokojoombackup&format=json', {
|
||||
fetch('index.php?option=com_mokosuitebackup&format=json', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
@@ -237,7 +283,7 @@ class FolderPickerField extends FormField
|
||||
var tokenName = Joomla.getOptions('csrf.token') || '';
|
||||
if (tokenName) form.append(tokenName, '1');
|
||||
|
||||
fetch('index.php?option=com_mokojoombackup&format=json', {
|
||||
fetch('index.php?option=com_mokosuitebackup&format=json', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package MokoJoomBackup
|
||||
* @subpackage com_mokojoombackup
|
||||
* @package MokoSuiteBackup
|
||||
* @subpackage com_mokosuitebackup
|
||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Model;
|
||||
namespace Joomla\Component\MokoSuiteBackup\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
@@ -20,7 +20,7 @@ class BackupModel extends AdminModel
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
$form = $this->loadForm(
|
||||
'com_mokojoombackup.backup',
|
||||
'com_mokosuitebackup.backup',
|
||||
'backup',
|
||||
['control' => 'jform', 'load_data' => $loadData]
|
||||
);
|
||||
@@ -30,7 +30,7 @@ class BackupModel extends AdminModel
|
||||
|
||||
protected function loadFormData(): object
|
||||
{
|
||||
$data = Factory::getApplication()->getUserState('com_mokojoombackup.edit.backup.data', []);
|
||||
$data = Factory::getApplication()->getUserState('com_mokosuitebackup.edit.backup.data', []);
|
||||
|
||||
if (empty($data)) {
|
||||
$data = $this->getItem();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user