Compare commits
14 Commits
development
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| af82b46fe0 | |||
| 02d8bfb089 | |||
| 6aebfc1953 | |||
| 8fb3262eb3 | |||
| 03b53d937a | |||
| eb5513f4af | |||
| 003e9617a0 | |||
| 01139c6fd4 | |||
| a6d843fd9b | |||
| b40482d8a5 | |||
| 8ebbfa7aed | |||
| 83b47ce849 | |||
| d383d1fc09 | |||
| 31941e80c3 |
+1
-1
@@ -151,7 +151,7 @@ package-lock.json
|
|||||||
# PHP / Composer tooling
|
# PHP / Composer tooling
|
||||||
# ============================================================
|
# ============================================================
|
||||||
vendor/
|
vendor/
|
||||||
!source/media/vendor/
|
!src/media/vendor/
|
||||||
composer.lock
|
composer.lock
|
||||||
*.phar
|
*.phar
|
||||||
codeception.phar
|
codeception.phar
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
# MokoJoomBackup
|
|
||||||
|
|
||||||
Full-site backup and restore for Joomla — database, files, and configuration. Replaces Akeeba Backup Pro.
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|
||||||
| **Package** | `pkg_mokojoombackup` |
|
|
||||||
| **Language** | PHP 8.1+ |
|
|
||||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
|
||||||
| **Wiki** | [MokoJoomBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/wiki) |
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make build # Build package ZIP
|
|
||||||
make lint # Run linters
|
|
||||||
make validate # Validate structure
|
|
||||||
make release # Full release pipeline
|
|
||||||
make clean # Clean build artifacts
|
|
||||||
composer install # Install PHP dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Joomla **package** with four sub-extensions:
|
|
||||||
|
|
||||||
### com_mokojoombackup (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
|
|
||||||
|
|
||||||
### plg_system_mokojoombackup (System Plugin)
|
|
||||||
- Cleanup of expired backup archives (age + count limits)
|
|
||||||
- Namespace: `Joomla\Plugin\System\MokoJoomBackup`
|
|
||||||
|
|
||||||
### plg_task_mokojoombackup (Task Plugin)
|
|
||||||
- Integrates with Joomla's Scheduled Tasks (com_scheduler)
|
|
||||||
- Registers "Run Backup Profile" task type
|
|
||||||
- Namespace: `Joomla\Plugin\Task\MokoJoomBackup`
|
|
||||||
|
|
||||||
### plg_webservices_mokojoombackup (WebServices Plugin)
|
|
||||||
- REST API for remote backup management (wire-compatible with mcp_mokojoombackup)
|
|
||||||
- Endpoints: backup, backups, profiles, download, delete
|
|
||||||
- Namespace: `Joomla\Plugin\WebServices\MokoJoomBackup`
|
|
||||||
|
|
||||||
### Database Schema
|
|
||||||
|
|
||||||
- `#__mokojoombackup_profiles` — backup profiles (name, description, config JSON, filters JSON)
|
|
||||||
- `#__mokojoombackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps)
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
|
||||||
- **Attribution**: `Authored-by: Moko Consulting`
|
|
||||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
|
||||||
- **Minification**: handled at build time (CI)
|
|
||||||
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
|
||||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
|
||||||
|
|
||||||
## Coding Standards
|
|
||||||
|
|
||||||
- PHP 8.1+ minimum
|
|
||||||
- Joomla 4/5 DI container pattern: `services/provider.php` → Extension class
|
|
||||||
- Legacy stub `.php` file required for plugin loader but empty
|
|
||||||
- `SubscriberInterface` for event subscription (not `on*` method naming)
|
|
||||||
- `bind() → check() → store()` for Table operations (not `save()`)
|
|
||||||
- Language file placement: site (no `folder`) vs admin (`folder="administrator"`)
|
|
||||||
- SPDX license headers on all PHP files
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<display-name>Package - MokoJoomBackup</display-name>
|
<display-name>Package - MokoJoomBackup</display-name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
|
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
|
||||||
<version>01.01.21-dev</version>
|
<version>01.00.00</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
@@ -16,6 +16,6 @@
|
|||||||
<build>
|
<build>
|
||||||
<language>PHP</language>
|
<language>PHP</language>
|
||||||
<package-type>joomla-extension</package-type>
|
<package-type>joomla-extension</package-type>
|
||||||
<entry-point>source/</entry-point>
|
<entry-point>src/</entry-point>
|
||||||
</build>
|
</build>
|
||||||
</moko-platform>
|
</moko-platform>
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
|
||||||
# VERSION: 09.23.00
|
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
|
||||||
|
|
||||||
name: "Universal: Auto Version Bump"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- rc
|
|
||||||
- 'feature/**'
|
|
||||||
- 'patch/**'
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bump:
|
|
||||||
name: Version Bump
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
|
||||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
if [ -d "/opt/moko-platform/cli" ]; then
|
|
||||||
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Bump version
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/version_auto_bump.php \
|
|
||||||
--path . --branch "${GITHUB_REF_NAME}" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
@@ -1,324 +1,285 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, type-prefixed packages |
|
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
|
|
||||||
name: "Universal: Build & Release"
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
action:
|
||||||
description: 'Action to perform'
|
description: 'Action to perform'
|
||||||
required: false
|
required: false
|
||||||
type: choice
|
type: choice
|
||||||
default: release
|
default: release
|
||||||
options:
|
options:
|
||||||
- release
|
- release
|
||||||
- promote-rc
|
- promote-rc
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||||
promote-rc:
|
promote-rc:
|
||||||
name: Promote to RC
|
name: Promote to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
if ! command -v composer &> /dev/null; then
|
||||||
echo Using pre-installed /opt/moko-platform
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
fi
|
||||||
else
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
echo Falling back to fresh clone
|
rm -rf /tmp/moko-platform-api
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
git clone --depth 1 --branch main --quiet \
|
||||||
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
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
fi
|
/tmp/moko-platform-api
|
||||||
rm -rf /tmp/moko-platform-api
|
cd /tmp/moko-platform-api
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
composer install --no-dev --no-interaction --quiet
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
- name: Rename branch to rc
|
||||||
composer install --no-dev --no-interaction --quiet
|
run: |
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||||
fi
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
- name: Rename branch to rc
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
run: |
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
php ${MOKO_CLI}/branch_rename.php \
|
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
- name: Checkout rc and configure git
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
run: |
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
git fetch origin rc
|
||||||
--pr "${{ github.event.pull_request.number }}"
|
git checkout rc
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
- name: Checkout rc and configure git
|
git config --local user.name "gitea-actions[bot]"
|
||||||
run: |
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
git fetch origin rc
|
|
||||||
git checkout rc
|
- name: Publish RC release
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
run: |
|
||||||
git config --local user.name "gitea-actions[bot]"
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
--path . --stability rc --bump minor --branch rc \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
- name: Publish RC release
|
--skip-update-stream
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
- name: Summary
|
||||||
--path . --stability rc --bump minor --branch rc \
|
if: always()
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
run: |
|
||||||
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Summary
|
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||||
if: always()
|
|
||||||
run: |
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
release:
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
if: >-
|
||||||
release:
|
github.event.pull_request.merged == true ||
|
||||||
name: Build & Release Pipeline
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
runs-on: release
|
|
||||||
if: >-
|
steps:
|
||||||
github.event.pull_request.merged == true ||
|
- name: Checkout repository
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
steps:
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
- name: Checkout repository
|
fetch-depth: 0
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
- name: Configure git for bot pushes
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
run: |
|
||||||
fetch-depth: 0
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
- name: Configure git for bot pushes
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
run: |
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
- name: Check for merge conflict markers
|
||||||
git config --local user.name "gitea-actions[bot]"
|
run: |
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
- name: Check for merge conflict markers
|
echo "::error::Merge conflict markers found — aborting release"
|
||||||
run: |
|
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
if [ -n "$CONFLICTS" ]; then
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "::error::Merge conflict markers found — aborting release"
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
exit 1
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
fi
|
||||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
echo "No conflict markers found"
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
- name: Setup moko-platform tools
|
||||||
fi
|
env:
|
||||||
echo "No conflict markers found"
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
- name: Setup moko-platform tools
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
env:
|
run: |
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
# Ensure PHP + Composer are available
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
if ! command -v composer &> /dev/null; then
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
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
|
||||||
run: |
|
fi
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
echo Using pre-installed /opt/moko-platform
|
rm -rf /tmp/moko-platform-api
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
git clone --depth 1 --branch main --quiet \
|
||||||
else
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
echo Falling back to fresh clone
|
/tmp/moko-platform-api
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
cd /tmp/moko-platform-api
|
||||||
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
|
composer install --no-dev --no-interaction --quiet
|
||||||
fi
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
- name: "Publish stable release"
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
run: |
|
||||||
cd /tmp/moko-platform-api
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
composer install --no-dev --no-interaction --quiet
|
--path . --stability stable --bump minor --branch main \
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
fi
|
--skip-update-stream
|
||||||
|
|
||||||
- name: "Publish stable release"
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
run: |
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
if: >-
|
||||||
--path . --stability stable --bump minor --branch main \
|
steps.version.outputs.skip != 'true' &&
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
- name: Update release notes from CHANGELOG.md
|
run: |
|
||||||
run: |
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
# Extract [Unreleased] section from changelog
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
else
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
NOTES="Stable release"
|
--branch main 2>&1 || true
|
||||||
fi
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# Update release body via API
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
python3 -c "
|
continue-on-error: true
|
||||||
import json, urllib.request
|
run: |
|
||||||
body = open('/dev/stdin').read()
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
payload = json.dumps({'body': body}).encode()
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
req = urllib.request.Request(
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
data=payload, method='PATCH',
|
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
headers={
|
git fetch origin main --depth=1
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
'Content-Type': 'application/json'
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
})
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" <<< "$NOTES"
|
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||||
echo "Release notes updated from CHANGELOG.md"
|
if: steps.version.outputs.skip != 'true'
|
||||||
fi
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
if: >-
|
|
||||||
steps.version.outputs.skip != 'true' &&
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
secrets.GH_MIRROR_TOKEN != ''
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
continue-on-error: true
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
run: |
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
# Delete dev branch
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
-H "Content-Type: application/json" \
|
||||||
--branch main 2>&1 || true
|
"${API_BASE}/branches" \
|
||||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: "Step 10: Push main to GitHub mirror"
|
|
||||||
if: >-
|
- name: "Step 12: Create version branch from main"
|
||||||
steps.version.outputs.skip != 'true' &&
|
if: steps.version.outputs.skip != 'true'
|
||||||
secrets.GH_MIRROR_TOKEN != ''
|
continue-on-error: true
|
||||||
continue-on-error: true
|
run: |
|
||||||
run: |
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
BRANCH_NAME="version/${VERSION}"
|
||||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
|
||||||
git fetch origin main --depth=1
|
# Delete old version branch if it exists (same version re-release)
|
||||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||||
&& echo "main branch pushed to GitHub mirror" \
|
|
||||||
|| echo "WARNING: GitHub mirror push failed"
|
# Create version/XX.YY.ZZ from main
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||||
|
- name: "Post-release: Reset dev version"
|
||||||
# Delete rc branch (ephemeral — created by promote-rc)
|
if: steps.version.outputs.skip != 'true'
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
continue-on-error: true
|
||||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
run: |
|
||||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||||
# Delete dev branch
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
--branch dev --path . 2>&1 || true
|
||||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
|
||||||
|
# -- Summary --------------------------------------------------------------
|
||||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
- name: Pipeline Summary
|
||||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
if: always()
|
||||||
-H "Content-Type: application/json" \
|
run: |
|
||||||
"${API_BASE}/branches" \
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: "Step 12: Create version branch from main"
|
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||||
if: steps.version.outputs.skip != 'true'
|
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
continue-on-error: true
|
else
|
||||||
run: |
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||||
BRANCH_NAME="version/${VERSION}"
|
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
MAIN_SHA=$(git rev-parse HEAD)
|
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
# Delete old version branch if it exists (same version re-release)
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
# Create version/XX.YY.ZZ from main
|
fi
|
||||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
|
||||||
|
|
||||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
|
||||||
- name: "Post-release: Reset dev version"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
|
||||||
--branch dev --path . 2>&1 || true
|
|
||||||
|
|
||||||
# -- Summary --------------------------------------------------------------
|
|
||||||
- name: Pipeline Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
|
||||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
|
||||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoPlatform.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
|
||||||
# VERSION: 09.23.00
|
|
||||||
# BRIEF: Delete feature branches after PR merge
|
|
||||||
|
|
||||||
name: "Branch Cleanup"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cleanup:
|
|
||||||
name: Delete merged branch
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true &&
|
|
||||||
github.event.pull_request.head.ref != 'dev' &&
|
|
||||||
github.event.pull_request.head.ref != 'main'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Delete source branch
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
|
||||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
|
||||||
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
|
||||||
|
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
|
||||||
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ "$STATUS" = "204" ]; then
|
|
||||||
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ "$STATUS" = "404" ]; then
|
|
||||||
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
|
|
||||||
fi
|
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
- name: PHP syntax check
|
- name: PHP syntax check
|
||||||
run: |
|
run: |
|
||||||
ERRORS=0
|
ERRORS=0
|
||||||
for DIR in source/ src/ htdocs/; do
|
for DIR in src/ htdocs/; do
|
||||||
if [ -d "$DIR" ]; then
|
if [ -d "$DIR" ]; then
|
||||||
FOUND=1
|
FOUND=1
|
||||||
while IFS= read -r -d '' FILE; do
|
while IFS= read -r -d '' FILE; do
|
||||||
@@ -207,7 +207,7 @@ jobs:
|
|||||||
echo "### Language Directory Check" >> $GITHUB_STEP_SUMMARY
|
echo "### Language Directory Check" >> $GITHUB_STEP_SUMMARY
|
||||||
ERRORS=0
|
ERRORS=0
|
||||||
|
|
||||||
for DIR in source/ src/ htdocs/; do
|
for DIR in src/ htdocs/; do
|
||||||
[ -d "$DIR" ] || continue
|
[ -d "$DIR" ] || continue
|
||||||
# Find all language directories
|
# Find all language directories
|
||||||
while IFS= read -r -d '' LANG_DIR; do
|
while IFS= read -r -d '' LANG_DIR; do
|
||||||
@@ -239,7 +239,7 @@ jobs:
|
|||||||
MISSING=0
|
MISSING=0
|
||||||
CHECKED=0
|
CHECKED=0
|
||||||
|
|
||||||
for DIR in source/ src/ htdocs/; do
|
for DIR in src/ htdocs/; do
|
||||||
if [ -d "$DIR" ]; then
|
if [ -d "$DIR" ]; then
|
||||||
while IFS= read -r -d '' SUBDIR; do
|
while IFS= read -r -d '' SUBDIR; do
|
||||||
CHECKED=$((CHECKED + 1))
|
CHECKED=$((CHECKED + 1))
|
||||||
@@ -252,7 +252,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ "${CHECKED}" -eq 0 ]; then
|
if [ "${CHECKED}" -eq 0 ]; then
|
||||||
echo "No source/, src/, or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
|
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
elif [ "${MISSING}" -gt 0 ]; then
|
elif [ "${MISSING}" -gt 0 ]; then
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
|
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -450,7 +450,7 @@ jobs:
|
|||||||
|
|
||||||
# Determine source directory
|
# Determine source directory
|
||||||
SRC_DIR=""
|
SRC_DIR=""
|
||||||
for DIR in source/ src/ htdocs/ lib/; do
|
for DIR in src/ htdocs/ lib/; do
|
||||||
if [ -d "$DIR" ]; then
|
if [ -d "$DIR" ]; then
|
||||||
SRC_DIR="$DIR"
|
SRC_DIR="$DIR"
|
||||||
break
|
break
|
||||||
@@ -458,7 +458,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$SRC_DIR" ]; then
|
if [ -z "$SRC_DIR" ]; then
|
||||||
echo "No source directory found (source/, src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
|
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: moko-platform.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
name: "Universal: Repository Cleanup"
|
name: "Universal: Repository Cleanup"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Automation
|
|
||||||
# VERSION: 01.01.21
|
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-branch:
|
|
||||||
name: Create feature branch
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Create branch and comment
|
|
||||||
run: |
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
|
||||||
|
|
||||||
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
|
||||||
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
|
||||||
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
|
||||||
|
|
||||||
# Check dev branch exists
|
|
||||||
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
"${API}/branches/dev" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [ "${DEV_EXISTS}" != "200" ]; then
|
|
||||||
echo "No dev branch -- skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create branch from dev
|
|
||||||
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/branches" \
|
|
||||||
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [ "${HTTP}" = "201" ]; then
|
|
||||||
echo "Created branch: ${BRANCH}"
|
|
||||||
|
|
||||||
# Comment on issue with branch link
|
|
||||||
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
|
||||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
|
||||||
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues/${ISSUE_NUM}/comments" \
|
|
||||||
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
|
|
||||||
|
|
||||||
echo "Commented on issue #${ISSUE_NUM}"
|
|
||||||
else
|
|
||||||
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
|
|
||||||
fi
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Notifications
|
# INGROUP: moko-platform.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
name: "Universal: Notifications"
|
name: "Universal: Notifications"
|
||||||
|
|||||||
+508
-509
File diff suppressed because it is too large
Load Diff
@@ -1,243 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
|
||||||
# VERSION: 05.01.00
|
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
pull_request_target:
|
|
||||||
types: [synchronize, opened, reopened]
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Pre-release channel'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- release-candidate
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
|
||||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
|
||||||
|
|
||||||
- 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: |
|
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
|
||||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
|
||||||
STABILITY="release-candidate"
|
|
||||||
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 dev --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
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Security
|
# INGROUP: moko-platform.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
name: "Universal: Security Audit"
|
||||||
|
|||||||
+38
-59
@@ -2,83 +2,62 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
## 01.00.00 — 2026-06-02
|
||||||
- Joomla-styled standalone installer (MokoRestore) with 7-step wizard, admin password reset, and client provisioning
|
|
||||||
- Placeholder support for backup directories and archive filenames ([host], [date], [profile_name], etc.)
|
|
||||||
- Archive Name Format field on backup profiles with customizable filename templates
|
|
||||||
- Interactive directory tree browser for exclude filters (replaces plain text input)
|
|
||||||
- Backup log viewer modal in backup records list and inline in detail view
|
|
||||||
- Clickable dashboard status tiles linking to backup records, detail views, and scheduled tasks
|
|
||||||
- Table exclusion now supports separate Data and Structure checkboxes (backward compatible)
|
|
||||||
- Tar.gz archive format support
|
|
||||||
- User group notifications for backup events
|
|
||||||
- Folder picker field with live server directory browsing
|
|
||||||
- Default directory dashboard warning when backups are stored inside web root
|
|
||||||
- Backup log files written alongside archives (.log)
|
|
||||||
- Backup detail view with checksum, file path, DB size, and embedded log
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Renamed source directory from src/ to source/ per MokoStandards convention
|
|
||||||
- Dashboard health check shows actual resolved backup directory path from profiles
|
|
||||||
- Update site post-install notice links to filtered list view (avoids Joomla core bug)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Download ERR_INVALID_RESPONSE — flush output buffers before sending file headers
|
|
||||||
- Backup directory path resolution for absolute paths outside web root
|
|
||||||
- Schema migrations consolidated to version 01.01.02 (within extension version range)
|
|
||||||
- Console plugin namespace and quickicon translation keys
|
|
||||||
- CLI exit codes and SQL schema defaults
|
|
||||||
- Component Options page (added config.xml)
|
|
||||||
|
|
||||||
## 01.01 — 2026-06-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Admin dashboard view as default landing page with status cards, quick actions, and system health checklist (#28)
|
|
||||||
- Console plugin (plg_console_mokojoombackup) — CLI commands: run, list, profiles, restore, cleanup (#29)
|
|
||||||
- Content plugin (plg_content_mokojoombackup) — auto-backup before extension install/update (#30)
|
|
||||||
- Actionlog plugin (plg_actionlog_mokojoombackup) — logs backup and profile actions to User Action Logs (#31)
|
|
||||||
- BackupEngine dispatches onMokoJoomBackupAfterRun event for plugin listeners
|
|
||||||
- Update site notice on dashboard and post-install
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Renamed Kickstart to MokoRestore throughout
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- SQL update migration and error handling
|
|
||||||
- Removed orphaned scriptfile from component manifest
|
|
||||||
- Consolidated admin files into single files block
|
|
||||||
|
|
||||||
## 01.00 — 2026-06-02
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial package structure with component, system plugin, task plugin, and webservices plugin
|
- Initial package structure with component, system plugin, task plugin, and webservices plugin
|
||||||
- Joomla Scheduled Tasks integration (plg_task_mokojoombackup) — create multiple tasks, each running a different backup profile on its own schedule
|
- Joomla Scheduled Tasks integration (plg_task_mokobackup) — create multiple tasks, each running a different backup profile on its own schedule
|
||||||
- Individual form fields for all profile settings (no raw JSON)
|
- Individual form fields for all profile settings (no raw JSON)
|
||||||
- FTP/FTPS uploader with recursive directory creation, passive mode, SSL, and size verification
|
- FTP/FTPS uploader with recursive directory creation, passive mode, SSL, and size verification
|
||||||
- Google Drive uploader using OAuth2 refresh tokens and resumable upload API
|
- Google Drive uploader using OAuth2 refresh tokens and resumable upload API (5 MB chunks)
|
||||||
- S3-compatible remote storage: AWS S3, Wasabi, Backblaze B2, MinIO (#16)
|
|
||||||
- RemoteUploaderInterface for pluggable storage backends
|
- RemoteUploaderInterface for pluggable storage backends
|
||||||
- Remote upload integrated into BackupEngine with option to delete local copy after upload
|
- Remote upload integrated into BackupEngine as Step 3 after archive creation
|
||||||
|
- Option to delete local copy after successful remote upload (per-profile setting)
|
||||||
- Restore engine with file restoration and database import
|
- Restore engine with file restoration and database import
|
||||||
- MokoRestore standalone restore script — self-contained site restoration without Joomla
|
- Standalone Kickstart restore script (restore.php) — self-contained site restoration without Joomla, like Akeeba Kickstart
|
||||||
- "Include Restore Script" toggle per profile
|
- "Include Restore Script" toggle per profile — wraps backup with restore.php + site-backup.zip
|
||||||
- FileRestorer with protected file handling (preserves configuration.php, .htaccess)
|
- FileRestorer class with protected file handling (preserves configuration.php, .htaccess)
|
||||||
- DatabaseImporter with streaming line-by-line SQL execution and error tolerance
|
- DatabaseImporter with streaming line-by-line SQL execution and error tolerance
|
||||||
- Admin dashboard quickicon widget — backup status at a glance with warnings (#18)
|
- Admin dashboard quickicon widget — backup status at a glance with warnings (#18)
|
||||||
- Differential backups — only back up files changed since last full backup (#19)
|
- Differential backups — only back up files changed since last full backup (#19)
|
||||||
- DifferentialScanner with file manifests stored in backup records
|
- DifferentialScanner: builds file manifests (path/size/mtime) and compares against base
|
||||||
|
- File manifest stored in backup record for future differential comparisons
|
||||||
|
- Automatic full-backup fallback when no base manifest exists
|
||||||
- JPA archive format import for Akeeba Backup migration (#20)
|
- JPA archive format import for Akeeba Backup migration (#20)
|
||||||
|
- JpaUnarchiver: parses Akeeba JPA binary format (headers, gzip, permissions)
|
||||||
|
- RestoreEngine auto-detects JPA vs ZIP format
|
||||||
- AES-256 archive encryption with per-profile password (#17)
|
- AES-256 archive encryption with per-profile password (#17)
|
||||||
- SHA-256 checksum verification for backup integrity (#15)
|
- Encrypted archive support in RestoreEngine (password parameter)
|
||||||
|
- Encrypted archive support in Kickstart restore.php (password field in UI)
|
||||||
|
- SHA-256 checksum computed and stored after archive creation (#15)
|
||||||
|
- "Verify Integrity" toolbar button re-computes hash and compares against stored checksum
|
||||||
|
- S3-compatible remote storage: AWS S3, Wasabi, Backblaze B2, MinIO (#16)
|
||||||
|
- S3 uploader with AWS Signature V4, single PUT for files <= 100 MB, multipart for larger
|
||||||
|
- S3 fields in profile form with showon conditional visibility
|
||||||
|
- Akeeba importer now maps S3 credentials from Akeeba profiles
|
||||||
- Email notifications on backup success/failure via Joomla mailer (#14)
|
- Email notifications on backup success/failure via Joomla mailer (#14)
|
||||||
- Akeeba Backup Pro importer — profiles, filters, remote storage, and backup history
|
- Per-profile notification settings: recipient emails, notify on success/failure
|
||||||
|
- Failure emails include last 30 lines of backup log for debugging
|
||||||
|
- mcp_mokobackup MCP server updated with MokoBackupClient for dual-backend support (#21)
|
||||||
|
- Akeeba Backup Pro importer — imports profiles, filters, remote storage settings, and backup history
|
||||||
- Auto-disables Akeeba plugins and scheduled tasks after successful import
|
- Auto-disables Akeeba plugins and scheduled tasks after successful import
|
||||||
|
- "Import from Akeeba" toolbar button in Profiles view (only shown when Akeeba tables detected)
|
||||||
|
- Supports both INI-format and JSON-format Akeeba configuration parsing
|
||||||
|
- Maps Akeeba filter format (per-root, nested) to newline-separated exclusion fields
|
||||||
|
- Profile selector dropdown in Backup Records view for choosing which profile to run
|
||||||
- AJAX step-based backup engine for shared hosting (overcomes max_execution_time)
|
- AJAX step-based backup engine for shared hosting (overcomes max_execution_time)
|
||||||
|
- SteppedBackupEngine: breaks backup into per-table DB dumps and file batches
|
||||||
|
- SteppedSession: persistent state between AJAX requests via temp JSON files
|
||||||
- Progress bar modal in admin UI with real-time phase/percentage updates
|
- Progress bar modal in admin UI with real-time phase/percentage updates
|
||||||
|
- AjaxController for init/step endpoints with CSRF protection
|
||||||
- Per-profile archive settings: format, compression level, split size, backup directory
|
- Per-profile archive settings: format, compression level, split size, backup directory
|
||||||
- Backup engine with database dumper, file scanner, and ZIP archive builder
|
- Backup engine with step-based execution for large sites
|
||||||
|
- Database dumper with table-level granularity
|
||||||
|
- File scanner with directory exclusion filters
|
||||||
|
- ZIP archive builder
|
||||||
- Backup profiles with independent configurations
|
- Backup profiles with independent configurations
|
||||||
- Backup record management (list, download, delete)
|
- Backup record management (list, download, delete)
|
||||||
|
- Admin dashboard with backup history
|
||||||
- CLI script for cron/scheduled backups
|
- CLI script for cron/scheduled backups
|
||||||
- REST API compatible with MokoJoomBackup MCP server
|
- REST API compatible with MokoBackup MCP server
|
||||||
- System plugin for automatic backup cleanup with configurable retention
|
- System plugin for automatic backup cleanup with configurable retention
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code when working with this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**MokoJoomBackup** -- Full-site backup and restore for Joomla — database, files, and configuration
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Platform** | joomla |
|
||||||
|
| **Language** | PHP |
|
||||||
|
| **Default branch** | main |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Wiki** | [MokoJoomBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/wiki) |
|
||||||
|
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # Build the project
|
||||||
|
make lint # Run linters
|
||||||
|
make validate # Validate structure
|
||||||
|
make release # Full release pipeline
|
||||||
|
make minify # Minify CSS/JS assets
|
||||||
|
make clean # Clean build artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install # Install PHP dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is a Joomla **package** extension (`pkg_mokobackup`) containing three sub-extensions:
|
||||||
|
|
||||||
|
### com_mokobackup (Component)
|
||||||
|
- Admin backend for managing backup profiles and backup records
|
||||||
|
- Backup engine: `Engine/BackupEngine`, `Engine/DatabaseDumper`, `Engine/FileScanner`, `Engine/Archiver`
|
||||||
|
- Joomla 4/5 MVC: Controllers, Models, Views, Tables
|
||||||
|
- Namespace: `Joomla\Component\MokoBackup\Administrator`
|
||||||
|
- Database tables: `#__mokobackup_profiles`, `#__mokobackup_records`
|
||||||
|
- CLI: `cli/mokobackup.php` for cron-based backups
|
||||||
|
|
||||||
|
### plg_system_mokobackup (System Plugin)
|
||||||
|
- Cleanup of expired backup archives (age + count limits)
|
||||||
|
- Namespace: `Joomla\Plugin\System\MokoBackup`
|
||||||
|
|
||||||
|
### plg_task_mokobackup (Task Plugin)
|
||||||
|
- Integrates with Joomla's Scheduled Tasks (com_scheduler)
|
||||||
|
- Registers "Run Backup Profile" task type
|
||||||
|
- Each scheduled task selects a backup profile — create multiple tasks for different schedules
|
||||||
|
- Namespace: `Joomla\Plugin\Task\MokoBackup`
|
||||||
|
|
||||||
|
### plg_webservices_mokobackup (WebServices Plugin)
|
||||||
|
- REST API for remote backup management
|
||||||
|
- Wire-compatible with existing mcp_mokobackup MCP server
|
||||||
|
- Endpoints: backup, backups, profiles, download, delete
|
||||||
|
- Namespace: `Joomla\Plugin\WebServices\MokoBackup`
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
Two tables:
|
||||||
|
- `#__mokobackup_profiles` — backup profiles (name, description, config JSON, filters JSON)
|
||||||
|
- `#__mokobackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps)
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
||||||
|
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
||||||
|
- **Minification**: handled at build time (CI)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
||||||
|
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
- PHP 8.1+ minimum
|
||||||
|
- Joomla 4/5 DI container pattern: `services/provider.php` > Extension class
|
||||||
|
- Legacy stub `.php` file required for plugin loader but empty
|
||||||
|
- `SubscriberInterface` for event subscription (not `on*` method naming)
|
||||||
|
- `bind() > check() > store()` for Table operations (not `save()`)
|
||||||
|
- Language file placement: site (no `folder`) vs admin (`folder="administrator"`)
|
||||||
|
- SPDX license headers on all PHP files
|
||||||
@@ -3,29 +3,43 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# MokoJoomBackup — Full-site backup and restore for Joomla
|
# MokoJoomBackup — Full-site backup and restore for Joomla
|
||||||
#
|
|
||||||
# Builds and releases are handled by CI workflows (pre-release.yml,
|
|
||||||
# auto-release.yml). This Makefile provides local validation helpers
|
|
||||||
# and workflow dispatch shortcuts.
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CONFIGURATION
|
# CONFIGURATION - Customize these for your extension
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
EXTENSION_NAME := mokojoombackup
|
# Extension Configuration
|
||||||
|
EXTENSION_NAME := mokobackup
|
||||||
EXTENSION_TYPE := package
|
EXTENSION_TYPE := package
|
||||||
|
# Options: module, plugin, component, package, template
|
||||||
|
EXTENSION_VERSION := 1.0.0
|
||||||
|
|
||||||
SRC_DIR := source
|
# Module Configuration (for modules only)
|
||||||
|
MODULE_TYPE := site
|
||||||
|
# Options: site, admin
|
||||||
|
|
||||||
# Gitea
|
# Plugin Configuration (for plugins only)
|
||||||
GITEA_URL := https://git.mokoconsulting.tech
|
PLUGIN_GROUP := system
|
||||||
GITEA_ORG := MokoConsulting
|
# Options: system, content, user, authentication, etc.
|
||||||
GITEA_REPO := MokoJoomBackup
|
|
||||||
|
# Directories
|
||||||
|
SRC_DIR := src
|
||||||
|
BUILD_DIR := build
|
||||||
|
DIST_DIR := dist
|
||||||
|
DOCS_DIR := docs
|
||||||
|
|
||||||
|
# Joomla Installation (for local testing - customize paths)
|
||||||
|
JOOMLA_ROOT := /var/www/html/joomla
|
||||||
|
JOOMLA_VERSION := 4
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
PHP := php
|
PHP := php
|
||||||
COMPOSER := composer
|
COMPOSER := composer
|
||||||
|
NPM := npm
|
||||||
PHPCS := vendor/bin/phpcs
|
PHPCS := vendor/bin/phpcs
|
||||||
|
PHPCBF := vendor/bin/phpcbf
|
||||||
|
PHPUNIT := vendor/bin/phpunit
|
||||||
|
ZIP := zip
|
||||||
|
|
||||||
# Coding Standards
|
# Coding Standards
|
||||||
PHPCS_STANDARD := Joomla
|
PHPCS_STANDARD := Joomla
|
||||||
@@ -44,122 +58,146 @@ COLOR_RED := \033[31m
|
|||||||
.PHONY: help
|
.PHONY: help
|
||||||
help: ## Show this help message
|
help: ## Show this help message
|
||||||
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
||||||
@echo "$(COLOR_BLUE)║ MokoJoomBackup Makefile ║$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
|
||||||
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
|
||||||
|
@echo ""
|
||||||
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
# -- Validation ----------------------------------------------------------------
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: ## Run PHP syntax check on all source files
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
|
|
||||||
@ERROR=0; \
|
|
||||||
find $(SRC_DIR) -name "*.php" -exec $(PHP) -l {} \; 2>&1 | grep -v "No syntax errors" || true; \
|
|
||||||
if find $(SRC_DIR) -name "*.php" -exec $(PHP) -l {} \; 2>&1 | grep -q "Parse error"; then \
|
|
||||||
echo "$(COLOR_RED)✗ Syntax errors found$(COLOR_RESET)"; exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: phpcs
|
|
||||||
phpcs: ## Run PHP CodeSniffer (Joomla standards)
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
|
|
||||||
@if [ -f "$(PHPCS)" ]; then \
|
|
||||||
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php $(SRC_DIR); \
|
|
||||||
else \
|
|
||||||
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: composer install$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: validate
|
|
||||||
validate: lint ## Run all local validation checks
|
|
||||||
@echo "$(COLOR_GREEN)✓ Validation passed$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: validate-xml
|
|
||||||
validate-xml: ## Validate all XML manifests are well-formed
|
|
||||||
@echo "$(COLOR_BLUE)Validating XML manifests...$(COLOR_RESET)"
|
|
||||||
@ERROR=0; \
|
|
||||||
for f in $$(find $(SRC_DIR) -name "*.xml"); do \
|
|
||||||
$(PHP) -r "new SimpleXMLElement(file_get_contents('$$f'));" 2>/dev/null \
|
|
||||||
|| { echo "$(COLOR_RED)✗ Invalid XML: $$f$(COLOR_RESET)"; ERROR=1; }; \
|
|
||||||
done; \
|
|
||||||
[ $$ERROR -eq 0 ] && echo "$(COLOR_GREEN)✓ All XML manifests valid$(COLOR_RESET)" || exit 1
|
|
||||||
|
|
||||||
# -- Dependencies --------------------------------------------------------------
|
|
||||||
|
|
||||||
.PHONY: install-deps
|
.PHONY: install-deps
|
||||||
install-deps: ## Install PHP dependencies via Composer
|
install-deps: ## Install all dependencies (Composer + npm)
|
||||||
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
|
||||||
@if [ -f "composer.json" ]; then \
|
@if [ -f "composer.json" ]; then \
|
||||||
$(COMPOSER) install; \
|
$(COMPOSER) install; \
|
||||||
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
|
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
.PHONY: security-check
|
.PHONY: lint
|
||||||
security-check: ## Run security audit on dependencies
|
lint: ## Run PHP linter (syntax check)
|
||||||
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
|
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
|
||||||
@if [ -f "composer.json" ]; then \
|
@find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
|
||||||
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
-exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
|
||||||
|
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
|
||||||
|
|
||||||
|
.PHONY: phpcs
|
||||||
|
phpcs: ## Run PHP CodeSniffer (Joomla standards)
|
||||||
|
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
|
||||||
|
@if [ -f "$(PHPCS)" ]; then \
|
||||||
|
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
|
||||||
|
else \
|
||||||
|
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -- Minify --------------------------------------------------------------------
|
.PHONY: validate
|
||||||
|
validate: lint phpcs ## Run all validation checks
|
||||||
|
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Clean build artifacts
|
||||||
|
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
|
||||||
|
@rm -rf $(BUILD_DIR) $(DIST_DIR)
|
||||||
|
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
|
||||||
|
|
||||||
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
|
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
|
||||||
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
||||||
|
|
||||||
.PHONY: minify
|
.PHONY: minify
|
||||||
minify: ## Minify CSS/JS assets
|
minify: ## Minify CSS/JS assets
|
||||||
@echo "$(COLOR_BLUE)Minifying assets...$(COLOR_RESET)"
|
@echo "Minifying assets..."
|
||||||
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
||||||
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
||||||
elif [ -f "scripts/minify.js" ]; then \
|
elif [ -f "scripts/minify.js" ]; then \
|
||||||
node scripts/minify.js; \
|
node scripts/minify.js; \
|
||||||
else \
|
else \
|
||||||
echo "$(COLOR_YELLOW)⚠ No minify script found$(COLOR_RESET)"; \
|
echo "No minify script found"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -- Release (CI workflow dispatch) --------------------------------------------
|
.PHONY: build
|
||||||
|
build: clean validate minify ## Build extension package
|
||||||
|
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
|
||||||
|
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
|
||||||
|
|
||||||
|
# Determine package prefix based on extension type
|
||||||
|
@case "$(EXTENSION_TYPE)" in \
|
||||||
|
module) \
|
||||||
|
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
|
||||||
|
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
||||||
|
;; \
|
||||||
|
plugin) \
|
||||||
|
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
|
||||||
|
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
||||||
|
;; \
|
||||||
|
component) \
|
||||||
|
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
|
||||||
|
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
||||||
|
;; \
|
||||||
|
package) \
|
||||||
|
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
|
||||||
|
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
||||||
|
;; \
|
||||||
|
template) \
|
||||||
|
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
|
||||||
|
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
||||||
|
;; \
|
||||||
|
*) \
|
||||||
|
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
|
||||||
|
exit 1; \
|
||||||
|
;; \
|
||||||
|
esac; \
|
||||||
|
\
|
||||||
|
mkdir -p "$$BUILD_TARGET"; \
|
||||||
|
\
|
||||||
|
echo "Building $$PACKAGE_PREFIX..."; \
|
||||||
|
\
|
||||||
|
rsync -av --progress \
|
||||||
|
--exclude='$(BUILD_DIR)' \
|
||||||
|
--exclude='$(DIST_DIR)' \
|
||||||
|
--exclude='.git*' \
|
||||||
|
--exclude='vendor/' \
|
||||||
|
--exclude='node_modules/' \
|
||||||
|
--exclude='tests/' \
|
||||||
|
--exclude='Makefile' \
|
||||||
|
--exclude='composer.json' \
|
||||||
|
--exclude='composer.lock' \
|
||||||
|
--exclude='package.json' \
|
||||||
|
--exclude='package-lock.json' \
|
||||||
|
--exclude='phpunit.xml' \
|
||||||
|
--exclude='*.md' \
|
||||||
|
--exclude='.editorconfig' \
|
||||||
|
. "$$BUILD_TARGET/"; \
|
||||||
|
\
|
||||||
|
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
|
||||||
|
\
|
||||||
|
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
|
||||||
|
|
||||||
|
.PHONY: package
|
||||||
|
package: build ## Alias for build
|
||||||
|
@echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release: validate validate-xml ## Trigger pre-release build via CI workflow
|
release: validate build ## Create a release (validate + build)
|
||||||
@echo "$(COLOR_BLUE)Triggering pre-release workflow...$(COLOR_RESET)"
|
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
|
||||||
@if ! command -v curl >/dev/null 2>&1; then \
|
|
||||||
echo "$(COLOR_RED)✗ curl required$(COLOR_RESET)"; exit 1; \
|
|
||||||
fi
|
|
||||||
@if [ -z "$$MOKOGITEA_TOKEN" ]; then \
|
|
||||||
echo "$(COLOR_RED)✗ MOKOGITEA_TOKEN not set$(COLOR_RESET)"; exit 1; \
|
|
||||||
fi
|
|
||||||
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token $$MOKOGITEA_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"$(GITEA_URL)/api/v1/repos/$(GITEA_ORG)/$(GITEA_REPO)/actions/workflows/pre-release.yml/dispatches" \
|
|
||||||
-d "{\"ref\":\"$$BRANCH\",\"inputs\":{\"stability\":\"development\"}}" \
|
|
||||||
&& echo "$(COLOR_GREEN)✓ Pre-release dispatched on $$BRANCH (development channel)$(COLOR_RESET)" \
|
|
||||||
|| { echo "$(COLOR_RED)✗ Dispatch failed$(COLOR_RESET)"; exit 1; }
|
|
||||||
|
|
||||||
.PHONY: release-rc
|
|
||||||
release-rc: validate validate-xml ## Trigger release-candidate build via CI workflow
|
|
||||||
@echo "$(COLOR_BLUE)Triggering RC pre-release workflow...$(COLOR_RESET)"
|
|
||||||
@if [ -z "$$MOKOGITEA_TOKEN" ]; then \
|
|
||||||
echo "$(COLOR_RED)✗ MOKOGITEA_TOKEN not set$(COLOR_RESET)"; exit 1; \
|
|
||||||
fi
|
|
||||||
@BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token $$MOKOGITEA_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"$(GITEA_URL)/api/v1/repos/$(GITEA_ORG)/$(GITEA_REPO)/actions/workflows/pre-release.yml/dispatches" \
|
|
||||||
-d "{\"ref\":\"$$BRANCH\",\"inputs\":{\"stability\":\"release-candidate\"}}" \
|
|
||||||
&& echo "$(COLOR_GREEN)✓ Pre-release dispatched on $$BRANCH (release-candidate channel)$(COLOR_RESET)" \
|
|
||||||
|| { echo "$(COLOR_RED)✗ Dispatch failed$(COLOR_RESET)"; exit 1; }
|
|
||||||
|
|
||||||
# -- Info ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
.PHONY: version
|
.PHONY: version
|
||||||
version: ## Display version from package manifest
|
version: ## Display version information
|
||||||
@VERSION=$$(grep '<version>' $(SRC_DIR)/pkg_mokojoombackup.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/'); \
|
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
|
||||||
echo "$(COLOR_BLUE)$(EXTENSION_NAME)$(COLOR_RESET) v$$VERSION ($(EXTENSION_TYPE))"
|
@echo " Name: $(EXTENSION_NAME)"
|
||||||
|
@echo " Type: $(EXTENSION_TYPE)"
|
||||||
|
@echo " Version: $(EXTENSION_VERSION)"
|
||||||
|
|
||||||
|
.PHONY: security-check
|
||||||
|
security-check: ## Run security checks on dependencies
|
||||||
|
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
|
||||||
|
@if [ -f "composer.json" ]; then \
|
||||||
|
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: install-deps validate build ## Run complete build pipeline
|
||||||
|
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoJoomBackup
|
# MokoJoomBackup
|
||||||
|
|
||||||
<!-- VERSION: 01.01.21 -->
|
<!-- VERSION: 01.00.00 -->
|
||||||
|
|
||||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -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."
|
|
||||||
@@ -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."
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
* @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
|
|
||||||
-->
|
|
||||||
<config>
|
|
||||||
<fieldset name="general" label="COM_MOKOJOOMBACKUP_CONFIG_GENERAL">
|
|
||||||
<field
|
|
||||||
name="default_backup_dir"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<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"
|
|
||||||
default="1"
|
|
||||||
>
|
|
||||||
<option value="1">Default Backup Profile</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="show_update_notice"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_SHOW_UPDATE_NOTICE_DESC"
|
|
||||||
default="1"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="webcron" label="COM_MOKOJOOMBACKUP_CONFIG_WEBCRON">
|
|
||||||
<field
|
|
||||||
name="webcron_secret"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET_DESC"
|
|
||||||
default=""
|
|
||||||
filter="string"
|
|
||||||
maxlength="64"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="webcron_enabled"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED_DESC"
|
|
||||||
default="0"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="webcron_ip_whitelist"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP_DESC"
|
|
||||||
default=""
|
|
||||||
filter="string"
|
|
||||||
hint="Leave blank to allow any IP"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="cleanup" label="COM_MOKOJOOMBACKUP_CONFIG_CLEANUP">
|
|
||||||
<field
|
|
||||||
name="max_age_days"
|
|
||||||
type="number"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_MAX_AGE_DESC"
|
|
||||||
default="30"
|
|
||||||
min="1"
|
|
||||||
max="365"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="max_backups"
|
|
||||||
type="number"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_MAX_BACKUPS"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_MAX_BACKUPS_DESC"
|
|
||||||
default="10"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="notifications" label="COM_MOKOJOOMBACKUP_CONFIG_NOTIFICATIONS">
|
|
||||||
<field
|
|
||||||
name="notify_email"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_EMAIL"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_EMAIL_DESC"
|
|
||||||
default=""
|
|
||||||
filter="string"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="notify_on_success"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_SUCCESS"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_SUCCESS_DESC"
|
|
||||||
default="0"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="notify_on_failure"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_FAILURE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_FAILURE_DESC"
|
|
||||||
default="1"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="permissions" label="JCONFIG_PERMISSIONS_LABEL"
|
|
||||||
description="JCONFIG_PERMISSIONS_DESC">
|
|
||||||
<field
|
|
||||||
name="rules"
|
|
||||||
type="rules"
|
|
||||||
label="JCONFIG_PERMISSIONS_LABEL"
|
|
||||||
filter="rules"
|
|
||||||
validate="rules"
|
|
||||||
component="com_mokojoombackup"
|
|
||||||
section="component"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</config>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<form>
|
|
||||||
<fieldset name="general">
|
|
||||||
<field name="id" type="hidden" />
|
|
||||||
<field name="profile_id" type="hidden" />
|
|
||||||
<field name="description" type="text" label="COM_MOKOJOOMBACKUP_FIELD_DESCRIPTION" readonly="true" />
|
|
||||||
<field name="status" type="text" label="COM_MOKOJOOMBACKUP_FIELD_STATUS" readonly="true" />
|
|
||||||
<field name="origin" type="text" label="COM_MOKOJOOMBACKUP_FIELD_ORIGIN" readonly="true" />
|
|
||||||
<field name="backup_type" type="text" label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE" readonly="true" />
|
|
||||||
<field name="archivename" type="text" label="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE" readonly="true" />
|
|
||||||
<field name="total_size" type="text" label="COM_MOKOJOOMBACKUP_FIELD_SIZE" readonly="true" />
|
|
||||||
<field name="backupstart" type="text" label="COM_MOKOJOOMBACKUP_FIELD_START" readonly="true" />
|
|
||||||
<field name="backupend" type="text" label="COM_MOKOJOOMBACKUP_FIELD_END" readonly="true" />
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<form>
|
|
||||||
<fields name="filter">
|
|
||||||
<field
|
|
||||||
name="search"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FILTER_SEARCH"
|
|
||||||
hint="JSEARCH_FILTER"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="status"
|
|
||||||
type="list"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FILTER_STATUS"
|
|
||||||
onchange="this.form.submit();"
|
|
||||||
>
|
|
||||||
<option value="">COM_MOKOJOOMBACKUP_FILTER_STATUS_ALL</option>
|
|
||||||
<option value="complete">COM_MOKOJOOMBACKUP_STATUS_COMPLETE</option>
|
|
||||||
<option value="running">COM_MOKOJOOMBACKUP_STATUS_RUNNING</option>
|
|
||||||
<option value="fail">COM_MOKOJOOMBACKUP_STATUS_FAIL</option>
|
|
||||||
<option value="pending">COM_MOKOJOOMBACKUP_STATUS_PENDING</option>
|
|
||||||
</field>
|
|
||||||
</fields>
|
|
||||||
|
|
||||||
<fields name="list">
|
|
||||||
<field
|
|
||||||
name="fullordering"
|
|
||||||
type="list"
|
|
||||||
label="JGLOBAL_SORT_BY"
|
|
||||||
default="a.backupstart DESC"
|
|
||||||
onchange="this.form.submit();"
|
|
||||||
>
|
|
||||||
<option value="a.backupstart DESC">COM_MOKOJOOMBACKUP_HEADING_DATE_DESC</option>
|
|
||||||
<option value="a.backupstart ASC">COM_MOKOJOOMBACKUP_HEADING_DATE_ASC</option>
|
|
||||||
<option value="a.total_size DESC">COM_MOKOJOOMBACKUP_HEADING_SIZE_DESC</option>
|
|
||||||
<option value="a.total_size ASC">COM_MOKOJOOMBACKUP_HEADING_SIZE_ASC</option>
|
|
||||||
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
|
|
||||||
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="limit"
|
|
||||||
type="limitbox"
|
|
||||||
label="JGLOBAL_LIST_LIMIT"
|
|
||||||
default="25"
|
|
||||||
onchange="this.form.submit();"
|
|
||||||
/>
|
|
||||||
</fields>
|
|
||||||
</form>
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<form>
|
|
||||||
<fieldset name="general" label="COM_MOKOJOOMBACKUP_FIELDSET_GENERAL">
|
|
||||||
<field
|
|
||||||
name="title"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_TITLE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_TITLE_DESC"
|
|
||||||
required="true"
|
|
||||||
maxlength="255"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="description"
|
|
||||||
type="textarea"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_DESCRIPTION"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_DESCRIPTION_DESC"
|
|
||||||
rows="3"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="backup_type"
|
|
||||||
type="list"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE_DESC"
|
|
||||||
default="full"
|
|
||||||
>
|
|
||||||
<option value="full">COM_MOKOJOOMBACKUP_TYPE_FULL</option>
|
|
||||||
<option value="database">COM_MOKOJOOMBACKUP_TYPE_DATABASE</option>
|
|
||||||
<option value="files">COM_MOKOJOOMBACKUP_TYPE_FILES</option>
|
|
||||||
<option value="differential">COM_MOKOJOOMBACKUP_TYPE_DIFFERENTIAL</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="archive" label="COM_MOKOJOOMBACKUP_FIELDSET_ARCHIVE">
|
|
||||||
<field
|
|
||||||
name="archive_format"
|
|
||||||
type="list"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_FORMAT"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_FORMAT_DESC"
|
|
||||||
default="zip"
|
|
||||||
>
|
|
||||||
<option value="zip">ZIP</option>
|
|
||||||
<option value="tar.gz">tar.gz</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="compression_level"
|
|
||||||
type="list"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_COMPRESSION"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_COMPRESSION_DESC"
|
|
||||||
default="5"
|
|
||||||
>
|
|
||||||
<option value="0">COM_MOKOJOOMBACKUP_COMPRESSION_NONE</option>
|
|
||||||
<option value="1">COM_MOKOJOOMBACKUP_COMPRESSION_FASTEST</option>
|
|
||||||
<option value="5">COM_MOKOJOOMBACKUP_COMPRESSION_NORMAL</option>
|
|
||||||
<option value="9">COM_MOKOJOOMBACKUP_COMPRESSION_BEST</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="split_size"
|
|
||||||
type="number"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE_DESC"
|
|
||||||
default="0"
|
|
||||||
min="0"
|
|
||||||
hint="0 = no splitting"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="backup_dir"
|
|
||||||
type="FolderPicker"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC"
|
|
||||||
default="administrator/components/com_mokojoombackup/backups"
|
|
||||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="archive_name_format"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC"
|
|
||||||
default="[host]_[datetime]_profile[profile_id]"
|
|
||||||
maxlength="512"
|
|
||||||
hint="[host]_[datetime]_profile[profile_id]"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="include_mokorestore"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC"
|
|
||||||
default="0"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="encryption_password"
|
|
||||||
type="password"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD_DESC"
|
|
||||||
maxlength="255"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="sidebar" label="COM_MOKOJOOMBACKUP_FIELDSET_STATUS">
|
|
||||||
<field
|
|
||||||
name="id"
|
|
||||||
type="hidden"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="published"
|
|
||||||
type="list"
|
|
||||||
label="JSTATUS"
|
|
||||||
default="1"
|
|
||||||
>
|
|
||||||
<option value="1">JPUBLISHED</option>
|
|
||||||
<option value="0">JUNPUBLISHED</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="ordering"
|
|
||||||
type="number"
|
|
||||||
label="JFIELD_ORDERING_LABEL"
|
|
||||||
default="0"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="filters" label="COM_MOKOJOOMBACKUP_FIELDSET_FILTERS">
|
|
||||||
<field
|
|
||||||
name="exclude_dirs"
|
|
||||||
type="DirectoryFilter"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DIRS"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DIRS_DESC"
|
|
||||||
filter="raw"
|
|
||||||
hint="tmp"
|
|
||||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="exclude_files"
|
|
||||||
type="ExcludeList"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_FILES"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_FILES_DESC"
|
|
||||||
filter="raw"
|
|
||||||
hint="*.bak"
|
|
||||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="exclude_tables"
|
|
||||||
type="DatabaseTables"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_DESC"
|
|
||||||
filter="raw"
|
|
||||||
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="remote" label="COM_MOKOJOOMBACKUP_FIELDSET_REMOTE">
|
|
||||||
<field
|
|
||||||
name="remote_storage"
|
|
||||||
type="list"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_REMOTE_STORAGE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_REMOTE_STORAGE_DESC"
|
|
||||||
default="none"
|
|
||||||
>
|
|
||||||
<option value="none">COM_MOKOJOOMBACKUP_REMOTE_NONE</option>
|
|
||||||
<option value="ftp">COM_MOKOJOOMBACKUP_REMOTE_FTP</option>
|
|
||||||
<option value="google_drive">COM_MOKOJOOMBACKUP_REMOTE_GDRIVE</option>
|
|
||||||
<option value="s3">COM_MOKOJOOMBACKUP_REMOTE_S3</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="remote_keep_local"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_KEEP_LOCAL"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_KEEP_LOCAL_DESC"
|
|
||||||
default="1"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="notifications" label="COM_MOKOJOOMBACKUP_FIELDSET_NOTIFICATIONS">
|
|
||||||
<field
|
|
||||||
name="notify_email"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_EMAIL"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_EMAIL_DESC"
|
|
||||||
maxlength="512"
|
|
||||||
hint="admin@example.com, backup@example.com"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="notify_user_groups"
|
|
||||||
type="usergrouplist"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS_DESC"
|
|
||||||
multiple="true"
|
|
||||||
layout="joomla.form.field.list-fancy-select"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="notify_on_success"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_SUCCESS_DESC"
|
|
||||||
default="0"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="notify_on_failure"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_NOTIFY_FAILURE_DESC"
|
|
||||||
default="1"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="ftp" label="COM_MOKOJOOMBACKUP_FIELDSET_FTP">
|
|
||||||
<field
|
|
||||||
name="ftp_host"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_FTP_HOST"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_FTP_HOST_DESC"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:ftp"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="ftp_port"
|
|
||||||
type="number"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_FTP_PORT"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_FTP_PORT_DESC"
|
|
||||||
default="21"
|
|
||||||
min="1"
|
|
||||||
max="65535"
|
|
||||||
showon="remote_storage:ftp"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="ftp_username"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_FTP_USERNAME"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:ftp"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="ftp_password"
|
|
||||||
type="password"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_FTP_PASSWORD"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:ftp"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="ftp_path"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_FTP_PATH"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_FTP_PATH_DESC"
|
|
||||||
default="/backups"
|
|
||||||
maxlength="512"
|
|
||||||
showon="remote_storage:ftp"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="ftp_passive"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_FTP_PASSIVE"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_FTP_PASSIVE_DESC"
|
|
||||||
default="1"
|
|
||||||
class="btn-group"
|
|
||||||
showon="remote_storage:ftp"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="ftp_ssl"
|
|
||||||
type="radio"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_FTP_SSL"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_FTP_SSL_DESC"
|
|
||||||
default="0"
|
|
||||||
class="btn-group"
|
|
||||||
showon="remote_storage:ftp"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="google_drive" label="COM_MOKOJOOMBACKUP_FIELDSET_GDRIVE">
|
|
||||||
<field
|
|
||||||
name="gdrive_client_id"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_ID"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_ID_DESC"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:google_drive"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="gdrive_client_secret"
|
|
||||||
type="password"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_SECRET"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:google_drive"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="gdrive_refresh_token"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_GDRIVE_REFRESH_TOKEN"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_GDRIVE_REFRESH_TOKEN_DESC"
|
|
||||||
maxlength="512"
|
|
||||||
showon="remote_storage:google_drive"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="gdrive_folder_id"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_GDRIVE_FOLDER_ID"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_GDRIVE_FOLDER_ID_DESC"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:google_drive"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="s3" label="COM_MOKOJOOMBACKUP_FIELDSET_S3">
|
|
||||||
<field
|
|
||||||
name="s3_endpoint"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT_DESC"
|
|
||||||
maxlength="512"
|
|
||||||
hint="https://s3.amazonaws.com"
|
|
||||||
showon="remote_storage:s3"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="s3_region"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_S3_REGION"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_S3_REGION_DESC"
|
|
||||||
default="us-east-1"
|
|
||||||
maxlength="50"
|
|
||||||
showon="remote_storage:s3"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="s3_access_key"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_S3_ACCESS_KEY"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:s3"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="s3_secret_key"
|
|
||||||
type="password"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_S3_SECRET_KEY"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:s3"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="s3_bucket"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_S3_BUCKET"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_S3_BUCKET_DESC"
|
|
||||||
maxlength="255"
|
|
||||||
showon="remote_storage:s3"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="s3_path"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMBACKUP_FIELD_S3_PATH"
|
|
||||||
description="COM_MOKOJOOMBACKUP_FIELD_S3_PATH_DESC"
|
|
||||||
default="/backups"
|
|
||||||
maxlength="512"
|
|
||||||
showon="remote_storage:s3"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
; MokoJoomBackup — Component 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"
|
|
||||||
|
|
||||||
; Submenu
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
|
||||||
|
|
||||||
; Dashboard view
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE="Storage Used"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
|
||||||
|
|
||||||
; Backups view
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
|
|
||||||
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
|
||||||
COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
|
|
||||||
COM_MOKOJOOMBACKUP_DOWNLOAD="Download"
|
|
||||||
|
|
||||||
; Backup detail view
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUP_DETAIL="Backup Detail"
|
|
||||||
COM_MOKOJOOMBACKUP_VIEW_LOG="Backup Log"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_CHECKSUM="SHA-256 Checksum"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_PATH="File Path"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_DB_SIZE="DB Size"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_REMOTE="Remote Path"
|
|
||||||
|
|
||||||
; Profiles view
|
|
||||||
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Backup Profiles"
|
|
||||||
COM_MOKOJOOMBACKUP_PROFILES_TABLE_CAPTION="Table of backup profiles"
|
|
||||||
COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found."
|
|
||||||
COM_MOKOJOOMBACKUP_PROFILE_NEW="New Profile"
|
|
||||||
COM_MOKOJOOMBACKUP_PROFILE_EDIT="Edit Profile"
|
|
||||||
|
|
||||||
; Table headings
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION="Description"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_PROFILE="Profile"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_STATUS="Status"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_TYPE="Type"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_SIZE="Size"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_DATE="Date"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_ACTIONS="Actions"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_TITLE="Title"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_DATE_DESC="Date descending"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_DATE_ASC="Date ascending"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_SIZE_DESC="Size descending"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_SIZE_ASC="Size ascending"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_TITLE_ASC="Title ascending"
|
|
||||||
COM_MOKOJOOMBACKUP_HEADING_TITLE_DESC="Title descending"
|
|
||||||
|
|
||||||
; General fields
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_TITLE="Title"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_TITLE_DESC="Profile name"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_DESCRIPTION="Description"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_DESCRIPTION_DESC="Brief description of this profile"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE="Backup Type"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE_DESC="What to include in the backup"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_STATUS="Status"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_ORIGIN="Origin"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_SIZE="Total Size"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_START="Start Time"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_END="End Time"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE="Archive Name"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FILES_COUNT="Files Count"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_TABLES_COUNT="Tables Count"
|
|
||||||
|
|
||||||
; Archive settings
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_FORMAT="Archive Format"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_FORMAT_DESC="Format for the backup archive file"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_COMPRESSION="Compression Level"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_COMPRESSION_DESC="Higher compression = smaller file but slower"
|
|
||||||
COM_MOKOJOOMBACKUP_COMPRESSION_NONE="None (fastest)"
|
|
||||||
COM_MOKOJOOMBACKUP_COMPRESSION_FASTEST="Low (fast)"
|
|
||||||
COM_MOKOJOOMBACKUP_COMPRESSION_NORMAL="Normal (balanced)"
|
|
||||||
COM_MOKOJOOMBACKUP_COMPRESSION_BEST="Maximum (smallest)"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD="Encryption Password"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD_DESC="Set a password to encrypt the backup archive with AES-256. Leave blank for no encryption. Required to restore encrypted backups."
|
|
||||||
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_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"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="Include MokoRestore (standalone restore.php) inside the backup archive. Creates a self-contained package that can restore the site on a blank server without Joomla installed."
|
|
||||||
|
|
||||||
; Exclusion filter fields
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DIRS="Exclude Directories"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DIRS_DESC="Browse and check directories to exclude from file backup. You can also type paths manually."
|
|
||||||
COM_MOKOJOOMBACKUP_FILTER_EXCLUDED="Excluded"
|
|
||||||
COM_MOKOJOOMBACKUP_FILTER_INCLUDED="Included"
|
|
||||||
COM_MOKOJOOMBACKUP_FILTER_ADD_MANUAL="Add Path"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_FILES="Exclude Files"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_FILES_DESC="One filename or pattern per line. Supports wildcards (e.g. *.bak, *.tmp)."
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES="Exclude Tables"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_DESC="One table name per line (use #__ prefix). These tables will be skipped during database dump."
|
|
||||||
|
|
||||||
; Remote storage fields
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_REMOTE_STORAGE="Remote Storage"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_REMOTE_STORAGE_DESC="Optionally upload backup archives to a remote location after creation"
|
|
||||||
COM_MOKOJOOMBACKUP_REMOTE_NONE="None (local only)"
|
|
||||||
COM_MOKOJOOMBACKUP_REMOTE_FTP="FTP / FTPS"
|
|
||||||
COM_MOKOJOOMBACKUP_REMOTE_GDRIVE="Google Drive"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_KEEP_LOCAL="Keep Local Copy"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_KEEP_LOCAL_DESC="Keep the local backup file after uploading to remote storage"
|
|
||||||
|
|
||||||
; FTP fields
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_HOST="FTP Host"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_HOST_DESC="FTP server hostname or IP address"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_PORT="FTP Port"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_PORT_DESC="FTP server port (default: 21)"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_USERNAME="FTP Username"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_PASSWORD="FTP Password"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_PATH="Remote Path"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_PATH_DESC="Directory on the FTP server to upload backups to"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_PASSIVE="Passive Mode"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_PASSIVE_DESC="Use passive mode for FTP connections (recommended)"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_SSL="Use FTPS (SSL)"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_FTP_SSL_DESC="Connect using FTPS (FTP over SSL/TLS)"
|
|
||||||
|
|
||||||
; Google Drive fields
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_ID="Google Client ID"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_ID_DESC="OAuth 2.0 Client ID from Google Cloud Console"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_GDRIVE_CLIENT_SECRET="Google Client Secret"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_GDRIVE_REFRESH_TOKEN="Refresh Token"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_GDRIVE_REFRESH_TOKEN_DESC="OAuth 2.0 refresh token for offline access"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_GDRIVE_FOLDER_ID="Drive Folder ID"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_GDRIVE_FOLDER_ID_DESC="Google Drive folder ID where backups will be uploaded. Find this in the folder URL."
|
|
||||||
|
|
||||||
; Backup types
|
|
||||||
COM_MOKOJOOMBACKUP_TYPE_FULL="Full Site (Database + Files)"
|
|
||||||
COM_MOKOJOOMBACKUP_TYPE_DATABASE="Database Only"
|
|
||||||
COM_MOKOJOOMBACKUP_TYPE_FILES="Files Only"
|
|
||||||
COM_MOKOJOOMBACKUP_TYPE_DIFFERENTIAL="Differential (changed files + full DB)"
|
|
||||||
|
|
||||||
; Status labels
|
|
||||||
COM_MOKOJOOMBACKUP_STATUS_COMPLETE="Complete"
|
|
||||||
COM_MOKOJOOMBACKUP_STATUS_RUNNING="Running"
|
|
||||||
COM_MOKOJOOMBACKUP_STATUS_FAIL="Failed"
|
|
||||||
COM_MOKOJOOMBACKUP_STATUS_PENDING="Pending"
|
|
||||||
|
|
||||||
; Filters
|
|
||||||
COM_MOKOJOOMBACKUP_FILTER_SEARCH="Search"
|
|
||||||
COM_MOKOJOOMBACKUP_FILTER_STATUS="Status"
|
|
||||||
COM_MOKOJOOMBACKUP_FILTER_STATUS_ALL="- Select Status -"
|
|
||||||
|
|
||||||
; Tabs and fieldsets
|
|
||||||
COM_MOKOJOOMBACKUP_TAB_GENERAL="General"
|
|
||||||
COM_MOKOJOOMBACKUP_TAB_ARCHIVE="Archive Settings"
|
|
||||||
COM_MOKOJOOMBACKUP_TAB_FILTERS="Exclusion Filters"
|
|
||||||
COM_MOKOJOOMBACKUP_TAB_REMOTE="Remote Storage"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_GENERAL="General"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_ARCHIVE="Archive Settings"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_STATUS="Status"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_FILTERS="Exclusion Filters"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_REMOTE="Remote Storage"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_FTP="FTP Settings"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_GDRIVE="Google Drive Settings"
|
|
||||||
|
|
||||||
; Backup profile selector
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUP_PROFILE="Backup Profile"
|
|
||||||
|
|
||||||
; Restore
|
|
||||||
COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE="Restore"
|
|
||||||
COM_MOKOJOOMBACKUP_RESTORE_CONFIRM="WARNING: Restoring will overwrite your current site files and/or database. Are you sure you want to continue?"
|
|
||||||
|
|
||||||
; Notifications
|
|
||||||
COM_MOKOJOOMBACKUP_TAB_NOTIFICATIONS="Notifications"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_NOTIFICATIONS="Email Notifications"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_EMAIL="Notification Email(s)"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses to notify. Leave empty to disable notifications."
|
|
||||||
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."
|
|
||||||
|
|
||||||
; Integrity verification
|
|
||||||
COM_MOKOJOOMBACKUP_TOOLBAR_VERIFY="Verify Integrity"
|
|
||||||
COM_MOKOJOOMBACKUP_VERIFY_OK="Archive integrity verified — SHA-256 checksum matches."
|
|
||||||
COM_MOKOJOOMBACKUP_VERIFY_FAILED="INTEGRITY CHECK FAILED — archive has been modified or corrupted since backup."
|
|
||||||
COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM="No checksum stored for this backup. Only backups created after this update can be verified."
|
|
||||||
|
|
||||||
; S3 storage
|
|
||||||
COM_MOKOJOOMBACKUP_REMOTE_S3="Amazon S3 / S3-Compatible"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_S3="S3 Storage Settings"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT="S3 Endpoint"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_ENDPOINT_DESC="S3 API endpoint URL. Leave blank for AWS S3. For Wasabi, MinIO, Backblaze B2, enter their endpoint URL."
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_REGION="Region"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_REGION_DESC="AWS region (e.g. us-east-1, eu-west-1). Required for AWS Signature V4."
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_ACCESS_KEY="Access Key"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_SECRET_KEY="Secret Key"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_BUCKET="Bucket Name"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_BUCKET_DESC="S3 bucket name where backups will be stored."
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_PATH="Path Prefix"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_S3_PATH_DESC="Optional path prefix inside the bucket (e.g. /backups or /sites/mysite)."
|
|
||||||
|
|
||||||
; Akeeba Import
|
|
||||||
COM_MOKOJOOMBACKUP_TOOLBAR_IMPORT_AKEEBA="Import from Akeeba"
|
|
||||||
COM_MOKOJOOMBACKUP_AKEEBA_NOT_FOUND="Akeeba Backup tables not found. Is Akeeba Backup Pro installed?"
|
|
||||||
|
|
||||||
; 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."
|
|
||||||
|
|
||||||
; Component Options (config.xml)
|
|
||||||
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."
|
|
||||||
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_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."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_BACKUPS="Max Backup Count"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_BACKUPS_DESC="Default maximum number of completed backups to retain."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFICATIONS="Notifications"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_EMAIL="Global Notification Email(s)"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses for global backup notifications. Per-profile settings override this."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_SUCCESS="Notify on Success"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_SUCCESS_DESC="Send email when any backup completes successfully (unless overridden by profile)."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_FAILURE="Notify on Failure"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_FAILURE_DESC="Send email when any backup fails (unless overridden by profile)."
|
|
||||||
|
|
||||||
; Web Cron
|
|
||||||
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_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."
|
|
||||||
|
|
||||||
; Folder picker
|
|
||||||
COM_MOKOJOOMBACKUP_FOLDER_EXISTS="Directory exists"
|
|
||||||
COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found"
|
|
||||||
COM_MOKOJOOMBACKUP_FOLDER_PLACEHOLDER="Uses placeholders (resolved at backup time)"
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)"
|
|
||||||
|
|
||||||
; Exclude fields
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_HELP="Check tables to exclude from database backup. Use Data to skip row data (keeps structure), Structure to skip CREATE TABLE, or both to fully exclude."
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DATA="Data"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_STRUCTURE="Structure"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_TABLE_NAME="Table Name"
|
|
||||||
|
|
||||||
; User group notifications
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS="Notify User Groups"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS_DESC="Select Joomla user groups whose members will receive backup notifications. Combined with email addresses above."
|
|
||||||
|
|
||||||
; Dashboard warnings
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security."
|
|
||||||
|
|
||||||
; Errors
|
|
||||||
COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND="Backup archive file not found or has been deleted."
|
|
||||||
COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED="No backup record selected for restore."
|
|
||||||
@@ -1,10 +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_BACKUPS="Backup Records"
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
; MokoJoomBackup — Component 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"
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_NO_SCHEDULED="No tasks scheduled"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_TOTAL_BACKUPS="Total Backups"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE="Storage Used"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_FAILURES_7D="%d failures (7 days)"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS="Quick Actions"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS="Scheduled Tasks"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE="Update Site"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH="System Health"
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
|
||||||
COM_MOKOJOOMBACKUP_PROFILES_TITLE="Backup Profiles"
|
|
||||||
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_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."
|
|
||||||
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_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."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_BACKUPS="Max Backup Count"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_MAX_BACKUPS_DESC="Default maximum number of completed backups to retain."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFICATIONS="Notifications"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_EMAIL="Global Notification Email(s)"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_EMAIL_DESC="Comma-separated list of email addresses for global backup notifications. Per-profile settings override this."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_SUCCESS="Notify on Success"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_SUCCESS_DESC="Send email when any backup completes successfully (unless overridden by profile)."
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_FAILURE="Notify on Failure"
|
|
||||||
COM_MOKOJOOMBACKUP_CONFIG_NOTIFY_FAILURE_DESC="Send email when any backup fails (unless overridden by profile)."
|
|
||||||
COM_MOKOJOOMBACKUP_FOLDER_EXISTS="Directory exists"
|
|
||||||
COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found"
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE="Backup directory is inside the web root"
|
|
||||||
COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING="One or more profiles store backups in the default directory inside the web root. This may expose backup archives if .htaccess is not supported. Move backups to a directory outside the web root for better security."
|
|
||||||
COM_MOKOJOOMBACKUP_FOLDER_EXISTS="Directory exists"
|
|
||||||
COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND="Directory not found"
|
|
||||||
COM_MOKOJOOMBACKUP_BACKUP_DIR_DEFAULT="Default (inside web root)"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_HELP="Check tables to exclude from database backup. Use Data to skip row data (keeps structure), Structure to skip CREATE TABLE, or both to fully exclude."
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DATA="Data"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_STRUCTURE="Structure"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_TABLE_NAME="Table Name"
|
|
||||||
COM_MOKOJOOMBACKUP_VIEW_LOG="Backup Log"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_CHECKSUM="SHA-256 Checksum"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_PATH="File Path"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_DB_SIZE="DB Size"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_REMOTE="Remote Path"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS="Notify User Groups"
|
|
||||||
COM_MOKOJOOMBACKUP_FIELD_NOTIFY_USER_GROUPS_DESC="Select Joomla user groups whose members will receive backup notifications. Combined with email addresses above."
|
|
||||||
@@ -1,10 +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_BACKUPS="Backup Records"
|
|
||||||
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
* @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
|
|
||||||
-->
|
|
||||||
<extension type="component" method="upgrade">
|
|
||||||
<name>com_mokojoombackup</name>
|
|
||||||
<version>01.01.21-dev</version>
|
|
||||||
<creationDate>2026-06-02</creationDate>
|
|
||||||
<author>Moko Consulting</author>
|
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
|
||||||
<license>GPL-3.0-or-later</license>
|
|
||||||
<description>COM_MOKOJOOMBACKUP_DESCRIPTION</description>
|
|
||||||
|
|
||||||
<namespace path="src">Joomla\Component\MokoJoomBackup</namespace>
|
|
||||||
|
|
||||||
<install>
|
|
||||||
<sql>
|
|
||||||
<file driver="mysql" charset="utf8">sql/install.mysql.sql</file>
|
|
||||||
</sql>
|
|
||||||
</install>
|
|
||||||
|
|
||||||
<uninstall>
|
|
||||||
<sql>
|
|
||||||
<file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file>
|
|
||||||
</sql>
|
|
||||||
</uninstall>
|
|
||||||
|
|
||||||
<update>
|
|
||||||
<schemas>
|
|
||||||
<schemapath type="mysql">sql/updates/mysql</schemapath>
|
|
||||||
</schemas>
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<administration>
|
|
||||||
<menu img="class:archive">COM_MOKOJOOMBACKUP</menu>
|
|
||||||
<submenu>
|
|
||||||
<menu link="option=com_mokojoombackup&view=dashboard" img="class:archive">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>
|
|
||||||
</submenu>
|
|
||||||
<files folder=".">
|
|
||||||
<folder>cli</folder>
|
|
||||||
<folder>forms</folder>
|
|
||||||
<folder>services</folder>
|
|
||||||
<folder>sql</folder>
|
|
||||||
<folder>src</folder>
|
|
||||||
<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>
|
|
||||||
</languages>
|
|
||||||
</administration>
|
|
||||||
|
|
||||||
<api>
|
|
||||||
<files folder="api">
|
|
||||||
<folder>src</folder>
|
|
||||||
</files>
|
|
||||||
</api>
|
|
||||||
</extension>
|
|
||||||
@@ -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,194 +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
|
|
||||||
*
|
|
||||||
* AJAX controller for step-based backups.
|
|
||||||
* Handles init and step requests from the admin UI JavaScript.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Controller\BaseController;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\SteppedBackupEngine;
|
|
||||||
|
|
||||||
class AjaxController extends BaseController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Initialize a new stepped backup.
|
|
||||||
* POST: task=ajax.init&profile_id=1&description=...
|
|
||||||
*/
|
|
||||||
public function init(): void
|
|
||||||
{
|
|
||||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$profileId = $this->input->getInt('profile_id', 1);
|
|
||||||
$description = $this->input->getString('description', '');
|
|
||||||
|
|
||||||
$engine = new SteppedBackupEngine();
|
|
||||||
$result = $engine->init($profileId, $description, 'backend');
|
|
||||||
|
|
||||||
$this->sendJson($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the next step of a backup session.
|
|
||||||
* POST: task=ajax.step&session_id=mb_...
|
|
||||||
*/
|
|
||||||
public function step(): void
|
|
||||||
{
|
|
||||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$sessionId = $this->input->getString('session_id', '');
|
|
||||||
|
|
||||||
if (empty($sessionId)) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Missing session_id']);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$engine = new SteppedBackupEngine();
|
|
||||||
$result = $engine->runStep($sessionId);
|
|
||||||
|
|
||||||
$this->sendJson($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Browse server directories for the folder picker field.
|
|
||||||
* POST: task=ajax.browseDir&path=/some/path
|
|
||||||
*/
|
|
||||||
public function browseDir(): void
|
|
||||||
{
|
|
||||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$path = $this->input->getString('path', JPATH_ROOT);
|
|
||||||
$path = realpath($path) ?: $path;
|
|
||||||
|
|
||||||
if (!is_dir($path)) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Directory not found: ' . $path]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security: only allow browsing within JPATH_ROOT or parent directories
|
|
||||||
// that could contain a backup folder (e.g., /home/user/backups)
|
|
||||||
$dirs = [];
|
|
||||||
$handle = @opendir($path);
|
|
||||||
|
|
||||||
if ($handle) {
|
|
||||||
while (($entry = readdir($handle)) !== false) {
|
|
||||||
if ($entry === '.' || $entry === '..') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fullPath = $path . '/' . $entry;
|
|
||||||
|
|
||||||
if (is_dir($fullPath) && $entry[0] !== '.') {
|
|
||||||
$dirs[] = [
|
|
||||||
'name' => $entry,
|
|
||||||
'path' => $fullPath,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closedir($handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
usort($dirs, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
|
||||||
|
|
||||||
$parent = dirname($path);
|
|
||||||
|
|
||||||
$this->sendJson([
|
|
||||||
'error' => false,
|
|
||||||
'current' => $path,
|
|
||||||
'parent' => ($parent !== $path) ? $parent : null,
|
|
||||||
'dirs' => $dirs,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load and return the log file contents for a backup record.
|
|
||||||
* POST: task=ajax.viewLog&id=123
|
|
||||||
*/
|
|
||||||
public function viewLog(): void
|
|
||||||
{
|
|
||||||
if (!Session::checkToken('get') && !Session::checkToken('post')) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Invalid token']);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = $this->input->getInt('id', 0);
|
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Missing record ID']);
|
|
||||||
|
|
||||||
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') . ' = ' . $id);
|
|
||||||
$db->setQuery($query);
|
|
||||||
$record = $db->loadObject();
|
|
||||||
|
|
||||||
if (!$record) {
|
|
||||||
$this->sendJson(['error' => true, 'message' => 'Record not found']);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load log from file alongside the archive
|
|
||||||
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $record->absolute_path);
|
|
||||||
$logContent = '';
|
|
||||||
|
|
||||||
if (is_file($logPath)) {
|
|
||||||
$logContent = file_get_contents($logPath);
|
|
||||||
} elseif (!empty($record->log)) {
|
|
||||||
// Fall back to database-stored log
|
|
||||||
$logContent = $record->log;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->sendJson([
|
|
||||||
'error' => false,
|
|
||||||
'log' => $logContent ?: '(no log available)',
|
|
||||||
'source' => is_file($logPath) ? 'file' : 'database',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a JSON response and close the application.
|
|
||||||
*/
|
|
||||||
private function sendJson(array $data): void
|
|
||||||
{
|
|
||||||
$app = $this->app;
|
|
||||||
$app->setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
||||||
$app->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
||||||
$app->sendHeaders();
|
|
||||||
|
|
||||||
echo json_encode($data);
|
|
||||||
|
|
||||||
$app->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +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\Engine;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
interface ArchiverInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Open or create the archive at the given path.
|
|
||||||
*/
|
|
||||||
public function open(string $path): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a string as a file inside the archive.
|
|
||||||
*/
|
|
||||||
public function addFromString(string $localName, string $contents): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a file from disk into the archive.
|
|
||||||
*/
|
|
||||||
public function addFile(string $filePath, string $localName): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finalize and close the archive.
|
|
||||||
*/
|
|
||||||
public function close(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the file extension for this archive type (e.g. 'zip', 'tar.gz').
|
|
||||||
*/
|
|
||||||
public function getExtension(): string;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,122 +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
|
|
||||||
*
|
|
||||||
* Resolves placeholders like [host], [date], [profile_name] in backup
|
|
||||||
* directory paths and archive filename formats.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
|
|
||||||
class PlaceholderResolver
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Supported placeholders and their descriptions (for documentation).
|
|
||||||
*/
|
|
||||||
public const PLACEHOLDERS = [
|
|
||||||
'[host]' => 'Server hostname',
|
|
||||||
'[date]' => 'Date as Ymd (e.g. 20260604)',
|
|
||||||
'[time]' => 'Time as His (e.g. 143025)',
|
|
||||||
'[datetime]' => 'Date and time as Ymd_His',
|
|
||||||
'[year]' => 'Four-digit year',
|
|
||||||
'[month]' => 'Two-digit month',
|
|
||||||
'[day]' => 'Two-digit day',
|
|
||||||
'[hour]' => 'Two-digit hour (24h)',
|
|
||||||
'[minute]' => 'Two-digit minute',
|
|
||||||
'[second]' => 'Two-digit second',
|
|
||||||
'[profile_id]' => 'Backup profile ID',
|
|
||||||
'[profile_name]' => 'Profile title (sanitized)',
|
|
||||||
'[site_name]' => 'Joomla site name (sanitized)',
|
|
||||||
'[type]' => 'Backup type (full, database, files, differential)',
|
|
||||||
'[random]' => 'Random 6-character hex string',
|
|
||||||
];
|
|
||||||
|
|
||||||
private array $replacements;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param object $profile The backup profile object
|
|
||||||
*/
|
|
||||||
public function __construct(object $profile)
|
|
||||||
{
|
|
||||||
$now = new \DateTimeImmutable('now');
|
|
||||||
$hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n'));
|
|
||||||
|
|
||||||
$siteName = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$siteName = Factory::getApplication()->get('sitename', '');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Fallback: not critical
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->replacements = [
|
|
||||||
'[host]' => $hostname,
|
|
||||||
'[date]' => $now->format('Ymd'),
|
|
||||||
'[time]' => $now->format('His'),
|
|
||||||
'[datetime]' => $now->format('Ymd_His'),
|
|
||||||
'[year]' => $now->format('Y'),
|
|
||||||
'[month]' => $now->format('m'),
|
|
||||||
'[day]' => $now->format('d'),
|
|
||||||
'[hour]' => $now->format('H'),
|
|
||||||
'[minute]' => $now->format('i'),
|
|
||||||
'[second]' => $now->format('s'),
|
|
||||||
'[profile_id]' => (string) ($profile->id ?? '0'),
|
|
||||||
'[profile_name]' => $this->sanitize($profile->title ?? 'default'),
|
|
||||||
'[site_name]' => $this->sanitize($siteName ?: 'joomla'),
|
|
||||||
'[type]' => $profile->backup_type ?? 'full',
|
|
||||||
'[random]' => bin2hex(random_bytes(3)),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace all placeholders in a string.
|
|
||||||
*
|
|
||||||
* @param string $template String containing [placeholder] tokens
|
|
||||||
*
|
|
||||||
* @return string Resolved string
|
|
||||||
*/
|
|
||||||
public function resolve(string $template): string
|
|
||||||
{
|
|
||||||
return str_replace(
|
|
||||||
array_keys($this->replacements),
|
|
||||||
array_values($this->replacements),
|
|
||||||
$template
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the raw hostname value (for backward compatibility).
|
|
||||||
*/
|
|
||||||
public function getHostname(): string
|
|
||||||
{
|
|
||||||
return $this->replacements['[host]'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the datetime tag value (for backward compatibility).
|
|
||||||
*/
|
|
||||||
public function getTag(): string
|
|
||||||
{
|
|
||||||
return $this->replacements['[datetime]'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize a string for use in filenames/paths.
|
|
||||||
* Keeps alphanumerics, dots, hyphens, underscores. Replaces spaces with hyphens.
|
|
||||||
*/
|
|
||||||
private function sanitize(string $value): string
|
|
||||||
{
|
|
||||||
$value = str_replace(' ', '-', trim($value));
|
|
||||||
|
|
||||||
return preg_replace('/[^a-zA-Z0-9._-]/', '', $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +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\Engine;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
class TarGzArchiver implements ArchiverInterface
|
|
||||||
{
|
|
||||||
private \PharData $tar;
|
|
||||||
private string $tarPath;
|
|
||||||
|
|
||||||
public function open(string $path): void
|
|
||||||
{
|
|
||||||
// PharData creates .tar first, then we compress to .tar.gz
|
|
||||||
// Strip .gz to get the .tar path for initial creation
|
|
||||||
$this->tarPath = preg_replace('/\.gz$/', '', $path);
|
|
||||||
|
|
||||||
// Remove existing files to avoid "already exists" errors
|
|
||||||
if (is_file($this->tarPath)) {
|
|
||||||
@unlink($this->tarPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_file($path)) {
|
|
||||||
@unlink($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->tar = new \PharData($this->tarPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addFromString(string $localName, string $contents): void
|
|
||||||
{
|
|
||||||
$this->tar->addFromString($localName, $contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addFile(string $filePath, string $localName): void
|
|
||||||
{
|
|
||||||
$this->tar->addFile($filePath, $localName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function close(): void
|
|
||||||
{
|
|
||||||
// Compress the .tar to .tar.gz
|
|
||||||
$this->tar->compress(\Phar::GZ);
|
|
||||||
|
|
||||||
// Remove the uncompressed .tar
|
|
||||||
if (is_file($this->tarPath)) {
|
|
||||||
@unlink($this->tarPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getExtension(): string
|
|
||||||
{
|
|
||||||
return 'tar.gz';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +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\Engine;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
class ZipArchiver implements ArchiverInterface
|
|
||||||
{
|
|
||||||
private \ZipArchive $zip;
|
|
||||||
|
|
||||||
public function open(string $path): void
|
|
||||||
{
|
|
||||||
$this->zip = new \ZipArchive();
|
|
||||||
|
|
||||||
if ($this->zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
|
||||||
throw new \RuntimeException('Cannot create ZIP archive: ' . $path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addFromString(string $localName, string $contents): void
|
|
||||||
{
|
|
||||||
$this->zip->addFromString($localName, $contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addFile(string $filePath, string $localName): void
|
|
||||||
{
|
|
||||||
$this->zip->addFile($filePath, $localName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function close(): void
|
|
||||||
{
|
|
||||||
$this->zip->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getExtension(): string
|
|
||||||
{
|
|
||||||
return 'zip';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +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\Field;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Form\FormField;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
|
|
||||||
class DatabaseTablesField extends FormField
|
|
||||||
{
|
|
||||||
protected $type = 'DatabaseTables';
|
|
||||||
|
|
||||||
protected function getInput(): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$tables = $db->getTableList();
|
|
||||||
$prefix = $db->getPrefix();
|
|
||||||
|
|
||||||
// Parse current exclusions (newline-separated, with optional :data-only suffix)
|
|
||||||
$excludeData = [];
|
|
||||||
$excludeStructure = [];
|
|
||||||
|
|
||||||
if (!empty($this->value)) {
|
|
||||||
$lines = array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value))));
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
// Normalize table name to real prefix for comparison
|
|
||||||
if (str_ends_with($line, ':data-only')) {
|
|
||||||
$tableName = str_replace('#__', $prefix, substr($line, 0, -10));
|
|
||||||
$excludeData[$tableName] = true;
|
|
||||||
} elseif (str_ends_with($line, ':structure-only')) {
|
|
||||||
$tableName = str_replace('#__', $prefix, substr($line, 0, -15));
|
|
||||||
$excludeStructure[$tableName] = true;
|
|
||||||
} else {
|
|
||||||
// No suffix = exclude both (backward compatible)
|
|
||||||
$tableName = str_replace('#__', $prefix, $line);
|
|
||||||
$excludeData[$tableName] = true;
|
|
||||||
$excludeStructure[$tableName] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
|
|
||||||
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
|
||||||
|
|
||||||
$html = '<div class="mb-2">';
|
|
||||||
$html .= '<input type="hidden" name="' . $name . '" id="' . $id . '" value="" />';
|
|
||||||
$html .= '<div class="form-text mb-2">' . Text::_('COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_HELP') . '</div>';
|
|
||||||
$html .= '<div class="table-responsive" style="max-height:400px; overflow-y:auto;">';
|
|
||||||
$html .= '<table class="table table-sm table-hover mb-0">';
|
|
||||||
$html .= '<thead class="sticky-top bg-white"><tr>';
|
|
||||||
$html .= '<th class="w-1"><input type="checkbox" id="' . $id . '_toggleData" title="Toggle all data" /></th>';
|
|
||||||
$html .= '<th class="w-1">' . Text::_('COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DATA') . '</th>';
|
|
||||||
$html .= '<th class="w-1"><input type="checkbox" id="' . $id . '_toggleStructure" title="Toggle all structure" /></th>';
|
|
||||||
$html .= '<th class="w-1">' . Text::_('COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_STRUCTURE') . '</th>';
|
|
||||||
$html .= '<th>' . Text::_('COM_MOKOJOOMBACKUP_FIELD_TABLE_NAME') . '</th>';
|
|
||||||
$html .= '</tr></thead><tbody>';
|
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
|
||||||
$dataChecked = isset($excludeData[$table]) ? ' checked' : '';
|
|
||||||
$structureChecked = isset($excludeStructure[$table]) ? ' checked' : '';
|
|
||||||
|
|
||||||
// Convert to #__ notation for storage
|
|
||||||
$storeValue = $table;
|
|
||||||
|
|
||||||
if (str_starts_with($table, $prefix)) {
|
|
||||||
$storeValue = '#__' . substr($table, \strlen($prefix));
|
|
||||||
}
|
|
||||||
|
|
||||||
$safeValue = htmlspecialchars($storeValue, ENT_QUOTES, 'UTF-8');
|
|
||||||
$safeTable = htmlspecialchars($table, ENT_QUOTES, 'UTF-8');
|
|
||||||
|
|
||||||
$html .= '<tr>';
|
|
||||||
$html .= '<td></td>';
|
|
||||||
$html .= '<td><input type="checkbox" class="' . $id . '_data" value="' . $safeValue . '"' . $dataChecked . ' /></td>';
|
|
||||||
$html .= '<td></td>';
|
|
||||||
$html .= '<td><input type="checkbox" class="' . $id . '_structure" value="' . $safeValue . '"' . $structureChecked . ' /></td>';
|
|
||||||
$html .= '<td><code>' . $safeTable . '</code></td>';
|
|
||||||
$html .= '</tr>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$html .= '</tbody></table></div></div>';
|
|
||||||
|
|
||||||
// Script to sync checkboxes to hidden field
|
|
||||||
$html .= <<<SCRIPT
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var hidden = document.getElementById('{$id}');
|
|
||||||
var dataCbs = document.querySelectorAll('.{$id}_data');
|
|
||||||
var structCbs = document.querySelectorAll('.{$id}_structure');
|
|
||||||
var toggleData = document.getElementById('{$id}_toggleData');
|
|
||||||
var toggleStructure = document.getElementById('{$id}_toggleStructure');
|
|
||||||
|
|
||||||
function sync() {
|
|
||||||
var result = {};
|
|
||||||
dataCbs.forEach(function(cb) {
|
|
||||||
if (cb.checked) result[cb.value] = (result[cb.value] || 0) | 1;
|
|
||||||
});
|
|
||||||
structCbs.forEach(function(cb) {
|
|
||||||
if (cb.checked) result[cb.value] = (result[cb.value] || 0) | 2;
|
|
||||||
});
|
|
||||||
var lines = [];
|
|
||||||
for (var table in result) {
|
|
||||||
if (result[table] === 3) {
|
|
||||||
lines.push(table);
|
|
||||||
} else if (result[table] === 1) {
|
|
||||||
lines.push(table + ':data-only');
|
|
||||||
} else if (result[table] === 2) {
|
|
||||||
lines.push(table + ':structure-only');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hidden.value = lines.join('\\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
dataCbs.forEach(function(cb) { cb.addEventListener('change', sync); });
|
|
||||||
structCbs.forEach(function(cb) { cb.addEventListener('change', sync); });
|
|
||||||
|
|
||||||
toggleData.addEventListener('change', function() {
|
|
||||||
var state = this.checked;
|
|
||||||
dataCbs.forEach(function(cb) { cb.checked = state; });
|
|
||||||
sync();
|
|
||||||
});
|
|
||||||
|
|
||||||
toggleStructure.addEventListener('change', function() {
|
|
||||||
var state = this.checked;
|
|
||||||
structCbs.forEach(function(cb) { cb.checked = state; });
|
|
||||||
sync();
|
|
||||||
});
|
|
||||||
|
|
||||||
sync();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
SCRIPT;
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,259 +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
|
|
||||||
*
|
|
||||||
* Interactive directory tree field with checkboxes for exclude/include filtering.
|
|
||||||
* Loads the directory tree from the server via AJAX (browseDir endpoint).
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoJoomBackup\Administrator\Field;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Form\FormField;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
|
|
||||||
class DirectoryFilterField extends FormField
|
|
||||||
{
|
|
||||||
protected $type = 'DirectoryFilter';
|
|
||||||
|
|
||||||
protected function getInput(): string
|
|
||||||
{
|
|
||||||
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
|
|
||||||
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
|
||||||
$mode = htmlspecialchars((string) ($this->element['mode'] ?? 'exclude'), ENT_QUOTES, 'UTF-8');
|
|
||||||
|
|
||||||
// Parse current values (newline-separated)
|
|
||||||
$items = [];
|
|
||||||
|
|
||||||
if (!empty($this->value)) {
|
|
||||||
$items = array_values(array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value)))));
|
|
||||||
}
|
|
||||||
|
|
||||||
$itemsJson = json_encode($items);
|
|
||||||
$jRoot = json_encode(JPATH_ROOT);
|
|
||||||
|
|
||||||
$labelExclude = Text::_('COM_MOKOJOOMBACKUP_FILTER_EXCLUDED');
|
|
||||||
$labelInclude = Text::_('COM_MOKOJOOMBACKUP_FILTER_INCLUDED');
|
|
||||||
$labelManual = Text::_('COM_MOKOJOOMBACKUP_FILTER_ADD_MANUAL');
|
|
||||||
$addLabel = Text::_('JGLOBAL_FIELD_ADD');
|
|
||||||
$placeholder = htmlspecialchars((string) ($this->element['hint'] ?? 'path/to/directory'), ENT_QUOTES, 'UTF-8');
|
|
||||||
|
|
||||||
return <<<HTML
|
|
||||||
<div id="{$id}_wrap">
|
|
||||||
<input type="hidden" name="{$name}" id="{$id}" value="" />
|
|
||||||
|
|
||||||
<!-- Manual entry row -->
|
|
||||||
<div class="input-group input-group-sm mb-2">
|
|
||||||
<input type="text" class="form-control" id="{$id}_manual" placeholder="{$placeholder}" />
|
|
||||||
<button type="button" class="btn btn-outline-success" id="{$id}_addBtn">
|
|
||||||
<span class="icon-plus" aria-hidden="true"></span> {$addLabel}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Selected items (pills) -->
|
|
||||||
<div id="{$id}_pills" class="mb-2 d-flex flex-wrap gap-1"></div>
|
|
||||||
|
|
||||||
<!-- Browsable tree -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header py-1 px-2 d-flex justify-content-between align-items-center">
|
|
||||||
<small class="fw-bold text-muted" id="{$id}_cwd"></small>
|
|
||||||
<button type="button" class="btn btn-sm btn-link p-0" id="{$id}_upBtn" style="display:none;">
|
|
||||||
<span class="icon-arrow-up-4" aria-hidden="true"></span> ..
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="{$id}_tree" class="list-group list-group-flush" style="max-height:300px; overflow-y:auto;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
#{$id}_wrap .mb-dir-pill {
|
|
||||||
display: inline-flex; align-items: center; gap: 0.3rem;
|
|
||||||
padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;
|
|
||||||
font-family: monospace; cursor: default;
|
|
||||||
}
|
|
||||||
#{$id}_wrap .mb-dir-pill.excluded { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
|
|
||||||
#{$id}_wrap .mb-dir-pill.included { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
|
|
||||||
#{$id}_wrap .mb-dir-pill .btn-close { font-size: 0.6rem; }
|
|
||||||
#{$id}_wrap .mb-dir-row { display: flex; align-items: center; padding: 0.35rem 0.75rem; gap: 0.5rem; border-bottom: 1px solid #eee; }
|
|
||||||
#{$id}_wrap .mb-dir-row:hover { background: #f8f9fa; }
|
|
||||||
#{$id}_wrap .mb-dir-row .mb-dir-name { cursor: pointer; flex: 1; font-size: 0.9rem; }
|
|
||||||
#{$id}_wrap .mb-dir-row .mb-dir-name:hover { color: #0d6efd; text-decoration: underline; }
|
|
||||||
#{$id}_wrap .mb-dir-check { width: 1rem; height: 1rem; cursor: pointer; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
const id = '{$id}';
|
|
||||||
const hidden = document.getElementById(id);
|
|
||||||
const pills = document.getElementById(id + '_pills');
|
|
||||||
const tree = document.getElementById(id + '_tree');
|
|
||||||
const cwdEl = document.getElementById(id + '_cwd');
|
|
||||||
const upBtn = document.getElementById(id + '_upBtn');
|
|
||||||
const manualInput = document.getElementById(id + '_manual');
|
|
||||||
const addBtn = document.getElementById(id + '_addBtn');
|
|
||||||
const jRoot = {$jRoot};
|
|
||||||
|
|
||||||
let selected = new Set({$itemsJson});
|
|
||||||
let currentPath = jRoot;
|
|
||||||
let parentPath = null;
|
|
||||||
|
|
||||||
function sync() {
|
|
||||||
hidden.value = Array.from(selected).join('\\n');
|
|
||||||
renderPills();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPills() {
|
|
||||||
while (pills.firstChild) pills.removeChild(pills.firstChild);
|
|
||||||
selected.forEach(function(path) {
|
|
||||||
const pill = document.createElement('span');
|
|
||||||
pill.className = 'mb-dir-pill excluded';
|
|
||||||
|
|
||||||
const icon = document.createElement('span');
|
|
||||||
icon.className = 'icon-folder';
|
|
||||||
icon.setAttribute('aria-hidden', 'true');
|
|
||||||
pill.appendChild(icon);
|
|
||||||
|
|
||||||
pill.appendChild(document.createTextNode(' ' + path + ' '));
|
|
||||||
|
|
||||||
const closeBtn = document.createElement('button');
|
|
||||||
closeBtn.type = 'button';
|
|
||||||
closeBtn.className = 'btn-close btn-close-sm';
|
|
||||||
closeBtn.setAttribute('aria-label', 'Remove');
|
|
||||||
closeBtn.addEventListener('click', function() {
|
|
||||||
selected.delete(path);
|
|
||||||
sync();
|
|
||||||
refreshTree();
|
|
||||||
});
|
|
||||||
pill.appendChild(closeBtn);
|
|
||||||
|
|
||||||
pills.appendChild(pill);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toRelative(absPath) {
|
|
||||||
if (absPath.indexOf(jRoot) === 0) {
|
|
||||||
let rel = absPath.substring(jRoot.length);
|
|
||||||
if (rel.charAt(0) === '/') rel = rel.substring(1);
|
|
||||||
return rel;
|
|
||||||
}
|
|
||||||
return absPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTreeMessage(text, cls) {
|
|
||||||
while (tree.firstChild) tree.removeChild(tree.firstChild);
|
|
||||||
const msg = document.createElement('div');
|
|
||||||
msg.className = 'p-2 ' + cls;
|
|
||||||
msg.textContent = text;
|
|
||||||
tree.appendChild(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadDir(path) {
|
|
||||||
setTreeMessage('Loading...', 'text-muted');
|
|
||||||
currentPath = path;
|
|
||||||
|
|
||||||
const form = new URLSearchParams();
|
|
||||||
form.append('task', 'ajax.browseDir');
|
|
||||||
form.append('path', path);
|
|
||||||
const tokenName = Joomla.getOptions('csrf.token') || '';
|
|
||||||
if (tokenName) form.append(tokenName, '1');
|
|
||||||
|
|
||||||
fetch('index.php?option=com_mokojoombackup&format=json', {
|
|
||||||
method: 'POST', body: form,
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.error) {
|
|
||||||
setTreeMessage(data.message || 'Error', 'text-danger');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
parentPath = data.parent || null;
|
|
||||||
cwdEl.textContent = data.current || path;
|
|
||||||
upBtn.style.display = parentPath ? '' : 'none';
|
|
||||||
renderTree(data.dirs || []);
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
setTreeMessage('Error: ' + err.message, 'text-danger');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshTree() {
|
|
||||||
loadDir(currentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTree(dirs) {
|
|
||||||
while (tree.firstChild) tree.removeChild(tree.firstChild);
|
|
||||||
if (dirs.length === 0) {
|
|
||||||
setTreeMessage('(empty)', 'text-muted');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dirs.forEach(function(dir) {
|
|
||||||
const rel = toRelative(dir.path);
|
|
||||||
const isExcluded = selected.has(rel);
|
|
||||||
|
|
||||||
const row = document.createElement('div');
|
|
||||||
row.className = 'mb-dir-row' + (isExcluded ? ' bg-danger bg-opacity-10' : '');
|
|
||||||
|
|
||||||
const cb = document.createElement('input');
|
|
||||||
cb.type = 'checkbox';
|
|
||||||
cb.className = 'mb-dir-check form-check-input';
|
|
||||||
cb.checked = isExcluded;
|
|
||||||
cb.title = isExcluded ? 'Excluded — uncheck to include' : 'Check to exclude';
|
|
||||||
cb.addEventListener('change', function() {
|
|
||||||
if (cb.checked) {
|
|
||||||
selected.add(rel);
|
|
||||||
} else {
|
|
||||||
selected.delete(rel);
|
|
||||||
}
|
|
||||||
sync();
|
|
||||||
refreshTree();
|
|
||||||
});
|
|
||||||
|
|
||||||
const icon = document.createElement('span');
|
|
||||||
icon.className = isExcluded ? 'icon-unpublish text-danger' : 'icon-folder text-warning';
|
|
||||||
icon.setAttribute('aria-hidden', 'true');
|
|
||||||
|
|
||||||
const nameEl = document.createElement('span');
|
|
||||||
nameEl.className = 'mb-dir-name';
|
|
||||||
nameEl.textContent = dir.name;
|
|
||||||
nameEl.addEventListener('click', function() { loadDir(dir.path); });
|
|
||||||
|
|
||||||
row.appendChild(cb);
|
|
||||||
row.appendChild(icon);
|
|
||||||
row.appendChild(nameEl);
|
|
||||||
tree.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
upBtn.addEventListener('click', function() {
|
|
||||||
if (parentPath) loadDir(parentPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
addBtn.addEventListener('click', function() {
|
|
||||||
const val = manualInput.value.trim();
|
|
||||||
if (val && !selected.has(val)) {
|
|
||||||
selected.add(val);
|
|
||||||
manualInput.value = '';
|
|
||||||
sync();
|
|
||||||
refreshTree();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
manualInput.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
|
|
||||||
});
|
|
||||||
|
|
||||||
sync();
|
|
||||||
loadDir(jRoot);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
HTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +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\Field;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Form\FormField;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
|
|
||||||
class ExcludeListField extends FormField
|
|
||||||
{
|
|
||||||
protected $type = 'ExcludeList';
|
|
||||||
|
|
||||||
protected function getInput(): string
|
|
||||||
{
|
|
||||||
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
|
|
||||||
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
|
||||||
$placeholder = htmlspecialchars((string) ($this->element['hint'] ?? ''), ENT_QUOTES, 'UTF-8');
|
|
||||||
|
|
||||||
// Parse current values (newline-separated)
|
|
||||||
$items = [];
|
|
||||||
|
|
||||||
if (!empty($this->value)) {
|
|
||||||
$items = array_values(array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value)))));
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = '<div id="' . $id . '_wrapper">';
|
|
||||||
$html .= '<input type="hidden" name="' . $name . '" id="' . $id . '" value="" />';
|
|
||||||
$html .= '<table class="table table-sm mb-1" id="' . $id . '_table">';
|
|
||||||
$html .= '<tbody>';
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$safeItem = htmlspecialchars($item, ENT_QUOTES, 'UTF-8');
|
|
||||||
$html .= '<tr>';
|
|
||||||
$html .= '<td><input type="text" class="form-control form-control-sm ' . $id . '_input" value="' . $safeItem . '" placeholder="' . $placeholder . '" /></td>';
|
|
||||||
$html .= '<td class="w-1"><button type="button" class="btn btn-sm btn-outline-danger ' . $id . '_remove"><span class="icon-delete" aria-hidden="true"></span></button></td>';
|
|
||||||
$html .= '</tr>';
|
|
||||||
}
|
|
||||||
|
|
||||||
$html .= '</tbody></table>';
|
|
||||||
$html .= '<button type="button" class="btn btn-sm btn-outline-success" id="' . $id . '_add">';
|
|
||||||
$html .= '<span class="icon-plus" aria-hidden="true"></span> ' . Text::_('JGLOBAL_FIELD_ADD') . '</button>';
|
|
||||||
$html .= '</div>';
|
|
||||||
|
|
||||||
$html .= <<<SCRIPT
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var wrapper = document.getElementById('{$id}_wrapper');
|
|
||||||
var hidden = document.getElementById('{$id}');
|
|
||||||
var tbody = document.querySelector('#{$id}_table tbody');
|
|
||||||
var addBtn = document.getElementById('{$id}_add');
|
|
||||||
var placeholder = '{$placeholder}';
|
|
||||||
|
|
||||||
function sync() {
|
|
||||||
var vals = [];
|
|
||||||
wrapper.querySelectorAll('.{$id}_input').forEach(function(inp) {
|
|
||||||
var v = inp.value.trim();
|
|
||||||
if (v) vals.push(v);
|
|
||||||
});
|
|
||||||
hidden.value = vals.join('\\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRow(value) {
|
|
||||||
var tr = document.createElement('tr');
|
|
||||||
var td1 = document.createElement('td');
|
|
||||||
var inp = document.createElement('input');
|
|
||||||
inp.type = 'text';
|
|
||||||
inp.className = 'form-control form-control-sm {$id}_input';
|
|
||||||
inp.value = value || '';
|
|
||||||
inp.placeholder = placeholder;
|
|
||||||
inp.addEventListener('input', sync);
|
|
||||||
td1.appendChild(inp);
|
|
||||||
|
|
||||||
var td2 = document.createElement('td');
|
|
||||||
td2.className = 'w-1';
|
|
||||||
var btn = document.createElement('button');
|
|
||||||
btn.type = 'button';
|
|
||||||
btn.className = 'btn btn-sm btn-outline-danger {$id}_remove';
|
|
||||||
var icon = document.createElement('span');
|
|
||||||
icon.className = 'icon-delete';
|
|
||||||
icon.setAttribute('aria-hidden', 'true');
|
|
||||||
btn.appendChild(icon);
|
|
||||||
btn.addEventListener('click', function() { tr.remove(); sync(); });
|
|
||||||
td2.appendChild(btn);
|
|
||||||
|
|
||||||
tr.appendChild(td1);
|
|
||||||
tr.appendChild(td2);
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
inp.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
addBtn.addEventListener('click', function() { addRow(''); });
|
|
||||||
|
|
||||||
wrapper.querySelectorAll('.{$id}_input').forEach(function(inp) {
|
|
||||||
inp.addEventListener('input', sync);
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.querySelectorAll('.{$id}_remove').forEach(function(btn) {
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
btn.closest('tr').remove();
|
|
||||||
sync();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
sync();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
SCRIPT;
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,239 +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\Field;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Form\FormField;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
|
|
||||||
class FolderPickerField extends FormField
|
|
||||||
{
|
|
||||||
protected $type = 'FolderPicker';
|
|
||||||
|
|
||||||
protected function getInput(): string
|
|
||||||
{
|
|
||||||
$value = htmlspecialchars($this->value ?: $this->default, ENT_QUOTES, 'UTF-8');
|
|
||||||
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
|
|
||||||
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
|
||||||
$jRoot = JPATH_ROOT;
|
|
||||||
|
|
||||||
// Resolve to absolute for display
|
|
||||||
$rawValue = $this->value ?: $this->default;
|
|
||||||
|
|
||||||
if ($rawValue && $rawValue[0] !== '/') {
|
|
||||||
$absPath = $jRoot . '/' . $rawValue;
|
|
||||||
} else {
|
|
||||||
$absPath = $rawValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build placeholder map for JS resolution
|
|
||||||
$hostname = preg_replace('/[^a-zA-Z0-9._-]/', '', $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? php_uname('n'));
|
|
||||||
$siteName = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$siteName = Factory::getApplication()->get('sitename', '');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
$sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName)));
|
|
||||||
|
|
||||||
$placeholders = [
|
|
||||||
'[host]' => $hostname,
|
|
||||||
'[site_name]' => $sanitizedSiteName ?: 'joomla',
|
|
||||||
'[profile_id]' => '1',
|
|
||||||
'[profile_name]' => 'default',
|
|
||||||
'[type]' => 'full',
|
|
||||||
'[year]' => date('Y'),
|
|
||||||
'[month]' => date('m'),
|
|
||||||
'[day]' => date('d'),
|
|
||||||
'[date]' => date('Ymd'),
|
|
||||||
];
|
|
||||||
|
|
||||||
$placeholdersJson = json_encode($placeholders);
|
|
||||||
|
|
||||||
// Resolve placeholders for the status display
|
|
||||||
$resolvedPath = str_replace(array_keys($placeholders), array_values($placeholders), $absPath);
|
|
||||||
$hasPlaceholders = preg_match('/\[.+\]/', $absPath);
|
|
||||||
|
|
||||||
if ($hasPlaceholders) {
|
|
||||||
$exists = is_dir($resolvedPath);
|
|
||||||
$statusClass = $exists ? 'text-success' : 'text-info';
|
|
||||||
$statusIcon = $exists ? 'icon-publish' : 'icon-info-circle';
|
|
||||||
$statusText = Text::_('COM_MOKOJOOMBACKUP_FOLDER_PLACEHOLDER');
|
|
||||||
$resolvedSafe = htmlspecialchars($resolvedPath, ENT_QUOTES, 'UTF-8');
|
|
||||||
$statusDetail = "{$statusText}: <code>{$resolvedSafe}</code>";
|
|
||||||
} else {
|
|
||||||
$exists = is_dir($absPath);
|
|
||||||
$statusClass = $exists ? 'text-success' : 'text-danger';
|
|
||||||
$statusIcon = $exists ? 'icon-publish' : 'icon-unpublish';
|
|
||||||
$statusText = $exists
|
|
||||||
? Text::_('COM_MOKOJOOMBACKUP_FOLDER_EXISTS')
|
|
||||||
: Text::_('COM_MOKOJOOMBACKUP_FOLDER_NOT_FOUND');
|
|
||||||
$absPathSafe = htmlspecialchars($absPath, ENT_QUOTES, 'UTF-8');
|
|
||||||
$statusDetail = "{$statusText}: <code>{$absPathSafe}</code>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return <<<HTML
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" name="{$name}" id="{$id}" value="{$value}"
|
|
||||||
class="form-control" maxlength="512"
|
|
||||||
placeholder="/home/user/backups/[host] or administrator/components/com_mokojoombackup/backups" />
|
|
||||||
<button type="button" class="btn btn-outline-secondary" id="{$id}_btn">
|
|
||||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
|
||||||
Browse
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1" id="{$id}_status">
|
|
||||||
<small class="{$statusClass}">
|
|
||||||
<span class="{$statusIcon}" aria-hidden="true"></span>
|
|
||||||
{$statusDetail}
|
|
||||||
</small>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var fieldId = '{$id}';
|
|
||||||
var btn = document.getElementById(fieldId + '_btn');
|
|
||||||
var browser = document.getElementById(fieldId + '_browser');
|
|
||||||
var tree = document.getElementById(fieldId + '_tree');
|
|
||||||
var input = document.getElementById(fieldId);
|
|
||||||
var placeholders = {$placeholdersJson};
|
|
||||||
|
|
||||||
// Resolve placeholders in a path (forward: [site_name] -> actual value)
|
|
||||||
function resolve(path) {
|
|
||||||
for (var key in placeholders) {
|
|
||||||
path = path.split(key).join(placeholders[key]);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse-replace actual values back to placeholders for portable storage
|
|
||||||
function unresolve(path) {
|
|
||||||
for (var key in placeholders) {
|
|
||||||
if (placeholders[key] && placeholders[key].length > 1) {
|
|
||||||
path = path.split(placeholders[key]).join(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
btn.addEventListener('click', function() {
|
|
||||||
if (browser.style.display !== 'none') {
|
|
||||||
browser.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
browser.style.display = 'block';
|
|
||||||
// Resolve placeholders before browsing so the server sees real paths
|
|
||||||
loadDir(resolve(input.value || '/'));
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadDir(path) {
|
|
||||||
tree.textContent = 'Loading...';
|
|
||||||
|
|
||||||
var form = new URLSearchParams();
|
|
||||||
form.append('task', 'ajax.browseDir');
|
|
||||||
form.append('path', path);
|
|
||||||
|
|
||||||
var tokenName = Joomla.getOptions('csrf.token') || '';
|
|
||||||
if (tokenName) form.append(tokenName, '1');
|
|
||||||
|
|
||||||
fetch('index.php?option=com_mokojoombackup&format=json', {
|
|
||||||
method: 'POST',
|
|
||||||
body: form,
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.error) {
|
|
||||||
tree.textContent = data.message || 'Error loading directory';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renderTree(data, path);
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
tree.textContent = 'Error: ' + err.message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTree(data, path) {
|
|
||||||
while (tree.firstChild) tree.removeChild(tree.firstChild);
|
|
||||||
|
|
||||||
var list = document.createElement('div');
|
|
||||||
list.className = 'list-group list-group-flush';
|
|
||||||
|
|
||||||
if (data.parent) {
|
|
||||||
var up = document.createElement('a');
|
|
||||||
up.href = '#';
|
|
||||||
up.className = 'list-group-item list-group-item-action py-1';
|
|
||||||
var upIcon = document.createElement('span');
|
|
||||||
upIcon.className = 'icon-arrow-up-4';
|
|
||||||
upIcon.setAttribute('aria-hidden', 'true');
|
|
||||||
up.appendChild(upIcon);
|
|
||||||
up.appendChild(document.createTextNode(' ..'));
|
|
||||||
up.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
loadDir(data.parent);
|
|
||||||
});
|
|
||||||
list.appendChild(up);
|
|
||||||
}
|
|
||||||
|
|
||||||
(data.dirs || []).forEach(function(dir) {
|
|
||||||
var item = document.createElement('a');
|
|
||||||
item.href = '#';
|
|
||||||
item.className = 'list-group-item list-group-item-action py-1';
|
|
||||||
var icon = document.createElement('span');
|
|
||||||
icon.className = 'icon-folder';
|
|
||||||
icon.setAttribute('aria-hidden', 'true');
|
|
||||||
item.appendChild(icon);
|
|
||||||
item.appendChild(document.createTextNode(' ' + dir.name));
|
|
||||||
item.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
// Store with placeholders reversed back in
|
|
||||||
input.value = unresolve(dir.path);
|
|
||||||
loadDir(dir.path);
|
|
||||||
});
|
|
||||||
item.addEventListener('dblclick', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
input.value = unresolve(dir.path);
|
|
||||||
browser.style.display = 'none';
|
|
||||||
});
|
|
||||||
list.appendChild(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
tree.appendChild(list);
|
|
||||||
|
|
||||||
var info = document.createElement('div');
|
|
||||||
info.className = 'mt-2 p-1';
|
|
||||||
var small = document.createElement('small');
|
|
||||||
small.className = 'text-muted';
|
|
||||||
small.textContent = 'Current: ' + (data.current || path);
|
|
||||||
info.appendChild(small);
|
|
||||||
|
|
||||||
// Show what will be stored (with placeholders)
|
|
||||||
var stored = document.createElement('small');
|
|
||||||
stored.className = 'text-info d-block';
|
|
||||||
stored.textContent = 'Stored as: ' + unresolve(data.current || path);
|
|
||||||
info.appendChild(stored);
|
|
||||||
|
|
||||||
tree.appendChild(info);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
HTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +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\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
||||||
|
|
||||||
class DashboardModel extends BaseDatabaseModel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the most recent completed backup record.
|
|
||||||
*
|
|
||||||
* @return object|null
|
|
||||||
*/
|
|
||||||
public function getLastBackup(): ?object
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('r.*, p.title AS profile_title')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records', 'r'))
|
|
||||||
->join('LEFT', $db->quoteName('#__mokojoombackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
|
||||||
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
|
|
||||||
->order($db->quoteName('r.backupend') . ' DESC');
|
|
||||||
$db->setQuery($query, 0, 1);
|
|
||||||
|
|
||||||
return $db->loadObject() ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query com_scheduler for the next scheduled MokoJoomBackup task.
|
|
||||||
*
|
|
||||||
* @return object|null Object with next_execution and title, or null
|
|
||||||
*/
|
|
||||||
public function getNextScheduled(): ?object
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName(['t.next_execution', 't.title']))
|
|
||||||
->from($db->quoteName('#__scheduler_tasks', 't'))
|
|
||||||
->where($db->quoteName('t.type') . ' = ' . $db->quote('mokojoombackup.run_profile'))
|
|
||||||
->where($db->quoteName('t.state') . ' = 1')
|
|
||||||
->order($db->quoteName('t.next_execution') . ' ASC');
|
|
||||||
$db->setQuery($query, 0, 1);
|
|
||||||
|
|
||||||
return $db->loadObject() ?: null;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get backup statistics.
|
|
||||||
*
|
|
||||||
* @return object Object with total_count, total_size, fail_count_7d
|
|
||||||
*/
|
|
||||||
public function getStats(): object
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
|
|
||||||
// Total completed backups and storage
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('COUNT(*) AS total_count')
|
|
||||||
->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
|
||||||
$db->setQuery($query);
|
|
||||||
$stats = $db->loadObject();
|
|
||||||
|
|
||||||
// Failures in last 7 days
|
|
||||||
$cutoff = date('Y-m-d H:i:s', strtotime('-7 days'));
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('COUNT(*) AS fail_count')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('fail'))
|
|
||||||
->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff));
|
|
||||||
$db->setQuery($query);
|
|
||||||
$stats->fail_count_7d = (int) $db->loadResult();
|
|
||||||
|
|
||||||
return $stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check system health for backup readiness.
|
|
||||||
*
|
|
||||||
* @return array Array of check results [{label, status, detail}]
|
|
||||||
*/
|
|
||||||
public function getSystemHealth(): array
|
|
||||||
{
|
|
||||||
$checks = [];
|
|
||||||
|
|
||||||
// PHP version
|
|
||||||
$checks[] = (object) [
|
|
||||||
'label' => 'PHP Version',
|
|
||||||
'status' => version_compare(PHP_VERSION, '8.1.0', '>='),
|
|
||||||
'detail' => PHP_VERSION,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ZipArchive extension
|
|
||||||
$checks[] = (object) [
|
|
||||||
'label' => 'ZipArchive',
|
|
||||||
'status' => extension_loaded('zip'),
|
|
||||||
'detail' => extension_loaded('zip') ? 'Loaded' : 'Not loaded',
|
|
||||||
];
|
|
||||||
|
|
||||||
// AES-256 encryption support
|
|
||||||
$aesSupport = defined('ZipArchive::EM_AES_256');
|
|
||||||
$checks[] = (object) [
|
|
||||||
'label' => 'AES-256 Encryption',
|
|
||||||
'status' => $aesSupport,
|
|
||||||
'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Backup directory writable — check the default path
|
|
||||||
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
|
||||||
$backupDir = $defaultDir;
|
|
||||||
|
|
||||||
// If profiles use a custom directory, check that instead
|
|
||||||
$db2 = $this->getDatabase();
|
|
||||||
$qDir = $db2->getQuery(true)
|
|
||||||
->select($db2->quoteName('backup_dir'))
|
|
||||||
->from($db2->quoteName('#__mokojoombackup_profiles'))
|
|
||||||
->where($db2->quoteName('published') . ' = 1')
|
|
||||||
->where($db2->quoteName('backup_dir') . ' != ' . $db2->quote(''))
|
|
||||||
->where($db2->quoteName('backup_dir') . ' IS NOT NULL');
|
|
||||||
$db2->setQuery($qDir, 0, 1);
|
|
||||||
$profileDir = $db2->loadResult();
|
|
||||||
|
|
||||||
if ($profileDir) {
|
|
||||||
// Absolute paths used as-is, relative resolved from JPATH_ROOT
|
|
||||||
if ($profileDir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $profileDir)) {
|
|
||||||
$backupDir = rtrim($profileDir, '/\\');
|
|
||||||
} else {
|
|
||||||
$backupDir = JPATH_ROOT . '/' . $profileDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip filesystem check if path contains placeholders (resolved at backup time)
|
|
||||||
if (preg_match('/\[.+\]/', $backupDir)) {
|
|
||||||
$checks[] = (object) [
|
|
||||||
'label' => 'Backup Directory',
|
|
||||||
'status' => true,
|
|
||||||
'detail' => 'Uses placeholders (resolved at backup time) — ' . $backupDir,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$writable = is_dir($backupDir) && is_writable($backupDir);
|
|
||||||
$checks[] = (object) [
|
|
||||||
'label' => 'Backup Directory',
|
|
||||||
'status' => $writable,
|
|
||||||
'detail' => ($writable ? 'Writable' : 'Not writable or missing') . ' — ' . $backupDir,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disk space
|
|
||||||
$freeSpace = @disk_free_space($backupDir ?: JPATH_ROOT);
|
|
||||||
$freeGB = $freeSpace ? round($freeSpace / 1073741824, 1) : 0;
|
|
||||||
$checks[] = (object) [
|
|
||||||
'label' => 'Free Disk Space',
|
|
||||||
'status' => $freeGB >= 1.0,
|
|
||||||
'detail' => $freeGB . ' GB free',
|
|
||||||
];
|
|
||||||
|
|
||||||
return $checks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if any profiles use the default (web-root) backup directory.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isUsingDefaultBackupDir(): bool
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$default = 'administrator/components/com_mokojoombackup/backups';
|
|
||||||
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('COUNT(*)')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
|
||||||
->where($db->quoteName('published') . ' = 1')
|
|
||||||
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default)
|
|
||||||
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
|
||||||
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return (int) $db->loadResult() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get published backup profiles for the quick-action selector.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getProfiles(): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName(['id', 'title', 'backup_type']))
|
|
||||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
|
||||||
->where($db->quoteName('published') . ' = 1')
|
|
||||||
->order($db->quoteName('ordering') . ' ASC');
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +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\View\Backups;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
|
||||||
|
|
||||||
class HtmlView extends BaseHtmlView
|
|
||||||
{
|
|
||||||
protected $items;
|
|
||||||
protected $pagination;
|
|
||||||
protected $state;
|
|
||||||
public $filterForm;
|
|
||||||
public $activeFilters = [];
|
|
||||||
public $profiles = [];
|
|
||||||
|
|
||||||
public function display($tpl = null): void
|
|
||||||
{
|
|
||||||
$this->items = $this->get('Items');
|
|
||||||
$this->pagination = $this->get('Pagination');
|
|
||||||
$this->state = $this->get('State');
|
|
||||||
$this->filterForm = $this->get('FilterForm');
|
|
||||||
$this->activeFilters = $this->get('ActiveFilters');
|
|
||||||
|
|
||||||
// Load published profiles for the backup selector
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName(['id', 'title', 'backup_type']))
|
|
||||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
|
||||||
->where($db->quoteName('published') . ' = 1')
|
|
||||||
->order($db->quoteName('ordering') . ' ASC');
|
|
||||||
$db->setQuery($query);
|
|
||||||
$this->profiles = $db->loadObjectList() ?: [];
|
|
||||||
|
|
||||||
$this->checkUpdateSite();
|
|
||||||
$this->addToolbar();
|
|
||||||
|
|
||||||
parent::display($tpl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an info notice linking to the update site record so the user
|
|
||||||
* can configure their download key for automatic updates.
|
|
||||||
*/
|
|
||||||
protected function checkUpdateSite(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
|
|
||||||
// Find the update site linked to pkg_mokojoombackup
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select([
|
|
||||||
$db->quoteName('us.update_site_id'),
|
|
||||||
$db->quoteName('us.extra_query'),
|
|
||||||
])
|
|
||||||
->from($db->quoteName('#__update_sites', 'us'))
|
|
||||||
->join(
|
|
||||||
'INNER',
|
|
||||||
$db->quoteName('#__update_sites_extensions', 'use')
|
|
||||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
|
||||||
)
|
|
||||||
->join(
|
|
||||||
'INNER',
|
|
||||||
$db->quoteName('#__extensions', 'e')
|
|
||||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
|
||||||
)
|
|
||||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokojoombackup'))
|
|
||||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
|
||||||
->setLimit(1);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$site = $db->loadObject();
|
|
||||||
|
|
||||||
if (!$site) {
|
|
||||||
Factory::getApplication()->enqueueMessage(
|
|
||||||
Text::_('COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING'),
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
} elseif (empty($site->extra_query) || strpos($site->extra_query, 'dlid=') === false) {
|
|
||||||
// Update site exists but no download key configured
|
|
||||||
$editUrl = Route::_(
|
|
||||||
'index.php?option=com_installer&view=updatesites&filter[search]=mokojoombackup'
|
|
||||||
);
|
|
||||||
|
|
||||||
Factory::getApplication()->enqueueMessage(
|
|
||||||
Text::sprintf('COM_MOKOJOOMBACKUP_UPDATE_SITE_NOTICE', $editUrl),
|
|
||||||
'info'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// If key is present, show nothing — all good
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Non-critical — silently ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function addToolbar(): void
|
|
||||||
{
|
|
||||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
|
|
||||||
ToolbarHelper::custom('backups.start', 'download', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW', false);
|
|
||||||
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
|
|
||||||
ToolbarHelper::custom('backups.verify', 'shield', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_VERIFY', true);
|
|
||||||
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'backups.delete');
|
|
||||||
ToolbarHelper::preferences('com_mokojoombackup');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +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\View\Dashboard;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
|
||||||
|
|
||||||
class HtmlView extends BaseHtmlView
|
|
||||||
{
|
|
||||||
public ?object $lastBackup = null;
|
|
||||||
public ?object $nextScheduled = null;
|
|
||||||
public object $stats;
|
|
||||||
public array $systemHealth = [];
|
|
||||||
public array $profiles = [];
|
|
||||||
public bool $defaultDirWarning = false;
|
|
||||||
|
|
||||||
public function display($tpl = null): void
|
|
||||||
{
|
|
||||||
/** @var \Joomla\Component\MokoJoomBackup\Administrator\Model\DashboardModel $model */
|
|
||||||
$model = $this->getModel();
|
|
||||||
|
|
||||||
$this->lastBackup = $model->getLastBackup();
|
|
||||||
$this->nextScheduled = $model->getNextScheduled();
|
|
||||||
$this->stats = $model->getStats();
|
|
||||||
$this->systemHealth = $model->getSystemHealth();
|
|
||||||
$this->profiles = $model->getProfiles();
|
|
||||||
$this->defaultDirWarning = $model->isUsingDefaultBackupDir();
|
|
||||||
|
|
||||||
$this->addToolbar();
|
|
||||||
|
|
||||||
parent::display($tpl);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function addToolbar(): void
|
|
||||||
{
|
|
||||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_TITLE'), 'archive');
|
|
||||||
ToolbarHelper::preferences('com_mokojoombackup');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
|
|
||||||
$ajaxToken = Session::getFormToken();
|
|
||||||
$ajaxUrl = Route::_('index.php?option=com_mokojoombackup&format=json', false);
|
|
||||||
?>
|
|
||||||
<div class="main-card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2><?php echo $this->escape($this->item->description); ?></h2>
|
|
||||||
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_STATUS'); ?></th>
|
|
||||||
<td>
|
|
||||||
<?php
|
|
||||||
$statusClass = match ($this->item->status) {
|
|
||||||
'complete' => 'badge bg-success',
|
|
||||||
'running' => 'badge bg-info',
|
|
||||||
'fail' => 'badge bg-danger',
|
|
||||||
default => 'badge bg-secondary',
|
|
||||||
};
|
|
||||||
?>
|
|
||||||
<span class="<?php echo $statusClass; ?>"><?php echo $this->escape($this->item->status); ?></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_BACKUP_TYPE'); ?></th>
|
|
||||||
<td><?php echo $this->escape($this->item->backup_type); ?></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ORIGIN'); ?></th>
|
|
||||||
<td><?php echo $this->escape($this->item->origin); ?></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_SIZE'); ?></th>
|
|
||||||
<td>
|
|
||||||
<?php echo HTMLHelper::_('number.bytes', $this->item->total_size); ?>
|
|
||||||
<?php if ($this->item->db_size > 0) : ?>
|
|
||||||
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_DB_SIZE'); ?>: <?php echo HTMLHelper::_('number.bytes', $this->item->db_size); ?>)</small>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_START'); ?></th>
|
|
||||||
<td><?php echo HTMLHelper::_('date', $this->item->backupstart, Text::_('DATE_FORMAT_LC2')); ?></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_END'); ?></th>
|
|
||||||
<td><?php echo HTMLHelper::_('date', $this->item->backupend, Text::_('DATE_FORMAT_LC2')); ?></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ARCHIVE'); ?></th>
|
|
||||||
<td><code><?php echo $this->escape($this->item->archivename); ?></code></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_PATH'); ?></th>
|
|
||||||
<td><code><?php echo $this->escape($this->item->absolute_path); ?></code></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_FILES_COUNT'); ?></th>
|
|
||||||
<td><?php echo (int) $this->item->files_count; ?></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_TABLES_COUNT'); ?></th>
|
|
||||||
<td><?php echo (int) $this->item->tables_count; ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php if (!empty($this->item->checksum)) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_CHECKSUM'); ?></th>
|
|
||||||
<td><code class="font-monospace" style="font-size:0.85em;"><?php echo $this->escape($this->item->checksum); ?></code></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (!empty($this->item->remote_filename)) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_REMOTE'); ?></th>
|
|
||||||
<td><code><?php echo $this->escape($this->item->remote_filename); ?></code></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Backup Log -->
|
|
||||||
<h4 class="mt-4"><?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?></h4>
|
|
||||||
<div id="mb-detail-log" class="bg-light p-3 rounded" style="max-height:400px; overflow-y:auto;">
|
|
||||||
<pre id="mb-detail-log-body" style="white-space:pre-wrap; word-break:break-word; font-size:0.85rem; margin:0;">Loading...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var form = new URLSearchParams();
|
|
||||||
form.append('task', 'ajax.viewLog');
|
|
||||||
form.append('id', <?php echo (int) $this->item->id; ?>);
|
|
||||||
form.append(<?php echo json_encode($ajaxToken); ?>, '1');
|
|
||||||
|
|
||||||
fetch(<?php echo json_encode($ajaxUrl); ?>, {
|
|
||||||
method: 'POST',
|
|
||||||
body: form,
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
document.getElementById('mb-detail-log-body').textContent = data.error ? data.message : data.log;
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
document.getElementById('mb-detail-log-body').textContent = 'Error: ' + err.message;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -1,296 +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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
|
|
||||||
$ajaxToken = Session::getFormToken();
|
|
||||||
$ajaxUrl = Route::_('index.php?option=com_mokojoombackup&format=json', false);
|
|
||||||
?>
|
|
||||||
<?php if ($this->defaultDirWarning) : ?>
|
|
||||||
<div class="alert alert-warning d-flex align-items-center mb-3" role="alert">
|
|
||||||
<span class="icon-warning-circle fs-4 me-3" aria-hidden="true"></span>
|
|
||||||
<div>
|
|
||||||
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING_TITLE'); ?></strong><br>
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_DEFAULT_DIR_WARNING'); ?>
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_mokojoombackup&view=profiles'); ?>" class="alert-link">
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_SUBMENU_PROFILES'); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<!-- Row 1: Status Cards (clickable) -->
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<div class="card h-100 mb-tile" role="link" data-href="<?php echo $this->lastBackup ? Route::_('index.php?option=com_mokojoombackup&view=backup&id=' . $this->lastBackup->id) : Route::_('index.php?option=com_mokojoombackup&view=backups'); ?>">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="icon-database fs-1 text-primary" aria-hidden="true"></span>
|
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP'); ?></h5>
|
|
||||||
<?php if ($this->lastBackup) : ?>
|
|
||||||
<p class="card-text text-success fw-bold">
|
|
||||||
<?php echo HTMLHelper::_('date', $this->lastBackup->backupend, Text::_('DATE_FORMAT_LC4')); ?>
|
|
||||||
</p>
|
|
||||||
<small class="text-muted">
|
|
||||||
<?php echo $this->escape($this->lastBackup->profile_title); ?>
|
|
||||||
—
|
|
||||||
<?php echo HTMLHelper::_('number.bytes', $this->lastBackup->total_size); ?>
|
|
||||||
</small>
|
|
||||||
<?php else : ?>
|
|
||||||
<p class="card-text text-warning"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="icon-calendar fs-1 text-info" aria-hidden="true"></span>
|
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED'); ?></h5>
|
|
||||||
<?php if ($this->nextScheduled) : ?>
|
|
||||||
<p class="card-text fw-bold">
|
|
||||||
<?php echo HTMLHelper::_('date', $this->nextScheduled->next_execution, Text::_('DATE_FORMAT_LC4')); ?>
|
|
||||||
</p>
|
|
||||||
<small class="text-muted"><?php echo $this->escape($this->nextScheduled->title); ?></small>
|
|
||||||
<?php else : ?>
|
|
||||||
<p class="card-text text-muted"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_NO_SCHEDULED'); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_mokojoombackup&view=backups'); ?>">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="icon-copy fs-1 text-secondary" aria-hidden="true"></span>
|
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_TOTAL_BACKUPS'); ?></h5>
|
|
||||||
<p class="card-text fw-bold fs-3"><?php echo (int) $this->stats->total_count; ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_mokojoombackup&view=backups'); ?>">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="icon-folder-open fs-1 text-warning" aria-hidden="true"></span>
|
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE'); ?></h5>
|
|
||||||
<p class="card-text fw-bold fs-3">
|
|
||||||
<?php echo HTMLHelper::_('number.bytes', (int) $this->stats->total_size); ?>
|
|
||||||
</p>
|
|
||||||
<?php if ($this->stats->fail_count_7d > 0) : ?>
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<?php echo Text::sprintf('COM_MOKOJOOMBACKUP_DASHBOARD_FAILURES_7D', $this->stats->fail_count_7d); ?>
|
|
||||||
</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.mb-tile { cursor: pointer; transition: box-shadow 0.2s, transform 0.1s; }
|
|
||||||
.mb-tile:hover { box-shadow: 0 .5rem 1rem rgba(0,0,0,.15); transform: translateY(-2px); }
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
document.querySelectorAll('.mb-tile').forEach(function(tile) {
|
|
||||||
tile.addEventListener('click', function() { window.location.href = this.dataset.href; });
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Row 2: Quick Actions -->
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_QUICK_ACTIONS'); ?></h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<?php if (!empty($this->profiles)) : ?>
|
|
||||||
<div class="d-flex align-items-center gap-3 mb-3">
|
|
||||||
<select id="mb-profile-select" class="form-select" style="max-width:250px;">
|
|
||||||
<?php foreach ($this->profiles as $profile) : ?>
|
|
||||||
<option value="<?php echo (int) $profile->id; ?>">
|
|
||||||
<?php echo $this->escape($profile->title); ?>
|
|
||||||
(<?php echo $this->escape($profile->backup_type); ?>)
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<button type="button" class="btn btn-primary" onclick="window.mokojoombackupStart()">
|
|
||||||
<span class="icon-download" aria-hidden="true"></span>
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW'); ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="list-group">
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_mokojoombackup&view=backups'); ?>" class="list-group-item list-group-item-action">
|
|
||||||
<span class="icon-database" aria-hidden="true"></span>
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS'); ?>
|
|
||||||
</a>
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_mokojoombackup&view=profiles'); ?>" class="list-group-item list-group-item-action">
|
|
||||||
<span class="icon-cog" aria-hidden="true"></span>
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_SUBMENU_PROFILES'); ?>
|
|
||||||
</a>
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>" class="list-group-item list-group-item-action">
|
|
||||||
<span class="icon-calendar" aria-hidden="true"></span>
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_SCHEDULED_TASKS'); ?>
|
|
||||||
</a>
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_installer&view=updatesites'); ?>" class="list-group-item list-group-item-action">
|
|
||||||
<span class="icon-refresh" aria-hidden="true"></span>
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_UPDATE_SITE'); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Row 2 right: System Health -->
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_DASHBOARD_SYSTEM_HEALTH'); ?></h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<table class="table table-sm mb-0">
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($this->systemHealth as $check) : ?>
|
|
||||||
<tr>
|
|
||||||
<td class="w-1 text-center">
|
|
||||||
<?php if ($check->status) : ?>
|
|
||||||
<span class="icon-publish text-success" aria-hidden="true"></span>
|
|
||||||
<?php else : ?>
|
|
||||||
<span class="icon-unpublish text-danger" aria-hidden="true"></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td><?php echo $this->escape($check->label); ?></td>
|
|
||||||
<td class="text-muted"><?php echo $this->escape($check->detail); ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stepped Backup Modal (reused from backups view) -->
|
|
||||||
<div id="mokojoombackup-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:10000;">
|
|
||||||
<div style="max-width:500px; margin:10% auto; background:#fff; border-radius:8px; padding:2rem; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
|
||||||
<h3 id="mb-modal-title" style="margin:0 0 1rem;">Backup in Progress</h3>
|
|
||||||
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
|
|
||||||
<div id="mb-progress-bar" style="height:100%; background:#0d6efd; transition:width 0.3s; width:0%; display:flex; align-items:center; justify-content:center; color:#fff; font-size:0.8rem; font-weight:bold;">0%</div>
|
|
||||||
</div>
|
|
||||||
<p id="mb-status" style="color:#666; font-size:0.9rem; margin:0.5rem 0;">Initializing...</p>
|
|
||||||
<p id="mb-phase" style="color:#999; font-size:0.8rem; margin:0;">Phase: init</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
|
||||||
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
|
|
||||||
|
|
||||||
var backupRunning = false;
|
|
||||||
|
|
||||||
window.addEventListener('beforeunload', function(e) {
|
|
||||||
if (backupRunning) { e.preventDefault(); e.returnValue = ''; }
|
|
||||||
});
|
|
||||||
|
|
||||||
function showModal() {
|
|
||||||
backupRunning = true;
|
|
||||||
document.getElementById('mokojoombackup-modal').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideModal() {
|
|
||||||
backupRunning = false;
|
|
||||||
document.getElementById('mokojoombackup-modal').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateProgress(progress, message, phase) {
|
|
||||||
const bar = document.getElementById('mb-progress-bar');
|
|
||||||
bar.style.width = progress + '%';
|
|
||||||
bar.textContent = progress + '%';
|
|
||||||
document.getElementById('mb-status').textContent = message;
|
|
||||||
document.getElementById('mb-phase').textContent = 'Phase: ' + phase;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postAjax(params) {
|
|
||||||
const form = new URLSearchParams();
|
|
||||||
form.append(TOKEN_NAME, '1');
|
|
||||||
for (const [k, v] of Object.entries(params)) {
|
|
||||||
form.append(k, v);
|
|
||||||
}
|
|
||||||
const res = await fetch(AJAX_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
body: form,
|
|
||||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
||||||
});
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startSteppedBackup() {
|
|
||||||
const profileSelect = document.getElementById('mb-profile-select');
|
|
||||||
const profileId = profileSelect ? profileSelect.value : '1';
|
|
||||||
|
|
||||||
showModal();
|
|
||||||
updateProgress(0, 'Initializing backup...', 'init');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const initResult = await postAjax({
|
|
||||||
task: 'ajax.init',
|
|
||||||
profile_id: profileId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (initResult.error) {
|
|
||||||
updateProgress(0, 'ERROR: ' + initResult.message, 'failed');
|
|
||||||
setTimeout(hideModal, 3000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionId = initResult.session_id;
|
|
||||||
updateProgress(initResult.progress, initResult.message, initResult.phase);
|
|
||||||
|
|
||||||
let done = false;
|
|
||||||
while (!done) {
|
|
||||||
const stepResult = await postAjax({
|
|
||||||
task: 'ajax.step',
|
|
||||||
session_id: sessionId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (stepResult.error) {
|
|
||||||
updateProgress(0, 'ERROR: ' + stepResult.message, 'failed');
|
|
||||||
setTimeout(hideModal, 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgress(stepResult.progress, stepResult.message, stepResult.phase);
|
|
||||||
done = stepResult.done || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('mb-modal-title').textContent = 'Backup Complete';
|
|
||||||
setTimeout(function() {
|
|
||||||
hideModal();
|
|
||||||
location.reload();
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
updateProgress(0, 'ERROR: ' + err.message, 'failed');
|
|
||||||
setTimeout(hideModal, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.mokojoombackupStart = startSteppedBackup;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
; MokoJoomBackup — Actionlog Plugin language file (en-GB)
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Actionlog Plugin system language file (en-GB)
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
; MokoJoomBackup — Actionlog Plugin language file (en-US)
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_CREATED="User {username} created backup profile "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_UPDATED="User {username} updated backup profile "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED="User {username} deleted backup profile "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_RECORD_DELETED="User {username} deleted backup record "{title}" (ID: {id})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_BACKUP_COMPLETE="Backup completed: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_BACKUP_FAILED="Backup FAILED: "{title}" (ID: {id}, profile: {profile_id}, origin: {origin})"
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Actionlog Plugin system language file (en-US)
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP="Action Log - MokoJoomBackup"
|
|
||||||
PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION="Logs MokoJoomBackup actions (backup, restore, profile changes) to User Action Logs."
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_actionlog_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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_actionlog_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
|
|
||||||
-->
|
|
||||||
<extension type="plugin" group="actionlog" method="upgrade">
|
|
||||||
<name>plg_actionlog_mokojoombackup</name>
|
|
||||||
<version>01.01.21-dev</version>
|
|
||||||
<creationDate>2026-06-04</creationDate>
|
|
||||||
<author>Moko Consulting</author>
|
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
|
||||||
<license>GPL-3.0-or-later</license>
|
|
||||||
<description>PLG_ACTIONLOG_MOKOJOOMBACKUP_DESCRIPTION</description>
|
|
||||||
|
|
||||||
<namespace path="src">Joomla\Plugin\Actionlog\MokoJoomBackup</namespace>
|
|
||||||
|
|
||||||
<files>
|
|
||||||
<filename plugin="mokojoombackup">mokojoombackup.php</filename>
|
|
||||||
<folder>services</folder>
|
|
||||||
<folder>src</folder>
|
|
||||||
</files>
|
|
||||||
|
|
||||||
<languages>
|
|
||||||
<language tag="en-GB">language/en-GB/plg_actionlog_mokojoombackup.ini</language>
|
|
||||||
<language tag="en-GB">language/en-GB/plg_actionlog_mokojoombackup.sys.ini</language>
|
|
||||||
</languages>
|
|
||||||
</extension>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_actionlog_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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Extension\PluginInterface;
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Plugin\PluginHelper;
|
|
||||||
use Joomla\DI\Container;
|
|
||||||
use Joomla\DI\ServiceProviderInterface;
|
|
||||||
use Joomla\Event\DispatcherInterface;
|
|
||||||
use Joomla\Plugin\Actionlog\MokoJoomBackup\Extension\MokoJoomBackupActionlog;
|
|
||||||
|
|
||||||
return new class () implements ServiceProviderInterface {
|
|
||||||
public function register(Container $container): void
|
|
||||||
{
|
|
||||||
$container->set(
|
|
||||||
PluginInterface::class,
|
|
||||||
function (Container $container) {
|
|
||||||
$plugin = new MokoJoomBackupActionlog(
|
|
||||||
$container->get(DispatcherInterface::class),
|
|
||||||
(array) PluginHelper::getPlugin('actionlog', 'mokojoombackup')
|
|
||||||
);
|
|
||||||
$plugin->setApplication(Factory::getApplication());
|
|
||||||
|
|
||||||
return $plugin;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
-173
@@ -1,173 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_actionlog_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\Plugin\Actionlog\MokoJoomBackup\Extension;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Event\Model;
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
|
||||||
use Joomla\Event\Event;
|
|
||||||
use Joomla\Event\SubscriberInterface;
|
|
||||||
|
|
||||||
final class MokoJoomBackupActionlog extends CMSPlugin implements SubscriberInterface
|
|
||||||
{
|
|
||||||
protected $autoloadLanguage = true;
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onContentAfterSave' => 'onContentAfterSave',
|
|
||||||
'onContentAfterDelete' => 'onContentAfterDelete',
|
|
||||||
'onMokoJoomBackupAfterRun' => 'onMokoJoomBackupAfterRun',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log when a backup profile is saved (created or updated).
|
|
||||||
*/
|
|
||||||
public function onContentAfterSave(Event $event): void
|
|
||||||
{
|
|
||||||
[$context, $table, $isNew] = array_values($event->getArguments());
|
|
||||||
|
|
||||||
if ($context !== 'com_mokojoombackup.profile') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messageKey = $isNew
|
|
||||||
? 'PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_CREATED'
|
|
||||||
: 'PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_UPDATED';
|
|
||||||
|
|
||||||
$this->addLog(
|
|
||||||
[
|
|
||||||
$messageKey,
|
|
||||||
'id' => $table->id,
|
|
||||||
'title' => $table->title,
|
|
||||||
'userid' => $this->getCurrentUserId(),
|
|
||||||
'username' => $this->getCurrentUserName(),
|
|
||||||
],
|
|
||||||
$messageKey,
|
|
||||||
'com_mokojoombackup.profile',
|
|
||||||
$this->getCurrentUserId()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log when a backup profile or record is deleted.
|
|
||||||
*/
|
|
||||||
public function onContentAfterDelete(Event $event): void
|
|
||||||
{
|
|
||||||
[$context, $table] = array_values($event->getArguments());
|
|
||||||
|
|
||||||
if ($context === 'com_mokojoombackup.profile') {
|
|
||||||
$this->addLog(
|
|
||||||
[
|
|
||||||
'PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED',
|
|
||||||
'id' => $table->id,
|
|
||||||
'title' => $table->title ?? '',
|
|
||||||
'userid' => $this->getCurrentUserId(),
|
|
||||||
'username' => $this->getCurrentUserName(),
|
|
||||||
],
|
|
||||||
'PLG_ACTIONLOG_MOKOJOOMBACKUP_PROFILE_DELETED',
|
|
||||||
'com_mokojoombackup.profile',
|
|
||||||
$this->getCurrentUserId()
|
|
||||||
);
|
|
||||||
} elseif ($context === 'com_mokojoombackup.backup') {
|
|
||||||
$this->addLog(
|
|
||||||
[
|
|
||||||
'PLG_ACTIONLOG_MOKOJOOMBACKUP_RECORD_DELETED',
|
|
||||||
'id' => $table->id,
|
|
||||||
'title' => $table->description ?? 'Backup #' . $table->id,
|
|
||||||
'userid' => $this->getCurrentUserId(),
|
|
||||||
'username' => $this->getCurrentUserName(),
|
|
||||||
],
|
|
||||||
'PLG_ACTIONLOG_MOKOJOOMBACKUP_RECORD_DELETED',
|
|
||||||
'com_mokojoombackup.backup',
|
|
||||||
$this->getCurrentUserId()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log when a backup completes or fails.
|
|
||||||
* This event should be dispatched from BackupEngine.
|
|
||||||
*/
|
|
||||||
public function onMokoJoomBackupAfterRun(Event $event): void
|
|
||||||
{
|
|
||||||
$args = $event->getArguments();
|
|
||||||
|
|
||||||
$success = $args['success'] ?? false;
|
|
||||||
$recordId = $args['record_id'] ?? 0;
|
|
||||||
$description = $args['description'] ?? '';
|
|
||||||
$profileId = $args['profile_id'] ?? 0;
|
|
||||||
$origin = $args['origin'] ?? 'backend';
|
|
||||||
|
|
||||||
$messageKey = $success
|
|
||||||
? 'PLG_ACTIONLOG_MOKOJOOMBACKUP_BACKUP_COMPLETE'
|
|
||||||
: 'PLG_ACTIONLOG_MOKOJOOMBACKUP_BACKUP_FAILED';
|
|
||||||
|
|
||||||
$this->addLog(
|
|
||||||
[
|
|
||||||
$messageKey,
|
|
||||||
'id' => $recordId,
|
|
||||||
'title' => $description ?: 'Backup #' . $recordId,
|
|
||||||
'profile_id' => $profileId,
|
|
||||||
'origin' => $origin,
|
|
||||||
'userid' => $this->getCurrentUserId(),
|
|
||||||
'username' => $this->getCurrentUserName(),
|
|
||||||
],
|
|
||||||
$messageKey,
|
|
||||||
'com_mokojoombackup.backup',
|
|
||||||
$this->getCurrentUserId()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write an action log entry.
|
|
||||||
*/
|
|
||||||
private function addLog(array $message, string $messageLanguageKey, string $context, int $userId): void
|
|
||||||
{
|
|
||||||
$params = [
|
|
||||||
'message_language_key' => $messageLanguageKey,
|
|
||||||
'message' => json_encode($message),
|
|
||||||
'date' => date('Y-m-d H:i:s'),
|
|
||||||
'extension' => 'com_mokojoombackup',
|
|
||||||
'user_id' => $userId,
|
|
||||||
'ip_address' => Factory::getApplication()->input->server->getString('REMOTE_ADDR', ''),
|
|
||||||
'item_id' => $message['id'] ?? 0,
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$db->insertObject('#__action_logs', (object) $params);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Non-critical — don't break the operation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getCurrentUserId(): int
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return (int) Factory::getApplication()->getIdentity()->id;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getCurrentUserName(): string
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return Factory::getApplication()->getIdentity()->username ?: 'system';
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
return 'system';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Console Plugin language file (en-GB)
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup"
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Console Plugin system language file (en-GB)
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup"
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Console Plugin language file (en-US)
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup"
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Console Plugin system language file (en-US)
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP="Console - MokoJoomBackup"
|
|
||||||
PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION="CLI commands for MokoJoomBackup: run, list, profiles, restore, cleanup."
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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
|
|
||||||
-->
|
|
||||||
<extension type="plugin" group="console" method="upgrade">
|
|
||||||
<name>plg_console_mokojoombackup</name>
|
|
||||||
<version>01.01.21-dev</version>
|
|
||||||
<creationDate>2026-06-04</creationDate>
|
|
||||||
<author>Moko Consulting</author>
|
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
|
||||||
<license>GPL-3.0-or-later</license>
|
|
||||||
<description>PLG_CONSOLE_MOKOJOOMBACKUP_DESCRIPTION</description>
|
|
||||||
|
|
||||||
<namespace path="src">Joomla\Plugin\Console\MokoJoomBackup</namespace>
|
|
||||||
|
|
||||||
<files>
|
|
||||||
<filename plugin="mokojoombackup">mokojoombackup.php</filename>
|
|
||||||
<folder>services</folder>
|
|
||||||
<folder>src</folder>
|
|
||||||
</files>
|
|
||||||
|
|
||||||
<languages>
|
|
||||||
<language tag="en-GB">language/en-GB/plg_console_mokojoombackup.ini</language>
|
|
||||||
<language tag="en-GB">language/en-GB/plg_console_mokojoombackup.sys.ini</language>
|
|
||||||
</languages>
|
|
||||||
</extension>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Extension\PluginInterface;
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Plugin\PluginHelper;
|
|
||||||
use Joomla\DI\Container;
|
|
||||||
use Joomla\DI\ServiceProviderInterface;
|
|
||||||
use Joomla\Event\DispatcherInterface;
|
|
||||||
use Joomla\Plugin\Console\MokoJoomBackup\Extension\MokoJoomBackupConsole;
|
|
||||||
|
|
||||||
return new class () implements ServiceProviderInterface {
|
|
||||||
public function register(Container $container): void
|
|
||||||
{
|
|
||||||
$container->set(
|
|
||||||
PluginInterface::class,
|
|
||||||
function (Container $container) {
|
|
||||||
$plugin = new MokoJoomBackupConsole(
|
|
||||||
$container->get(DispatcherInterface::class),
|
|
||||||
(array) PluginHelper::getPlugin('console', 'mokojoombackup')
|
|
||||||
);
|
|
||||||
$plugin->setApplication(Factory::getApplication());
|
|
||||||
|
|
||||||
return $plugin;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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\Plugin\Console\MokoJoomBackup\Command;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\Console\Command\AbstractCommand;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class CleanupCommand extends AbstractCommand
|
|
||||||
{
|
|
||||||
protected static $defaultName = 'mokojoombackup:cleanup';
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Clean up old backup records and archive files');
|
|
||||||
$this->addOption('max-age', null, InputOption::VALUE_REQUIRED, 'Max age in days', '30');
|
|
||||||
$this->addOption('max-count', null, InputOption::VALUE_REQUIRED, 'Max number of backups to keep', '10');
|
|
||||||
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be deleted without deleting');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$maxAge = (int) $input->getOption('max-age');
|
|
||||||
$maxCount = (int) $input->getOption('max-count');
|
|
||||||
$dryRun = $input->getOption('dry-run');
|
|
||||||
|
|
||||||
$io->title('MokoJoomBackup — Cleanup');
|
|
||||||
|
|
||||||
if ($dryRun) {
|
|
||||||
$io->note('Dry run — no files will be deleted.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$deleted = 0;
|
|
||||||
|
|
||||||
// Delete by age
|
|
||||||
$cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days"));
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('id, absolute_path, description, backupstart')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
|
||||||
$db->setQuery($query);
|
|
||||||
$expired = $db->loadObjectList();
|
|
||||||
|
|
||||||
foreach ($expired as $record) {
|
|
||||||
$io->text('Expired: #' . $record->id . ' — ' . $record->backupstart . ' — ' . ($record->description ?: 'no description'));
|
|
||||||
|
|
||||||
if (!$dryRun) {
|
|
||||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
|
||||||
@unlink($record->absolute_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
|
||||||
);
|
|
||||||
$db->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
$deleted++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce max count
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('COUNT(*)')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
|
||||||
$db->setQuery($query);
|
|
||||||
$totalCount = (int) $db->loadResult();
|
|
||||||
|
|
||||||
if ($totalCount > $maxCount) {
|
|
||||||
$excess = $totalCount - $maxCount;
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('id, absolute_path, description, backupstart')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
|
||||||
->order($db->quoteName('backupstart') . ' ASC');
|
|
||||||
$db->setQuery($query, 0, $excess);
|
|
||||||
$oldest = $db->loadObjectList();
|
|
||||||
|
|
||||||
foreach ($oldest as $record) {
|
|
||||||
$io->text('Over limit: #' . $record->id . ' — ' . $record->backupstart);
|
|
||||||
|
|
||||||
if (!$dryRun) {
|
|
||||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
|
||||||
@unlink($record->absolute_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
|
||||||
);
|
|
||||||
$db->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
$deleted++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($deleted === 0) {
|
|
||||||
$io->success('No backups to clean up.');
|
|
||||||
} else {
|
|
||||||
$io->success(($dryRun ? 'Would delete ' : 'Deleted ') . $deleted . ' backup record(s).');
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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\Plugin\Console\MokoJoomBackup\Command;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\Console\Command\AbstractCommand;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class ListCommand extends AbstractCommand
|
|
||||||
{
|
|
||||||
protected static $defaultName = 'mokojoombackup:list';
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('List backup records');
|
|
||||||
$this->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Number of records to show', '20');
|
|
||||||
$this->addOption('status', 's', InputOption::VALUE_OPTIONAL, 'Filter by status (complete, fail, running)');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$limit = (int) $input->getOption('limit');
|
|
||||||
$status = $input->getOption('status');
|
|
||||||
|
|
||||||
$io->title('MokoJoomBackup — Backup Records');
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('r.id, r.description, r.status, r.origin, r.backup_type, r.total_size, r.backupstart, r.backupend')
|
|
||||||
->select($db->quoteName('p.title', 'profile_title'))
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records', 'r'))
|
|
||||||
->join('LEFT', $db->quoteName('#__mokojoombackup_profiles', 'p') . ' ON p.id = r.profile_id')
|
|
||||||
->order($db->quoteName('r.backupstart') . ' DESC');
|
|
||||||
|
|
||||||
if ($status) {
|
|
||||||
$query->where($db->quoteName('r.status') . ' = ' . $db->quote($status));
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, $limit);
|
|
||||||
$records = $db->loadObjectList();
|
|
||||||
|
|
||||||
if (empty($records)) {
|
|
||||||
$io->info('No backup records found.');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
|
|
||||||
foreach ($records as $record) {
|
|
||||||
$size = $record->total_size > 0
|
|
||||||
? round($record->total_size / 1048576, 2) . ' MB'
|
|
||||||
: '—';
|
|
||||||
|
|
||||||
$rows[] = [
|
|
||||||
$record->id,
|
|
||||||
$record->profile_title ?: '—',
|
|
||||||
$record->status,
|
|
||||||
$record->backup_type,
|
|
||||||
$size,
|
|
||||||
$record->origin,
|
|
||||||
$record->backupstart,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->table(
|
|
||||||
['ID', 'Profile', 'Status', 'Type', 'Size', 'Origin', 'Started'],
|
|
||||||
$rows
|
|
||||||
);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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\Plugin\Console\MokoJoomBackup\Command;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\Console\Command\AbstractCommand;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class ProfilesCommand extends AbstractCommand
|
|
||||||
{
|
|
||||||
protected static $defaultName = 'mokojoombackup:profiles';
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('List available backup profiles');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
|
|
||||||
$io->title('MokoJoomBackup — Backup Profiles');
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('id, title, backup_type, published, ordering')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
|
||||||
->order($db->quoteName('ordering') . ' ASC');
|
|
||||||
$db->setQuery($query);
|
|
||||||
$profiles = $db->loadObjectList();
|
|
||||||
|
|
||||||
if (empty($profiles)) {
|
|
||||||
$io->info('No backup profiles found.');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rows = [];
|
|
||||||
|
|
||||||
foreach ($profiles as $profile) {
|
|
||||||
$rows[] = [
|
|
||||||
$profile->id,
|
|
||||||
$profile->title,
|
|
||||||
$profile->backup_type,
|
|
||||||
$profile->published ? 'Yes' : 'No',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->table(
|
|
||||||
['ID', 'Title', 'Type', 'Published'],
|
|
||||||
$rows
|
|
||||||
);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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\Plugin\Console\MokoJoomBackup\Command;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\RestoreEngine;
|
|
||||||
use Joomla\Console\Command\AbstractCommand;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class RestoreCommand extends AbstractCommand
|
|
||||||
{
|
|
||||||
protected static $defaultName = 'mokojoombackup:restore';
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Restore a backup by record ID');
|
|
||||||
$this->addArgument('id', InputArgument::REQUIRED, 'Backup record ID to restore');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$recordId = (int) $input->getArgument('id');
|
|
||||||
|
|
||||||
$io->title('MokoJoomBackup — Restore Backup');
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('*')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('id') . ' = ' . $recordId);
|
|
||||||
$db->setQuery($query);
|
|
||||||
$record = $db->loadObject();
|
|
||||||
|
|
||||||
if (!$record) {
|
|
||||||
$io->error('Backup record not found: ' . $recordId);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($record->status !== 'complete') {
|
|
||||||
$io->error('Cannot restore — backup status is: ' . $record->status);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($record->absolute_path) || !is_file($record->absolute_path)) {
|
|
||||||
$io->error('Backup archive not found: ' . ($record->absolute_path ?: 'no path'));
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->warning('This will overwrite the current site files and/or database.');
|
|
||||||
$io->text('Archive: ' . $record->absolute_path);
|
|
||||||
$io->text('Type: ' . $record->backup_type);
|
|
||||||
|
|
||||||
if (!$io->confirm('Are you sure you want to continue?', false)) {
|
|
||||||
$io->info('Restore cancelled.');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/src/Engine/RestoreEngine.php';
|
|
||||||
|
|
||||||
if (!file_exists($engineFile)) {
|
|
||||||
$io->error('RestoreEngine not found. Is the component fully installed?');
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists(RestoreEngine::class)) {
|
|
||||||
require_once $engineFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
$engine = new RestoreEngine();
|
|
||||||
$result = $engine->restore($record->absolute_path, $record->backup_type);
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$io->success($result['message']);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->error($result['message']);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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\Plugin\Console\MokoJoomBackup\Command;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine;
|
|
||||||
use Joomla\Console\Command\AbstractCommand;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class RunCommand extends AbstractCommand
|
|
||||||
{
|
|
||||||
protected static $defaultName = 'mokojoombackup:run';
|
|
||||||
|
|
||||||
protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->setDescription('Run a backup using a specified profile');
|
|
||||||
$this->addOption('profile', 'p', InputOption::VALUE_REQUIRED, 'Profile ID to use', '1');
|
|
||||||
$this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'Backup description', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function doExecute(InputInterface $input, OutputInterface $output): int
|
|
||||||
{
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$profileId = (int) $input->getOption('profile');
|
|
||||||
$desc = $input->getOption('description') ?: '';
|
|
||||||
|
|
||||||
$io->title('MokoJoomBackup — Run Backup');
|
|
||||||
$io->text('Profile ID: ' . $profileId);
|
|
||||||
|
|
||||||
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/src/Engine/BackupEngine.php';
|
|
||||||
|
|
||||||
if (!file_exists($engineFile)) {
|
|
||||||
$io->error('MokoJoomBackup component not installed.');
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists(BackupEngine::class)) {
|
|
||||||
require_once $engineFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
$engine = new BackupEngine();
|
|
||||||
$result = $engine->run($profileId, $desc ?: 'CLI backup', 'cli');
|
|
||||||
|
|
||||||
if ($result['success']) {
|
|
||||||
$io->success($result['message']);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io->error($result['message']);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_console_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\Plugin\Console\MokoJoomBackup\Extension;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
|
||||||
use Joomla\Event\Event;
|
|
||||||
use Joomla\Event\SubscriberInterface;
|
|
||||||
use Joomla\Plugin\Console\MokoJoomBackup\Command\CleanupCommand;
|
|
||||||
use Joomla\Plugin\Console\MokoJoomBackup\Command\ListCommand;
|
|
||||||
use Joomla\Plugin\Console\MokoJoomBackup\Command\ProfilesCommand;
|
|
||||||
use Joomla\Plugin\Console\MokoJoomBackup\Command\RestoreCommand;
|
|
||||||
use Joomla\Plugin\Console\MokoJoomBackup\Command\RunCommand;
|
|
||||||
|
|
||||||
final class MokoJoomBackupConsole extends CMSPlugin implements SubscriberInterface
|
|
||||||
{
|
|
||||||
protected $autoloadLanguage = true;
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
\Joomla\Application\ApplicationEvents::BEFORE_EXECUTE => 'registerCommands',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function registerCommands(Event $event): void
|
|
||||||
{
|
|
||||||
$app = $this->getApplication();
|
|
||||||
|
|
||||||
$app->addCommand(new RunCommand());
|
|
||||||
$app->addCommand(new ListCommand());
|
|
||||||
$app->addCommand(new ProfilesCommand());
|
|
||||||
$app->addCommand(new RestoreCommand());
|
|
||||||
$app->addCommand(new CleanupCommand());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
; MokoJoomBackup — Content Plugin language file (en-GB)
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed."
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated."
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Content Plugin system language file (en-GB)
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
; MokoJoomBackup — Content Plugin language file (en-US)
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL="Backup Before Install"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL_DESC="Run an automatic backup before a new extension is installed."
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_UPDATE="Backup Before Update"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_UPDATE_DESC="Run an automatic backup before an extension is updated."
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Which backup profile to use for automatic backups."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Content Plugin system language file (en-US)
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP="Content - MokoJoomBackup"
|
|
||||||
PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION="Automatically triggers a backup before extension installs or updates."
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_content_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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!--
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_content_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
|
|
||||||
-->
|
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
|
||||||
<name>plg_content_mokojoombackup</name>
|
|
||||||
<version>01.01.21-dev</version>
|
|
||||||
<creationDate>2026-06-04</creationDate>
|
|
||||||
<author>Moko Consulting</author>
|
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
|
||||||
<license>GPL-3.0-or-later</license>
|
|
||||||
<description>PLG_CONTENT_MOKOJOOMBACKUP_DESCRIPTION</description>
|
|
||||||
|
|
||||||
<namespace path="src">Joomla\Plugin\Content\MokoJoomBackup</namespace>
|
|
||||||
|
|
||||||
<files>
|
|
||||||
<filename plugin="mokojoombackup">mokojoombackup.php</filename>
|
|
||||||
<folder>services</folder>
|
|
||||||
<folder>src</folder>
|
|
||||||
</files>
|
|
||||||
|
|
||||||
<languages>
|
|
||||||
<language tag="en-GB">language/en-GB/plg_content_mokojoombackup.ini</language>
|
|
||||||
<language tag="en-GB">language/en-GB/plg_content_mokojoombackup.sys.ini</language>
|
|
||||||
</languages>
|
|
||||||
|
|
||||||
<config>
|
|
||||||
<fields name="params">
|
|
||||||
<fieldset name="basic">
|
|
||||||
<field
|
|
||||||
name="backup_before_install"
|
|
||||||
type="radio"
|
|
||||||
label="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL"
|
|
||||||
description="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_INSTALL_DESC"
|
|
||||||
default="0"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="backup_before_update"
|
|
||||||
type="radio"
|
|
||||||
label="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_UPDATE"
|
|
||||||
description="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_BEFORE_UPDATE_DESC"
|
|
||||||
default="1"
|
|
||||||
class="btn-group"
|
|
||||||
>
|
|
||||||
<option value="1">JYES</option>
|
|
||||||
<option value="0">JNO</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="profile_id"
|
|
||||||
type="sql"
|
|
||||||
label="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE"
|
|
||||||
description="PLG_CONTENT_MOKOJOOMBACKUP_FIELD_PROFILE_DESC"
|
|
||||||
query="SELECT id AS value, title AS text FROM #__mokojoombackup_profiles WHERE published = 1 ORDER BY ordering ASC"
|
|
||||||
default="1"
|
|
||||||
>
|
|
||||||
<option value="1">Default Backup Profile</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
</fields>
|
|
||||||
</config>
|
|
||||||
</extension>
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_content_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\Plugin\Content\MokoJoomBackup\Extension;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
|
||||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine;
|
|
||||||
use Joomla\Event\Event;
|
|
||||||
use Joomla\Event\SubscriberInterface;
|
|
||||||
|
|
||||||
final class MokoJoomBackupContent extends CMSPlugin implements SubscriberInterface
|
|
||||||
{
|
|
||||||
protected $autoloadLanguage = true;
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onExtensionBeforeInstall' => 'onExtensionBeforeInstall',
|
|
||||||
'onExtensionBeforeUpdate' => 'onExtensionBeforeUpdate',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger a backup before a new extension is installed.
|
|
||||||
*/
|
|
||||||
public function onExtensionBeforeInstall(Event $event): void
|
|
||||||
{
|
|
||||||
if (!(int) $this->params->get('backup_before_install', 0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->triggerAutoBackup('Pre-install backup');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger a backup before an extension is updated.
|
|
||||||
*/
|
|
||||||
public function onExtensionBeforeUpdate(Event $event): void
|
|
||||||
{
|
|
||||||
if (!(int) $this->params->get('backup_before_update', 1)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->triggerAutoBackup('Pre-update backup');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a backup using the configured profile.
|
|
||||||
*/
|
|
||||||
private function triggerAutoBackup(string $description): void
|
|
||||||
{
|
|
||||||
$profileId = (int) $this->params->get('profile_id', 1);
|
|
||||||
|
|
||||||
// Throttle: only one auto-backup per hour via session
|
|
||||||
$session = Factory::getSession();
|
|
||||||
$lastRun = $session->get('mokojoombackup.content_last_autobackup', 0);
|
|
||||||
|
|
||||||
if (time() - $lastRun < 3600) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$session->set('mokojoombackup.content_last_autobackup', time());
|
|
||||||
|
|
||||||
$engineFile = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/src/Engine/BackupEngine.php';
|
|
||||||
|
|
||||||
if (!file_exists($engineFile)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!class_exists(BackupEngine::class)) {
|
|
||||||
require_once $engineFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$engine = new BackupEngine();
|
|
||||||
$engine->run($profileId, $description, 'backend');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Non-fatal — log and continue with the install/update
|
|
||||||
Factory::getApplication()->enqueueMessage(
|
|
||||||
'MokoJoomBackup auto-backup failed: ' . $e->getMessage(),
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard."
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_OK="Backups: OK"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_NO_BACKUPS="Backups: No backups yet!"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_FAILURES="Backups: Recent failures!"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_STALE="Backups: Last backup > 7 days ago"
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard."
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard."
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_OK="Backups: OK"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_NO_BACKUPS="Backups: No backups yet!"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_FAILURES="Backups: Recent failures!"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_STALE="Backups: Last backup > 7 days ago"
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
PLG_QUICKICON_MOKOJOOMBACKUP="Quick Icon - MokoJoomBackup"
|
|
||||||
PLG_QUICKICON_MOKOJOOMBACKUP_DESCRIPTION="Shows backup status on the administrator dashboard."
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
; MokoJoomBackup — System Plugin language file (en-GB)
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers."
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP="Auto Cleanup"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP_DESC="Automatically remove old backup archives based on age and count limits."
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE="Max Backup Age (days)"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE_DESC="Delete backup records older than this many days."
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS="Max Backup Count"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS_DESC="Keep at most this many completed backup records."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — System Plugin system language file (en-GB)
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers."
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
; MokoJoomBackup — System Plugin language file (en-US)
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers."
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP="Auto Cleanup"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_AUTO_CLEANUP_DESC="Automatically remove old backup archives based on age and count limits."
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE="Max Backup Age (days)"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_AGE_DESC="Delete backup records older than this many days."
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS="Max Backup Count"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_FIELD_MAX_BACKUPS_DESC="Keep at most this many completed backup records."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — System Plugin system language file (en-US)
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP="System - MokoJoomBackup"
|
|
||||||
PLG_SYSTEM_MOKOJOOMBACKUP_DESCRIPTION="Automatic cleanup of expired backup archives and scheduled backup triggers."
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_system_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\Plugin\System\MokoJoomBackup\Extension;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Component\ComponentHelper;
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
|
||||||
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine;
|
|
||||||
use Joomla\Event\Event;
|
|
||||||
use Joomla\Event\SubscriberInterface;
|
|
||||||
|
|
||||||
final class MokoJoomBackup extends CMSPlugin implements SubscriberInterface
|
|
||||||
{
|
|
||||||
protected $autoloadLanguage = true;
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onAfterInitialise' => 'onAfterInitialise',
|
|
||||||
'onAfterRoute' => 'onAfterRoute',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Web cron trigger — runs before routing so no authentication is needed.
|
|
||||||
*
|
|
||||||
* URL: index.php?mokojoombackup_cron=SECRET&profile_id=1
|
|
||||||
*
|
|
||||||
* External cron services (cron-job.org, UptimeRobot, etc.) can call this
|
|
||||||
* URL on a schedule to trigger backups on shared hosting without crontab.
|
|
||||||
*/
|
|
||||||
public function onAfterInitialise(Event $event): void
|
|
||||||
{
|
|
||||||
$app = $this->getApplication();
|
|
||||||
$secret = $app->input->getString('mokojoombackup_cron', '');
|
|
||||||
|
|
||||||
if ($secret === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load component params
|
|
||||||
$params = ComponentHelper::getParams('com_mokojoombackup');
|
|
||||||
$enabled = (int) $params->get('webcron_enabled', 0);
|
|
||||||
$configSecret = trim($params->get('webcron_secret', ''));
|
|
||||||
$ipWhitelist = trim($params->get('webcron_ip_whitelist', ''));
|
|
||||||
|
|
||||||
// Reject if disabled or no secret configured
|
|
||||||
if (!$enabled || $configSecret === '') {
|
|
||||||
$this->sendJsonResponse(false, 'Web cron is not enabled', 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate secret (timing-safe comparison)
|
|
||||||
if (!hash_equals($configSecret, $secret)) {
|
|
||||||
$this->sendJsonResponse(false, 'Invalid secret', 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP whitelist check (if configured)
|
|
||||||
if ($ipWhitelist !== '') {
|
|
||||||
$allowedIps = array_map('trim', explode(',', $ipWhitelist));
|
|
||||||
$clientIp = $app->input->server->getString('REMOTE_ADDR', '');
|
|
||||||
|
|
||||||
if (!in_array($clientIp, $allowedIps, true)) {
|
|
||||||
$this->sendJsonResponse(false, 'IP not allowed', 403);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine profile
|
|
||||||
$profileId = $app->input->getInt('profile_id', (int) $params->get('default_profile', 1));
|
|
||||||
|
|
||||||
// Override PHP limits
|
|
||||||
@set_time_limit(0);
|
|
||||||
@ini_set('max_execution_time', '0');
|
|
||||||
@ini_set('memory_limit', '512M');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$engine = new BackupEngine();
|
|
||||||
$result = $engine->run($profileId, 'Web cron backup', 'webcron');
|
|
||||||
|
|
||||||
$this->sendJsonResponse(
|
|
||||||
$result['success'],
|
|
||||||
$result['message'],
|
|
||||||
$result['success'] ? 200 : 500
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$this->sendJsonResponse(false, 'Backup failed: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup expired backups on admin page loads (lightweight check).
|
|
||||||
*/
|
|
||||||
public function onAfterRoute(Event $event): void
|
|
||||||
{
|
|
||||||
$app = $this->getApplication();
|
|
||||||
|
|
||||||
// Skip if this is a web cron request (already handled)
|
|
||||||
if ($app->input->getString('mokojoombackup_cron', '') !== '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only run in admin, and only on component page loads (not AJAX)
|
|
||||||
if (!$app->isClient('administrator') || $app->input->getCmd('format', 'html') !== 'html') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(int) $this->params->get('auto_cleanup', 1)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Throttle: only check once per hour via session flag
|
|
||||||
$session = Factory::getSession();
|
|
||||||
$lastCheck = $session->get('mokojoombackup.last_cleanup', 0);
|
|
||||||
|
|
||||||
if (time() - $lastCheck < 3600) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$session->set('mokojoombackup.last_cleanup', time());
|
|
||||||
|
|
||||||
$this->cleanupOldBackups();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove backup records and files older than max_age_days or exceeding max_backups.
|
|
||||||
*/
|
|
||||||
private function cleanupOldBackups(): void
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$maxAge = (int) $this->params->get('max_age_days', 30);
|
|
||||||
$maxBackups = (int) $this->params->get('max_backups', 10);
|
|
||||||
|
|
||||||
// Delete by age
|
|
||||||
$cutoff = date('Y-m-d H:i:s', strtotime("-{$maxAge} days"));
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('id, absolute_path')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('backupstart') . ' < ' . $db->quote($cutoff))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
|
||||||
$db->setQuery($query);
|
|
||||||
$expired = $db->loadObjectList();
|
|
||||||
|
|
||||||
foreach ($expired as $record) {
|
|
||||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
|
||||||
@unlink($record->absolute_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
|
||||||
);
|
|
||||||
$db->execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce max backups count (keep newest)
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('COUNT(*)')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
|
|
||||||
$db->setQuery($query);
|
|
||||||
$totalCount = (int) $db->loadResult();
|
|
||||||
|
|
||||||
if ($totalCount > $maxBackups) {
|
|
||||||
$excess = $totalCount - $maxBackups;
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('id, absolute_path')
|
|
||||||
->from($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
|
|
||||||
->order($db->quoteName('backupstart') . ' ASC');
|
|
||||||
$db->setQuery($query, 0, $excess);
|
|
||||||
$oldest = $db->loadObjectList();
|
|
||||||
|
|
||||||
foreach ($oldest as $record) {
|
|
||||||
if (!empty($record->absolute_path) && is_file($record->absolute_path)) {
|
|
||||||
@unlink($record->absolute_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete($db->quoteName('#__mokojoombackup_records'))
|
|
||||||
->where($db->quoteName('id') . ' = ' . (int) $record->id)
|
|
||||||
);
|
|
||||||
$db->execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a JSON response and terminate — used by web cron handler.
|
|
||||||
*/
|
|
||||||
private function sendJsonResponse(bool $success, string $message, int $httpCode = 200): void
|
|
||||||
{
|
|
||||||
while (@ob_end_clean()) {
|
|
||||||
// flush buffers
|
|
||||||
}
|
|
||||||
|
|
||||||
http_response_code($httpCode);
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
'success' => $success,
|
|
||||||
'message' => $message,
|
|
||||||
'timestamp' => date('Y-m-d H:i:s'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->getApplication()->close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
; MokoJoomBackup — Task Plugin language file (en-GB)
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Allows running backup profiles on a schedule via Joomla's Scheduled Tasks system."
|
|
||||||
|
|
||||||
; Task type
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_TITLE="MokoJoomBackup: Run Backup Profile"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_DESC="Run a MokoJoomBackup backup using the selected profile. Create multiple tasks with different profiles for different backup schedules (e.g. daily full backup, hourly database-only backup)."
|
|
||||||
|
|
||||||
; Task form fields
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Select which backup profile to run. Each profile defines backup type (full/database/files), exclusion filters, and storage settings."
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_SELECT_PROFILE="- Select Profile -"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Task Plugin system language file (en-GB)
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks."
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
; MokoJoomBackup — Task Plugin language file (en-US)
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Allows running backup profiles on a schedule via Joomla's Scheduled Tasks system."
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_TITLE="MokoJoomBackup: Run Backup Profile"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_TASK_RUN_PROFILE_DESC="Run a MokoJoomBackup backup using the selected profile. Create multiple tasks with different profiles for different backup schedules."
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE="Backup Profile"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_FIELD_PROFILE_DESC="Select which backup profile to run."
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_SELECT_PROFILE="- Select Profile -"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — Task Plugin system language file (en-US)
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP="Task - MokoJoomBackup"
|
|
||||||
PLG_TASK_MOKOJOOMBACKUP_DESCRIPTION="Scheduled task plugin for MokoJoomBackup. Run backup profiles on a schedule via Joomla's Scheduled Tasks."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — WebServices Plugin language file (en-GB)
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup"
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management. Provides endpoints to start, list, download, and delete backups."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — WebServices Plugin system language file (en-GB)
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup"
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — WebServices Plugin language file (en-US)
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup"
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management. Provides endpoints to start, list, download, and delete backups."
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
; MokoJoomBackup — WebServices Plugin system language file (en-US)
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP="Web Services - MokoJoomBackup"
|
|
||||||
PLG_WEBSERVICES_MOKOJOOMBACKUP_DESCRIPTION="REST API for remote backup management."
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_webservices_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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomBackup
|
|
||||||
* @subpackage plg_webservices_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
|
|
||||||
*/
|
|
||||||
|
|
||||||
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\MokoJoomBackup\Extension\MokoJoomBackupWebServices;
|
|
||||||
|
|
||||||
return new class () implements ServiceProviderInterface {
|
|
||||||
public function register(Container $container): void
|
|
||||||
{
|
|
||||||
$container->set(
|
|
||||||
PluginInterface::class,
|
|
||||||
function (Container $container) {
|
|
||||||
$plugin = new MokoJoomBackupWebServices(
|
|
||||||
$container->get(DispatcherInterface::class),
|
|
||||||
(array) PluginHelper::getPlugin('webservices', 'mokojoombackup')
|
|
||||||
);
|
|
||||||
$plugin->setApplication(Factory::getApplication());
|
|
||||||
|
|
||||||
return $plugin;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Installer\InstallerAdapter;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
|
|
||||||
class Pkg_MokoJoomBackupInstallerScript
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Minimum Joomla version required
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $minimumJoomla = '4.0.0';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum PHP version required
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $minimumPhp = '8.1.0';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before any install/update/uninstall action.
|
|
||||||
*
|
|
||||||
* @param string $type Action type (install, update, uninstall)
|
|
||||||
* @param InstallerAdapter $parent Installer adapter
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function preflight(string $type, InstallerAdapter $parent): bool
|
|
||||||
{
|
|
||||||
if (version_compare(PHP_VERSION, $this->minimumPhp, '<')) {
|
|
||||||
Factory::getApplication()->enqueueMessage(
|
|
||||||
Text::sprintf('PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR', $this->minimumPhp),
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save download key before Joomla re-registers the update site
|
|
||||||
if ($type === 'update') {
|
|
||||||
$this->preflight_saveKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after install/update.
|
|
||||||
*
|
|
||||||
* @param string $type Action type
|
|
||||||
* @param InstallerAdapter $parent Installer adapter
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Called before install/update to preserve the download key.
|
|
||||||
*
|
|
||||||
* Joomla re-registers update sites from the manifest on every update,
|
|
||||||
* which can reset the extra_query (download key). We save it here
|
|
||||||
* and restore it in postflight.
|
|
||||||
*/
|
|
||||||
private ?string $savedDownloadKey = null;
|
|
||||||
|
|
||||||
public function preflight_saveKey(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('us.extra_query'))
|
|
||||||
->from($db->quoteName('#__update_sites', 'us'))
|
|
||||||
->join(
|
|
||||||
'INNER',
|
|
||||||
$db->quoteName('#__update_sites_extensions', 'use')
|
|
||||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
|
||||||
)
|
|
||||||
->join(
|
|
||||||
'INNER',
|
|
||||||
$db->quoteName('#__extensions', 'e')
|
|
||||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
|
||||||
)
|
|
||||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokojoombackup'))
|
|
||||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
|
||||||
->setLimit(1);
|
|
||||||
$db->setQuery($query);
|
|
||||||
$key = $db->loadResult();
|
|
||||||
|
|
||||||
if (!empty($key)) {
|
|
||||||
$this->savedDownloadKey = $key;
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Not critical
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function postflight(string $type, InstallerAdapter $parent): void
|
|
||||||
{
|
|
||||||
// Restore download key if it was saved before update
|
|
||||||
if ($this->savedDownloadKey !== null) {
|
|
||||||
$this->restoreDownloadKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'install') {
|
|
||||||
// Enable the system plugin automatically on fresh install
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__extensions'))
|
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Enable the quickicon plugin automatically
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__extensions'))
|
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('quickicon'))
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Enable the task plugin automatically
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__extensions'))
|
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('task'))
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Enable the webservices plugin automatically
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__extensions'))
|
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('webservices'))
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Enable the console plugin automatically
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__extensions'))
|
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('console'))
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Enable the content plugin automatically
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__extensions'))
|
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('content'))
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Enable the actionlog plugin automatically
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__extensions'))
|
|
||||||
->set($db->quoteName('enabled') . ' = 1')
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
|
||||||
->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog'))
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoombackup'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Create default backup directory
|
|
||||||
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
|
||||||
|
|
||||||
if (!is_dir($backupDir)) {
|
|
||||||
mkdir($backupDir, 0755, true);
|
|
||||||
|
|
||||||
// Protect backup directory with .htaccess
|
|
||||||
file_put_contents($backupDir . '/.htaccess', "Order deny,allow\nDeny from all\n");
|
|
||||||
file_put_contents($backupDir . '/index.html', '<!DOCTYPE html><title></title>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn if no license key configured
|
|
||||||
$this->warnMissingLicenseKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the download key to the (possibly new) update site record.
|
|
||||||
*/
|
|
||||||
private function restoreDownloadKey(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('us.update_site_id'))
|
|
||||||
->from($db->quoteName('#__update_sites', 'us'))
|
|
||||||
->join(
|
|
||||||
'INNER',
|
|
||||||
$db->quoteName('#__update_sites_extensions', 'use')
|
|
||||||
. ' ON ' . $db->quoteName('use.update_site_id') . ' = ' . $db->quoteName('us.update_site_id')
|
|
||||||
)
|
|
||||||
->join(
|
|
||||||
'INNER',
|
|
||||||
$db->quoteName('#__extensions', 'e')
|
|
||||||
. ' ON ' . $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id')
|
|
||||||
)
|
|
||||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_mokojoombackup'))
|
|
||||||
->where($db->quoteName('e.type') . ' = ' . $db->quote('package'))
|
|
||||||
->setLimit(1);
|
|
||||||
$db->setQuery($query);
|
|
||||||
$updateSiteId = (int) $db->loadResult();
|
|
||||||
|
|
||||||
if ($updateSiteId > 0) {
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__update_sites'))
|
|
||||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
|
|
||||||
->where($db->quoteName('update_site_id') . ' = ' . $updateSiteId);
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Not critical
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function warnMissingLicenseKey(): void
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
|
|
||||||
->from($db->quoteName('#__update_sites'))
|
|
||||||
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoJoomBackup%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoJoomBackup%') . ')')
|
|
||||||
->setLimit(1)
|
|
||||||
);
|
|
||||||
$site = $db->loadObject();
|
|
||||||
|
|
||||||
if ($site)
|
|
||||||
{
|
|
||||||
$eq = (string) ($site->extra_query ?? '');
|
|
||||||
if (!empty($eq) && strpos($eq, 'dlid=') !== false) { parse_str($eq, $p); if (!empty($p['dlid'])) { return; } }
|
|
||||||
$editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$editUrl = 'index.php?option=com_installer&view=updatesites';
|
|
||||||
}
|
|
||||||
|
|
||||||
Factory::getApplication()->enqueueMessage(
|
|
||||||
'<strong>Moko Consulting License Key Required</strong> — '
|
|
||||||
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
|
|
||||||
. '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>',
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (\Throwable $e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
; 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_MOKOBACKUP="Package - MokoJoomBackup"
|
||||||
|
PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||||
|
PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
; 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_MOKOBACKUP="Package - MokoJoomBackup"
|
||||||
|
PKG_MOKOBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
|
||||||
|
PKG_MOKOBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user