Compare commits

..

11 Commits

Author SHA1 Message Date
gitea-actions[bot] 11244374b0 chore(version): pre-release bump to 01.43.12-dev [skip ci] 2026-06-25 15:51:00 +00:00
jmiller 0fe14bf19b fix: remove run/backup buttons, move actions to detail view, custom restore script name, version bump 01.43.11-dev
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 24s
- Remove Run Backup / Backup Now buttons from profiles list, profile edit toolbar, and backup records view
- Move download, browse archive, and view log from backup list rows into individual backup record detail view
- Add download button to backup detail toolbar
- Link profile column in backup records list to profile edit
- Complete restore script filename customization across BackupEngine, SteppedBackupEngine, and MokoRestore
- Remove ordering field from profiles, default sort by ID ascending
- Fix untranslated JFIELD language keys
- Bump all manifests to 01.43.11-dev
2026-06-25 10:50:31 -05:00
jmiller 836d1bc8b7 fix(mokorestore): add Joomla detection warning, multi-zip selector, and standalone backup scan
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
- Preflight now detects existing Joomla installation (configuration.php / Version.php)
  and shows a yellow warning — does not block, but alerts the user
- Standalone mode: backup archive check scans for all ZIPs instead of hardcoded name
- Multi-zip selector integrated into extract step with radio buttons
- Selected backup file passed through to extract action
- Added warn-style CSS class (yellow) for preflight warnings
2026-06-25 10:00:07 -05:00
gitea-actions[bot] 79b3caa35a chore(version): pre-release bump to 01.43.05-dev [skip ci] 2026-06-25 13:39:28 +00:00
gitea-actions[bot] 6102c8f590 chore(version): pre-release bump to 01.43.04-dev [skip ci] 2026-06-25 13:39:01 +00:00
jmiller 88e53c5698 Merge pull request 'fix: Bootstrap 5 modals, language keys, ntfy default, MokoRestore error handling' (#146) from fix/bootstrap-modals into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-25 13:38:43 +00:00
gitea-actions[bot] ec1c3486c5 chore(version): pre-release bump to 01.43.03-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
2026-06-25 13:38:28 +00:00
jmiller 3742477aef fix: convert inline modals to Bootstrap 5, fix language keys, ntfy default, and MokoRestore error handling
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
Universal: PR Check / Secret Scan (pull_request) Successful in 9s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 29s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
- Convert 10 inline CSS modals to Bootstrap 5 (backups: 7, snapshots: 3)
- Replace style.display show/hide with Bootstrap Modal API
- Fix JFIELD_ORDERING_LABEL_ASC → JFIELD_ORDERING_ASC in profile filter
- Add COM_MOKOJOOMBACKUP_CONFIGURATION key for Options page title
- Change ntfy default server to ntfy.mokoconsulting.tech
- Add profile ID to dropdown labels across backups, dashboard, cpanel module
- Add error handling to MokoRestore post() and runPreflight() to prevent UI stalling
- Remove outdated SSH auth pattern references from field descriptions
2026-06-25 08:35:40 -05:00
gitea-actions[bot] bb8e4a258a chore(version): pre-release bump to 01.43.02-dev [skip ci] 2026-06-24 11:49:56 +00:00
gitea-actions[bot] e6d646011a chore(version): auto-bump patch 01.43.01-dev [skip ci] 2026-06-24 11:49:37 +00:00
jmiller 726291995c chore: sync main into dev (#145)
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
2026-06-24 11:49:19 +00:00
39 changed files with 689 additions and 1082 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ on:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
+10 -10
View File
@@ -52,7 +52,7 @@ on:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
MOKOGITEA_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_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
@@ -102,7 +102,7 @@ jobs:
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
@@ -121,7 +121,7 @@ jobs:
- name: Update RC release notes from CHANGELOG.md
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Extract [Unreleased] section from changelog
@@ -269,7 +269,7 @@ jobs:
!startsWith(steps.platform.outputs.platform, 'joomla')
run: |
VERSION="${{ steps.version.outputs.version }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
SEMVER_TAG="v${VERSION}"
@@ -294,7 +294,7 @@ jobs:
- name: Update release notes and promote changelog
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Get the stable release info (version and ID)
@@ -363,7 +363,7 @@ jobs:
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_mirror.php \
--version "$VERSION" --tag "$RELEASE_TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
@@ -392,7 +392,7 @@ jobs:
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
@@ -416,7 +416,7 @@ jobs:
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
BRANCH_NAME="version/${VERSION}"
@@ -437,7 +437,7 @@ jobs:
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/version_reset_dev.php \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
--branch dev --path . 2>&1 || true
@@ -463,5 +463,5 @@ jobs:
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](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/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,68 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
# VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
name: "Universal: CI Issue Reporter"
on:
workflow_call:
inputs:
gate:
description: "CI gate name (e.g. PR Validation, Repository Health)"
required: true
type: string
details:
description: "Human-readable failure description"
required: true
type: string
severity:
description: "error or warning"
required: false
type: string
default: "error"
workflow:
description: "Workflow name for the issue title"
required: false
type: string
default: ""
secrets:
MOKOGITEA_TOKEN:
required: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
report:
name: "Report: ${{ inputs.gate }}"
runs-on: ubuntu-latest
steps:
- name: Clone MokoCLI
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
- name: Report CI failure
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
/tmp/mokocli/cli/ci_issue_reporter.sh \
--gate "${{ inputs.gate }}" \
--details "${{ inputs.details }}" \
--severity "${{ inputs.severity }}" \
--workflow "${{ inputs.workflow }}"
+10 -10
View File
@@ -21,7 +21,7 @@ permissions:
contents: write
env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
cleanup:
@@ -33,17 +33,17 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
token: ${{ secrets.GA_TOKEN }}
- name: Delete merged branches
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Merged Branch Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# List branches via API
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches?limit=50" | jq -r '.[].name')
DELETED=0
@@ -56,7 +56,7 @@ jobs:
# Check if branch is merged into main
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
echo " Deleting merged branch: ${BRANCH}"
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/branches/${BRANCH}" 2>/dev/null || true
DELETED=$((DELETED + 1))
fi
@@ -66,20 +66,20 @@ jobs:
- name: Clean old workflow runs
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.GA_TOKEN }}
run: |
echo "=== Workflow Run Cleanup ==="
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
# Get old completed runs
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs?status=completed&limit=50" | \
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
DELETED=0
for RUN_ID in $RUNS; do
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
DELETED=$((DELETED + 1))
done
-126
View File
@@ -1,126 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
inputs:
clear_remote:
description: 'Delete all remote files before uploading'
required: false
default: 'false'
type: boolean
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
jobs:
deploy:
name: SFTP Deploy to Dev
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
run: |
php -v && composer --version
- name: Setup MokoStandards tools
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
run: |
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
- name: Check FTP configuration
id: check
env:
HOST: ${{ vars.DEV_FTP_HOST }}
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
PORT: ${{ vars.DEV_FTP_PORT }}
run: |
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "host=$HOST" >> "$GITHUB_OUTPUT"
REMOTE="${PATH_VAR%/}"
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
[ -z "$PORT" ] && PORT="22"
echo "port=$PORT" >> "$GITHUB_OUTPUT"
- name: Deploy via SFTP
if: steps.check.outputs.skip != 'true'
env:
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
> /tmp/sftp-config.json
if [ -n "$SFTP_KEY" ]; then
echo "$SFTP_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
fi
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
else
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Summary
if: always()
run: |
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
else
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
fi
+5 -5
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.00.00
# VERSION: 01.43.12
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
@@ -19,7 +19,7 @@ permissions:
issues: write
env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
jobs:
create-branch:
@@ -28,8 +28,8 @@ jobs:
steps:
- name: Create branch and comment
run: |
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
ISSUE_NUM="${{ github.event.issue.number }}"
ISSUE_TITLE="${{ github.event.issue.title }}"
@@ -58,7 +58,7 @@ jobs:
echo "Created branch: ${BRANCH}"
# Comment on issue with branch link
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
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 \
+23 -10
View File
@@ -496,26 +496,39 @@ jobs:
steps:
- name: Trigger RC pre-release
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ──────────────────────────────────────────────────────
report-issues:
name: Report Issues
runs-on: ubuntu-latest
needs: [branch-policy, validate]
if: >-
always() &&
needs.validate.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "PR Validation"
workflow: "PR Check"
severity: error
details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
secrets: inherit
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: "File issue for PR validation failure"
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
./automation/ci-issue-reporter.sh \
--gate "PR Validation" \
--workflow "PR Check" \
--severity error \
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
+5 -5
View File
@@ -40,7 +40,7 @@ permissions:
contents: write
env:
MOKOGITEA_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_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
@@ -182,7 +182,7 @@ jobs:
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
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" \
@@ -193,7 +193,7 @@ jobs:
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
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
@@ -230,7 +230,7 @@ jobs:
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
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" \
@@ -243,7 +243,7 @@ jobs:
if: steps.eligibility.outputs.proceed == 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
+37 -25
View File
@@ -77,7 +77,7 @@ jobs:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
@@ -671,30 +671,42 @@ jobs:
# ═══════════════════════════════════════════════════════════════════════
# Issue Reporter — file issues for failed gates
# ═══════════════════════════════════════════════════════════════════════
report-scripts:
name: "Report: Scripts Governance"
needs: [access_check, scripts_governance]
report-issues:
name: "Report Issues"
runs-on: ubuntu-latest
needs: [access_check, scripts_governance, repo_health]
if: >-
always() &&
needs.scripts_governance.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "Scripts Governance"
workflow: "Repo Health"
severity: error
details: "Scripts directory policy violations detected. Review required and allowed directories."
secrets: inherit
(needs.scripts_governance.result == 'failure' ||
needs.repo_health.result == 'failure')
report-health:
name: "Report: Repository Health"
needs: [access_check, repo_health]
if: >-
always() &&
needs.repo_health.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "Repository Health"
workflow: "Repo Health"
severity: error
details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
secrets: inherit
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: "File issues for failed gates"
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
REPORTER="./automation/ci-issue-reporter.sh"
WF="Repo Health"
report_gate() {
local gate="$1" result="$2" details="$3"
if [ "$result" = "failure" ]; then
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
fi
}
report_gate "Scripts Governance" \
"${{ needs.scripts_governance.result }}" \
"Scripts directory policy violations detected. Review required and allowed directories."
report_gate "Repository Health" \
"${{ needs.repo_health.result }}" \
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
-130
View File
@@ -1,130 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow.Template
# INGROUP: MokoStandards.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
# PATH: /.mokogitea/workflows/version-set.yml
# VERSION: 01.00.00
# BRIEF: Set or reset the extension version across all version-bearing files
name: "Joomla: Set Version"
on:
workflow_dispatch:
inputs:
version:
description: "Version number (e.g. 01.00.00)"
required: true
type: string
branch:
description: "Branch to update (default: current)"
required: false
type: string
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
set-version:
name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
VERSION="${{ inputs.version }}"
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
exit 1
fi
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
- name: Update manifest version
run: |
MANIFEST=""
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
MANIFEST="$XML_FILE"
break
fi
done
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla extension manifest found — skipping manifest update"
else
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
fi
- name: Update README.md version
run: |
if [ -f "README.md" ]; then
if grep -qP '^\s*VERSION:\s*\d' README.md; then
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
echo "README.md version updated to ${VERSION}"
else
echo "::warning::No VERSION line found in README.md — skipping"
fi
fi
- name: Update CHANGELOG.md
run: |
if [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
# Check if this version already has an entry
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
else
# Insert new version entry after [Unreleased] or at the top after header
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
else
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
fi
echo "CHANGELOG.md: added entry for ${VERSION}"
fi
else
echo "::warning::No CHANGELOG.md found — skipping"
fi
- name: Update FILE INFORMATION blocks
run: |
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
while IFS= read -r -d '' FILE; do
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
echo "Updated FILE INFORMATION VERSION in ${FILE}"
fi
done
- name: Commit and push
run: |
git config user.name "Moko Consulting [bot]"
git config user.email "hello@mokoconsulting.tech"
git add -A
if git diff --cached --quiet; then
echo "No version changes detected — nothing to commit"
else
git commit -m "chore: set version to ${VERSION} [skip bump]
Authored-by: Moko Consulting"
git push
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
fi
+4 -12
View File
@@ -13,7 +13,6 @@
name: "Universal: Workflow Sync Trigger"
on:
workflow_dispatch:
pull_request:
types: [closed]
branches:
@@ -27,9 +26,8 @@ jobs:
name: Sync workflows to live repos
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]'))
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.title, '[skip sync]')
steps:
- name: Determine platform from repo name
@@ -51,14 +49,8 @@ jobs:
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install PHP
run: |
if ! command -v php &> /dev/null; then
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
fi
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
- name: Install dependencies
run: |
+23 -1
View File
@@ -2,6 +2,28 @@
## [Unreleased]
### Added
- Customizable restore script filename per backup profile (reduces discoverability on remote servers)
- MokoRestore standalone mode: multi-ZIP selector when multiple backup archives are present
- MokoRestore preflight: Joomla installation detection warning before overwriting an existing site
- MokoRestore error handling: try/catch on fetch calls, HTTP status checks, JSON parse recovery
- Download button on individual backup record detail toolbar
- Profile column in backup records list links to the profile edit view
### Changed
- Moved download, browse archive, and view log actions from backup list rows into the individual backup record view
- Removed "Run Backup" / "Backup Now" buttons from profiles list, profile edit toolbar, and backup records view (backups are triggered from the dashboard only)
- Removed ordering field from profiles; default sort is now by ID ascending
- MokoRestore cleanup and security messages now reference the actual script filename instead of hardcoded "restore.php"
### Fixed
- Bootstrap 5 modal conversion for snapshots view (data-bs-dismiss, modal-footer, getOrCreateInstance)
- ntfy default URL changed from ntfy.sh to ntfy.mokoconsulting.tech
- Untranslated JFIELD_ORDERING_ASC / JFIELD_ORDERING_LABEL language keys replaced with component-specific keys
- Options page title now shows "MokoSuiteBackup Options" instead of raw language key
- Profile dropdown IDs in backup records and dashboard show "#ID — Title (type)" format
- MokoRestore stalling: unhandled promise rejections from network errors or non-JSON responses left UI in loading state
## [01.43.00] --- 2026-06-24
@@ -71,7 +93,7 @@
- Backup comparison: select two backups for side-by-side diff
- Archive browser: view files inside backup without extracting
- Manual purge: delete backups older than a date with count preview
- Run Backup button on profile list and edit views with backup count badges
- Backup count badges on profile list
- "Do not navigate away" warning in backup/restore progress modals
- Clickable placeholder pills for backup directory and archive name fields
- Comprehensive help modal with absolute/relative/placeholder path documentation
@@ -245,7 +245,7 @@
type="text"
label="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER"
description="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER_DESC"
default="https://ntfy.sh"
default="https://ntfy.mokoconsulting.tech"
filter="url"
/>
<field
@@ -24,10 +24,9 @@
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
default="a.ordering ASC"
default="a.id ASC"
onchange="this.form.submit();"
>
<option value="a.ordering ASC">JFIELD_ORDERING_LABEL_ASC</option>
<option value="a.title ASC">COM_MOKOJOOMBACKUP_HEADING_TITLE_ASC</option>
<option value="a.title DESC">COM_MOKOJOOMBACKUP_HEADING_TITLE_DESC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
@@ -93,6 +93,16 @@
<option value="1">COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED</option>
<option value="standalone">COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE</option>
</field>
<field
name="restore_script_name"
type="text"
label="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME"
description="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC"
default="restore.php"
maxlength="128"
filter="string"
showon="include_mokorestore!:0"
/>
<field
name="encryption_password"
type="password"
@@ -164,12 +174,6 @@
<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">
@@ -5,6 +5,7 @@
; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_CONFIGURATION="MokoSuiteBackup Options"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
; Submenu
@@ -139,6 +140,8 @@ COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="None: no restore script. Wrap
COM_MOKOJOOMBACKUP_MOKORESTORE_NONE="None"
COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED="Wrapped (inside backup ZIP)"
COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE="Standalone (separate restore.php)"
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME="Restore Script Filename"
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC="Custom filename for the restore script. Must end in .php. Use a non-obvious name to reduce discoverability on remote servers (e.g. moko-install-xyz.php)."
; Data Sanitization
COM_MOKOJOOMBACKUP_FIELDSET_SANITIZATION="Data Sanitization"
@@ -275,9 +278,9 @@ COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT_DESC="SSH port (default: 22)"
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME="SSH Username"
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC="Username for SSH authentication"
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD="SSH Password"
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC="Password for SSH authentication. Leave blank if using a key file."
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC="Password for SSH authentication."
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY="SSH Private Key"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload your SSH private key (id_rsa, id_ed25519). Stored base64-encoded in DB, written to temp file during upload only. Leave blank for password auth."
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload your SSH private key (id_rsa, id_ed25519). Stored base64-encoded in DB, written to temp file during upload only."
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD="Upload Key File"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE="Replace Key"
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED="Key loaded"
@@ -7,7 +7,7 @@
-->
<extension type="component" method="upgrade">
<name>MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -259,14 +259,14 @@ class BackupEngine
// Step 2.5: MokoRestore script (if enabled)
$mokoRestoreMode = $profile->include_mokorestore ?? '0';
$restoreScriptName = $profile->restore_script_name ?? 'restore.php';
$restoreScriptPath = '';
if ($mokoRestoreMode === '1') {
// Wrapped mode: backup ZIP inside an outer ZIP with restore.php
$this->log('Wrapping with MokoRestore script...');
$mokoRestoreName = str_replace('.zip', '-mokorestore.zip', $archiveName);
$mokoRestorePath = $this->backupDir . '/' . $mokoRestoreName;
MokoRestore::wrap($archivePath, $mokoRestorePath);
MokoRestore::wrap($archivePath, $mokoRestorePath, $restoreScriptName);
if (is_file($archivePath) && !unlink($archivePath)) {
$this->log('WARNING: Could not remove pre-wrap archive');
@@ -278,11 +278,11 @@ class BackupEngine
$this->log('MokoRestore archive created: ' . $sizeHuman);
$this->log('SHA-256 (wrapped): ' . $checksum);
} elseif ($mokoRestoreMode === 'standalone') {
// Standalone mode: restore.php as a separate file next to the backup ZIP
$this->log('Generating standalone restore.php...');
$restoreScriptPath = $this->backupDir . '/restore.php';
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
$this->log('Generating standalone ' . $restoreScriptName . '...');
$restoreScriptPath = $this->backupDir . '/' . $restoreScriptName;
MokoRestore::generateStandalone($restoreScriptPath);
$this->log('Standalone restore.php generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
$this->log('Standalone ' . $restoreScriptName . ' generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
}
$remoteFilename = '';
@@ -303,9 +303,8 @@ class BackupEngine
$remoteFilename = $result['remote_path'] ?? $archiveName;
$this->log(' Upload complete: ' . $result['message']);
/* Upload standalone restore.php if in standalone mode */
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
$uploader->upload($restoreScriptPath, 'restore.php');
$uploader->upload($restoreScriptPath, basename($restoreScriptPath));
}
} else {
$uploadFailed = true;
@@ -336,15 +335,15 @@ class BackupEngine
$remoteFilename = $uploadResult['remote_path'] ?? $archiveName;
$this->log('Remote upload complete: ' . $uploadResult['message']);
// Upload standalone restore.php alongside the backup if in standalone mode
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
$this->log('Uploading standalone restore.php...');
$restoreUpload = $uploader->upload($restoreScriptPath, 'restore.php');
$restoreBasename = basename($restoreScriptPath);
$this->log('Uploading standalone ' . $restoreBasename . '...');
$restoreUpload = $uploader->upload($restoreScriptPath, $restoreBasename);
if ($restoreUpload['success']) {
$this->log('Standalone restore.php uploaded');
$this->log('Standalone ' . $restoreBasename . ' uploaded');
} else {
$this->log('WARNING: restore.php upload failed: ' . $restoreUpload['message']);
$this->log('WARNING: ' . $restoreBasename . ' upload failed: ' . $restoreUpload['message']);
}
}
@@ -35,25 +35,36 @@ class MokoRestore
*
* @return string Path to the wrapped archive
*/
public static function wrap(string $backupArchive, string $outputPath): string
public static function wrap(string $backupArchive, string $outputPath, string $scriptName = 'restore.php'): string
{
$scriptName = self::sanitizeScriptName($scriptName);
$zip = new \ZipArchive();
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath);
}
// Add the standalone restore script
$zip->addFromString('restore.php', self::generateRestoreScript());
// Add the original backup as a nested ZIP
$zip->addFromString($scriptName, self::generateRestoreScript());
$zip->addFile($backupArchive, 'site-backup.zip');
$zip->close();
return $outputPath;
}
public static function sanitizeScriptName(string $name): string
{
$name = basename(trim($name));
if ($name === '' || !str_ends_with(strtolower($name), '.php')) {
$name = 'restore.php';
}
$name = preg_replace('/[^a-zA-Z0-9._-]/', '', $name);
return $name ?: 'restore.php';
}
/**
* Generate the standalone restore.php script as a separate file.
*
@@ -165,7 +176,38 @@ SCANNER;
$php
);
/* Modify the pre-checks to use getSelectedBackupFile() */
/* Replace the backup archive check with one that scans for ZIPs
(must run BEFORE the blanket file_exists replacement below) */
$php = str_replace(
<<<'ORIG'
$checks[] = [
'label' => 'Backup Archive',
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
'ok' => file_exists(BACKUP_FILE),
'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
];
ORIG,
<<<'REPL'
$availableBackups = scanForBackups();
$backupCount = count($availableBackups);
$selectedFile = getSelectedBackupFile();
if ($selectedFile && file_exists($selectedFile)) {
$archiveValue = basename($selectedFile) . ' (' . number_format(filesize($selectedFile) / 1048576, 2) . ' MB)';
} elseif ($backupCount > 0) {
$archiveValue = $backupCount . ' ZIP file(s) found';
} else {
$archiveValue = 'No ZIP files found';
}
$checks[] = [
'label' => 'Backup Archive',
'value' => $archiveValue,
'ok' => $backupCount > 0,
'hint' => 'Place one or more backup ZIP files in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
];
REPL
);
/* Modify remaining pre-checks to use getSelectedBackupFile() */
$php = str_replace(
"file_exists(BACKUP_FILE)",
"(getSelectedBackupFile() !== '' || file_exists(BACKUP_FILE))",
@@ -174,65 +216,83 @@ SCANNER;
$html = self::generateFrontend();
/* Add backup file selector to the frontend before the extract step */
/* Inject backup file selector into the extract step (panel2) */
$selectorHtml = <<<'SELECTOR'
<!-- Backup File Selector (standalone mode) -->
<div id="mr-step-select" class="mr-step" style="display:none;">
<h2 class="mr-step-title">Select Backup File</h2>
<p class="mr-desc">Choose which backup archive to restore from.</p>
<div id="mr-backup-list"></div>
<input type="hidden" name="backup_file" id="mr-backup-file" value="">
</div>
<script>
(function() {
var backups = <?php echo json_encode(scanForBackups()); ?>;
var list = document.getElementById('mr-backup-list');
var hiddenInput = document.getElementById('mr-backup-file');
<div id="mr-backup-selector" class="mb-3">
<label class="mr-field-label" style="font-weight:600;margin-bottom:8px;display:block;">Backup Archive</label>
<div id="mr-backup-list"></div>
<input type="hidden" name="backup_file" id="mr-backup-file" value="">
</div>
<script>
(function() {
var backups = <?php echo json_encode(scanForBackups()); ?>;
var list = document.getElementById('mr-backup-list');
var hiddenInput = document.getElementById('mr-backup-file');
if (backups.length === 0) {
var alert = document.createElement('div');
alert.className = 'mr-alert mr-alert-danger';
alert.textContent = 'No ZIP files found in this directory. Upload a backup archive first.';
list.appendChild(alert);
} else if (backups.length === 1) {
hiddenInput.value = backups[0].name;
var found = document.createElement('div');
found.className = 'mr-alert mr-alert-success';
var strong = document.createElement('strong');
strong.textContent = backups[0].name;
found.appendChild(document.createTextNode('Found: '));
found.appendChild(strong);
found.appendChild(document.createTextNode(' (' + (backups[0].size / 1048576).toFixed(1) + ' MB)'));
list.appendChild(found);
} else {
var group = document.createElement('div');
group.className = 'mr-field-group';
backups.forEach(function(b) {
var label = document.createElement('label');
label.style.cssText = 'display:block; padding:8px; margin:4px 0; border:1px solid #ddd; border-radius:4px; cursor:pointer;';
var radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'backup_choice';
radio.value = b.name;
radio.style.marginRight = '8px';
radio.addEventListener('change', function() { hiddenInput.value = this.value; });
label.appendChild(radio);
var nameStrong = document.createElement('strong');
nameStrong.textContent = b.name;
label.appendChild(nameStrong);
label.appendChild(document.createTextNode(' \u2014 ' + (b.size / 1048576).toFixed(1) + ' MB \u2014 ' + b.date));
group.appendChild(label);
});
list.appendChild(group);
}
})();
</script>
if (backups.length === 0) {
var alert = document.createElement('div');
alert.style.cssText = 'padding:12px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#dc2626;';
alert.textContent = 'No ZIP files found in this directory. Upload a backup archive first.';
list.appendChild(alert);
} else if (backups.length === 1) {
hiddenInput.value = backups[0].name;
var found = document.createElement('div');
found.style.cssText = 'padding:12px;background:#dcfce7;border:1px solid #bbf7d0;border-radius:6px;color:#16a34a;';
var strong = document.createElement('strong');
strong.textContent = backups[0].name;
found.appendChild(document.createTextNode('Found: '));
found.appendChild(strong);
found.appendChild(document.createTextNode(' (' + (backups[0].size / 1048576).toFixed(1) + ' MB)'));
list.appendChild(found);
} else {
var hint = document.createElement('div');
hint.style.cssText = 'padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;color:#1d4ed8;margin-bottom:8px;font-size:0.9em;';
hint.textContent = 'Multiple backup archives found \u2014 select which one to restore:';
list.appendChild(hint);
backups.forEach(function(b, i) {
var label = document.createElement('label');
label.style.cssText = 'display:flex;align-items:center;padding:10px 12px;margin:4px 0;border:1px solid #e2e8f0;border-radius:6px;cursor:pointer;transition:background 0.15s;';
label.onmouseover = function() { this.style.background = '#f8fafc'; };
label.onmouseout = function() { this.style.background = ''; };
var radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'backup_choice';
radio.value = b.name;
radio.style.marginRight = '10px';
if (i === 0) { radio.checked = true; hiddenInput.value = b.name; }
radio.addEventListener('change', function() { hiddenInput.value = this.value; });
label.appendChild(radio);
var info = document.createElement('div');
var nameStrong = document.createElement('strong');
nameStrong.textContent = b.name;
info.appendChild(nameStrong);
var meta = document.createElement('div');
meta.style.cssText = 'font-size:0.85em;color:#64748b;margin-top:2px;';
meta.textContent = (b.size / 1048576).toFixed(1) + ' MB \u2014 ' + b.date;
info.appendChild(meta);
label.appendChild(info);
list.appendChild(label);
});
}
})();
</script>
SELECTOR;
/* Insert the selector before the extract step in the HTML */
/* Insert the selector into the extract panel */
$html = str_replace(
'<!-- Step: Extract -->',
$selectorHtml . "\n<!-- Step: Extract -->",
'<p class="mr-desc">Extract site-backup.zip into the current directory.</p>',
'<p class="mr-desc">Select a backup archive and extract it into the current directory.</p>' . "\n" . $selectorHtml,
$html
);
/* Pass selected backup file to the extract action */
$html = str_replace(
"const r = await post('extract', pw ? { archive_password: pw } : {});",
"var extraParams = {};\n" .
" if (pw) extraParams.archive_password = pw;\n" .
" var sel = document.getElementById('mr-backup-file');\n" .
" if (sel && sel.value) extraParams.backup_file = sel.value;\n" .
" const r = await post('extract', extraParams);",
$html
);
@@ -435,7 +495,7 @@ function actionPreflight(): array
'label' => 'Backup Archive',
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
'ok' => file_exists(BACKUP_FILE),
'hint' => 'site-backup.zip must be in the same directory as restore.php',
'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
];
$checks[] = [
@@ -462,15 +522,31 @@ function actionPreflight(): array
'hint' => 'Informational',
];
$joomlaExists = file_exists(RESTORE_DIR . '/configuration.php')
|| file_exists(RESTORE_DIR . '/libraries/src/Version.php');
$checks[] = [
'label' => 'Existing Installation',
'value' => $joomlaExists ? 'Joomla detected' : 'Clean directory',
'ok' => true,
'warn' => $joomlaExists,
'hint' => $joomlaExists
? 'WARNING: A Joomla installation already exists in this directory. Restoring will overwrite it.'
: 'No existing installation found — safe to proceed',
];
$allOk = true;
$warnings = [];
foreach ($checks as $c) {
if (!$c['ok']) {
$allOk = false;
}
if (!empty($c['warn'])) {
$warnings[] = $c['hint'];
}
}
return ['success' => $allOk, 'checks' => $checks];
return ['success' => $allOk, 'checks' => $checks, 'warnings' => $warnings];
}
function actionExtract(array $data): array
@@ -1425,6 +1501,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
.mr-checks li:last-child{border-bottom:none}
.mr-check-icon{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0}
.mr-check-ok{background:#dcfce7;color:#16a34a}
.mr-check-warn{background:#fef9c3;color:#a16207}
.mr-check-fail{background:#fef2f2;color:#dc2626}
.mr-check-info{background:#e0f2fe;color:#0284c7}
.mr-check-label{flex:1;font-weight:500}
@@ -1474,7 +1551,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<div class="mr-container">
<div class="mr-alert mr-alert-danger">
<strong>Security:</strong> Delete restore.php immediately after installation is complete.
<strong>Security:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> immediately after installation is complete.
</div>
<!-- Step Progress -->
@@ -1722,7 +1799,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<strong>Success!</strong> The site restoration is complete.
</div>
<div class="mr-alert mr-alert-danger">
<strong>Important:</strong> Delete <code>restore.php</code> and <code>site-backup.zip</code> from your server immediately for security.
<strong>Important:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> and <code>site-backup.zip</code> from your server immediately for security.
</div>
<div style="margin-top:1rem">
<button class="mr-btn mr-btn-danger" onclick="runCleanup()">Remove Restore Files</button>
@@ -1746,6 +1823,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<script>
const TOKEN = <?php echo json_encode($token); ?>;
const SCRIPT_URL = <?php echo json_encode(basename($_SERVER['SCRIPT_NAME'])); ?>;
let currentStep = 1;
let dbConfig = {};
@@ -1769,8 +1847,23 @@ async function post(action, extra) {
form.append(k, v);
}
}
const res = await fetch('restore.php', { method: 'POST', body: form });
return res.json();
var res;
try {
res = await fetch(SCRIPT_URL, { method: 'POST', body: form });
} catch (e) {
log('Network error: ' + e.message);
return { success: false, message: 'Network error: ' + e.message, checks: [] };
}
if (!res.ok) {
log('Server error: HTTP ' + res.status);
return { success: false, message: 'Server error (HTTP ' + res.status + ')', checks: [] };
}
try {
return await res.json();
} catch (e) {
log('Invalid response from server (not JSON)');
return { success: false, message: 'Invalid server response — check PHP error log', checks: [] };
}
}
function goStep(n) {
@@ -1845,42 +1938,66 @@ async function runPreflight() {
setBtnLoading(btn, true);
log('Running pre-flight checks...');
const r = await post('preflight');
const list = document.getElementById('checkList');
while (list.firstChild) list.removeChild(list.firstChild);
try {
const r = await post('preflight');
r.checks.forEach(function(c) {
const li = document.createElement('li');
const icon = document.createElement('span');
icon.className = 'mr-check-icon ' + (c.ok ? 'mr-check-ok' : 'mr-check-fail');
icon.textContent = c.ok ? '\u2713' : '\u2717';
if (!r.success && !r.checks.length) {
log('Pre-flight error: ' + (r.message || 'Unknown error'));
setBtnLoading(btn, false);
btn.textContent = 'Re-check';
setStatus('checkList', r.message || 'Pre-flight check failed', 'error');
return;
}
const label = document.createElement('span');
label.className = 'mr-check-label';
label.textContent = c.label;
const list = document.getElementById('checkList');
while (list.firstChild) list.removeChild(list.firstChild);
const val = document.createElement('span');
val.className = 'mr-check-value';
val.textContent = c.value;
r.checks.forEach(function(c) {
const li = document.createElement('li');
const icon = document.createElement('span');
var iconClass = c.ok ? 'mr-check-ok' : 'mr-check-fail';
if (c.warn) iconClass = 'mr-check-warn';
icon.className = 'mr-check-icon ' + iconClass;
icon.textContent = c.warn ? '\u26a0' : (c.ok ? '\u2713' : '\u2717');
li.appendChild(icon);
li.appendChild(label);
li.appendChild(val);
list.appendChild(li);
const label = document.createElement('span');
label.className = 'mr-check-label';
label.textContent = c.label;
log(' ' + (c.ok ? 'OK' : 'FAIL') + ': ' + c.label + ' = ' + c.value);
});
const val = document.createElement('span');
val.className = 'mr-check-value';
val.textContent = c.value;
setBtnLoading(btn, false);
li.appendChild(icon);
li.appendChild(label);
li.appendChild(val);
if (c.warn && c.hint) {
var hint = document.createElement('div');
hint.style.cssText = 'font-size:0.85em;color:#a16207;margin-top:4px;padding:4px 8px;background:#fef9c3;border-radius:4px;';
hint.textContent = c.hint;
li.appendChild(hint);
}
list.appendChild(li);
if (r.success) {
btn.textContent = 'Next \u2192';
btn.onclick = function() { goStep(2); };
btn.className = 'mr-btn mr-btn-success';
log('All checks passed');
} else {
var logPrefix = c.warn ? 'WARN' : (c.ok ? 'OK' : 'FAIL');
log(' ' + logPrefix + ': ' + c.label + ' = ' + c.value);
});
setBtnLoading(btn, false);
if (r.success) {
btn.textContent = 'Next \u2192';
btn.onclick = function() { goStep(2); };
btn.className = 'mr-btn mr-btn-success';
log('All checks passed');
} else {
btn.textContent = 'Re-check';
log('Some checks failed');
}
} catch (e) {
log('Pre-flight error: ' + e.message);
setBtnLoading(btn, false);
btn.textContent = 'Re-check';
log('Some checks failed');
}
}
@@ -70,7 +70,8 @@ class SteppedBackupEngine
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
$session->remoteStorage = $profile->remote_storage ?? 'none';
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
$session->includeMokoRestore = $profile->include_mokorestore ?? '0';
$session->restoreScriptName = $profile->restore_script_name ?? 'restore.php';
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
// Load multi-remote destinations from the remotes table
@@ -377,15 +378,24 @@ class SteppedBackupEngine
$this->verifyArchive($session->archivePath, $session->backupType);
$session->log('Archive integrity verified');
// MokoRestore wrapper
if ($session->includeMokoRestore) {
// MokoRestore
$mokoRestoreMode = $session->includeMokoRestore ?? '0';
$restoreScriptName = $session->restoreScriptName ?? 'restore.php';
if ($mokoRestoreMode === '1') {
$session->log('Wrapping with MokoRestore script...');
$mokoRestorePath = $session->archivePath . '.mokorestore.zip';
MokoRestore::wrap($session->archivePath, $mokoRestorePath);
MokoRestore::wrap($session->archivePath, $mokoRestorePath, $restoreScriptName);
@unlink($session->archivePath);
rename($mokoRestorePath, $session->archivePath);
$totalSize = filesize($session->archivePath);
$session->log('MokoRestore archive created');
} elseif ($mokoRestoreMode === 'standalone') {
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
$restoreDir = dirname($session->archivePath);
$session->restoreScriptPath = $restoreDir . '/' . $restoreScriptName;
MokoRestore::generateStandalone($session->restoreScriptPath);
$session->log('Standalone ' . $restoreScriptName . ' generated');
}
// Update record
@@ -463,6 +473,10 @@ class SteppedBackupEngine
if ($result['success']) {
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
$session->log(' Upload complete: ' . $result['message']);
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
$uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath));
}
} else {
$uploadFailed = true;
$session->log(' WARNING: Upload failed: ' . $result['message']);
@@ -525,6 +539,12 @@ class SteppedBackupEngine
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
$session->log('Remote upload complete: ' . $result['message']);
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
$restoreBasename = basename($session->restoreScriptPath);
$session->log('Uploading standalone ' . $restoreBasename . '...');
$uploader->upload($session->restoreScriptPath, $restoreBasename);
}
if (!$session->remoteKeepLocal && is_file($session->archivePath)) {
@unlink($session->archivePath);
$session->log('Local copy removed');
@@ -51,7 +51,9 @@ class SteppedSession
public array $excludeFiles = [];
public array $excludeTables = [];
public string $remoteStorage = 'none';
public bool $includeMokoRestore = false;
public string $includeMokoRestore = '0';
public string $restoreScriptName = 'restore.php';
public string $restoreScriptPath = '';
public bool $remoteKeepLocal = true;
public string $encryptionPassword = '';
@@ -60,14 +60,14 @@ class ProfilesModel extends ListModel
$query->where('(' . $db->quoteName('a.title') . ' LIKE ' . $search . ')');
}
$orderCol = $this->state->get('list.ordering', 'a.ordering');
$orderCol = $this->state->get('list.ordering', 'a.id');
$orderDir = $this->state->get('list.direction', 'ASC');
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
return $query;
}
protected function populateState($ordering = 'a.ordering', $direction = 'ASC'): void
protected function populateState($ordering = 'a.id', $direction = 'ASC'): void
{
parent::populateState($ordering, $direction);
}
@@ -12,8 +12,12 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\View\Backup;
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\Session\Session;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
@@ -34,6 +38,24 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
$user = Factory::getApplication()->getIdentity();
if ($this->item->status === 'complete'
&& !empty($this->item->filesexist)
&& $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')
) {
$toolbar = Toolbar::getInstance();
$downloadUrl = Route::_(
'index.php?option=com_mokosuitebackup&task=backups.download&id='
. (int) $this->item->id . '&' . Session::getFormToken() . '=1'
);
$toolbar->linkButton('download', 'COM_MOKOJOOMBACKUP_DOWNLOAD')
->url($downloadUrl)
->icon('icon-download')
->buttonClass('btn btn-success');
}
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitebackup&view=backups');
}
}
@@ -25,7 +25,6 @@ class HtmlView extends BaseHtmlView
protected $state;
public $filterForm;
public $activeFilters = [];
public $profiles = [];
public function display($tpl = null): void
{
@@ -35,16 +34,6 @@ class HtmlView extends BaseHtmlView
$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('#__mokosuitebackup_profiles'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->profiles = $db->loadObjectList() ?: [];
$this->checkUpdateSite();
$this->addToolbar();
@@ -112,10 +101,6 @@ class HtmlView extends BaseHtmlView
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
ToolbarHelper::custom('backups.start', 'download', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW', false);
}
if ($user->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
}
@@ -55,16 +55,6 @@ class HtmlView extends BaseHtmlView
$toolbar = Toolbar::getInstance();
$profileId = (int) $this->item->id;
// "Run Backup Now" button — links to backup start with CSRF token
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
$runUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $profileId . '&' . Session::getFormToken() . '=1');
$toolbar->linkButton('run-backup', 'COM_MOKOJOOMBACKUP_RUN_BACKUP_NOW')
->url($runUrl)
->icon('icon-play')
->buttonClass('btn btn-success');
}
// "View Backups" link button
$backupsUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[PROFILE_ID]=' . $profileId);
$toolbar->linkButton('view-backups', 'COM_MOKOJOOMBACKUP_VIEW_BACKUPS')
->url($backupsUrl)
@@ -31,30 +31,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<div class="row">
<div class="col-md-12">
<div id="j-main-container" class="j-main-container">
<!-- Profile selector for Backup Now -->
<?php $canRun = $user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup'); ?>
<?php if (!empty($this->profiles) && $canRun) : ?>
<div class="card mb-3">
<div class="card-body d-flex align-items-center gap-3">
<label for="mb-profile-select" class="form-label mb-0 fw-bold">
<?php echo Text::_('COM_MOKOJOOMBACKUP_BACKUP_PROFILE'); ?>:
</label>
<select id="mb-profile-select" class="form-select" style="max-width:300px;">
<?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.mokosuitebackupStart()">
<span class="icon-download" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW'); ?>
</button>
</div>
</div>
<?php endif; ?>
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
<?php if (empty($this->items)) : ?>
@@ -88,9 +64,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-5">
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
</th>
<th scope="col" class="w-5">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
@@ -111,7 +84,9 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<?php endif; ?>
</td>
<td>
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=profile.edit&id=' . (int) $item->profile_id); ?>">
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
</a>
</td>
<td>
<?php
@@ -139,35 +114,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<td>
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
</td>
<td class="d-flex gap-1">
<?php if ($item->status === 'complete' && $item->filesexist && $canDownload) : ?>
<?php
$isWebAccessible = !empty($item->absolute_path)
&& strpos(realpath($item->absolute_path) ?: $item->absolute_path, realpath(JPATH_ROOT) ?: JPATH_ROOT) === 0;
?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.download&id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
class="btn btn-sm btn-outline-primary" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_DOWNLOAD'); ?>">
<span class="icon-download"></span>
</a>
<?php if ($isWebAccessible) : ?>
<span class="badge bg-warning text-dark" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING'); ?>">
<span class="icon-warning-circle" aria-hidden="true"></span>
</span>
<?php endif; ?>
<?php endif; ?>
<?php if ($item->status === 'complete' && $item->filesexist) : ?>
<button type="button" class="btn btn-sm btn-outline-info mb-browse-archive"
data-id="<?php echo (int) $item->id; ?>"
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>">
<span class="icon-folder-open"></span>
</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-secondary mb-view-log"
data-id="<?php echo (int) $item->id; ?>"
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?>">
<span class="icon-file-alt"></span>
</button>
</td>
<td>
<?php echo (int) $item->id; ?>
</td>
@@ -188,18 +134,24 @@ $listDirn = $this->escape($this->state->get('list.direction'));
</form>
<!-- Stepped Backup Modal (for shared hosting) -->
<div id="mokosuitebackup-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 class="alert alert-warning py-1 px-2 mb-2" style="font-size:0.85rem;">
<span class="icon-warning-circle" aria-hidden="true"></span>
<strong>Do not navigate away or close this window</strong> while the backup is running.
<div class="modal fade" id="mokosuitebackup-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mb-modal-title">Backup in Progress</h5>
</div>
<div class="modal-body">
<div class="alert alert-warning py-1 px-2 mb-2" style="font-size:0.85rem;">
<span class="icon-warning-circle" aria-hidden="true"></span>
<strong>Do not navigate away or close this window</strong> while the backup is running.
</div>
<div class="progress mb-2" style="height:24px;">
<div id="mb-progress-bar" class="progress-bar" role="progressbar" style="width:0%;">0%</div>
</div>
<p id="mb-status" class="text-muted mb-1" style="font-size:0.9rem;">Initializing...</p>
<p id="mb-phase" class="text-muted mb-0" style="font-size:0.8rem;">Phase: init</p>
</div>
</div>
<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>
@@ -208,19 +160,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
// Override the toolbar "Backup Now" button to use stepped backup
document.addEventListener('DOMContentLoaded', function() {
// Find the backup toolbar button and override it
const toolbarBtn = document.querySelector('[onclick*="backups.start"], .button-download');
if (toolbarBtn) {
toolbarBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
startSteppedBackup();
return false;
}, true);
}
});
var backupRunning = false;
@@ -235,12 +174,12 @@ $listDirn = $this->escape($this->state->get('list.direction'));
function showModal() {
backupRunning = true;
document.getElementById('mokosuitebackup-modal').style.display = 'block';
bootstrap.Modal.getOrCreateInstance(document.getElementById('mokosuitebackup-modal')).show();
}
function hideModal() {
backupRunning = false;
document.getElementById('mokosuitebackup-modal').style.display = 'none';
bootstrap.Modal.getInstance(document.getElementById('mokosuitebackup-modal'))?.hide();
}
function updateProgress(progress, message, phase) {
@@ -344,31 +283,26 @@ $listDirn = $this->escape($this->state->get('list.direction'));
return false;
}
document.getElementById('mb-restore-record-id').value = checked[0].value;
document.getElementById('mb-restore-modal').style.display = 'block';
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-restore-modal')).show();
return false;
}, true);
}
});
// Close restore modal
document.addEventListener('click', function(e) {
if (e.target.classList.contains('mb-restore-close') || e.target.id === 'mb-restore-modal') {
document.getElementById('mb-restore-modal').style.display = 'none';
}
});
// Close restore modal handled by Bootstrap data-bs-dismiss
// AJAX stepped restore
var restoreRunning = false;
function showRestoreProgress() {
restoreRunning = true;
document.getElementById('mb-restore-modal').style.display = 'none';
document.getElementById('mb-restore-progress-modal').style.display = 'block';
bootstrap.Modal.getInstance(document.getElementById('mb-restore-modal'))?.hide();
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-restore-progress-modal')).show();
}
function hideRestoreProgress() {
restoreRunning = false;
document.getElementById('mb-restore-progress-modal').style.display = 'none';
bootstrap.Modal.getInstance(document.getElementById('mb-restore-progress-modal'))?.hide();
}
function updateRestoreProgress(progress, message, phase) {
@@ -457,310 +391,154 @@ $listDirn = $this->escape($this->state->get('list.direction'));
}
});
// View Log modal handler
document.addEventListener('click', function(e) {
var btn = e.target.closest('.mb-view-log');
if (!btn) return;
e.preventDefault();
var recordId = btn.getAttribute('data-id');
var modal = document.getElementById('mb-log-modal');
var body = document.getElementById('mb-log-body');
body.textContent = 'Loading...';
modal.style.display = 'block';
var form = new URLSearchParams();
form.append('task', 'ajax.viewLog');
form.append('id', recordId);
form.append(TOKEN_NAME, '1');
fetch(AJAX_URL, {
method: 'POST',
body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
body.textContent = data.message || 'Error loading log';
} else {
body.textContent = data.log;
}
})
.catch(function(err) {
body.textContent = 'Error: ' + err.message;
});
});
document.addEventListener('click', function(e) {
if (e.target.id === 'mb-log-modal' || e.target.classList.contains('mb-log-close')) {
document.getElementById('mb-log-modal').style.display = 'none';
}
});
// Browse Archive modal handler
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
var units = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
if (i >= units.length) i = units.length - 1;
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
}
function browseSetMessage(tbody, message, cssClass) {
tbody.textContent = '';
var tr = document.createElement('tr');
var td = document.createElement('td');
td.setAttribute('colspan', '3');
td.className = cssClass || 'text-center';
td.textContent = message;
tr.appendChild(td);
tbody.appendChild(tr);
}
function browseAddFileRow(tbody, file) {
var tr = document.createElement('tr');
var tdName = document.createElement('td');
tdName.style.wordBreak = 'break-all';
tdName.style.fontSize = '0.85rem';
var code = document.createElement('code');
code.textContent = file.name;
tdName.appendChild(code);
tr.appendChild(tdName);
var tdSize = document.createElement('td');
tdSize.className = 'text-end text-nowrap';
tdSize.textContent = formatFileSize(file.size);
tr.appendChild(tdSize);
var tdComp = document.createElement('td');
tdComp.className = 'text-end text-nowrap';
tdComp.textContent = formatFileSize(file.compressed_size);
tr.appendChild(tdComp);
tbody.appendChild(tr);
}
document.addEventListener('click', function(e) {
var btn = e.target.closest('.mb-browse-archive');
if (!btn) return;
e.preventDefault();
var recordId = btn.getAttribute('data-id');
var modal = document.getElementById('mb-browse-modal');
var tbody = document.getElementById('mb-browse-tbody');
var summary = document.getElementById('mb-browse-summary');
browseSetMessage(tbody, 'Loading...');
summary.textContent = '';
modal.style.display = 'block';
postAjax({ task: 'ajax.browseArchive', id: recordId })
.then(function(data) {
if (data.error) {
browseSetMessage(tbody, data.message || 'Error', 'text-danger');
return;
}
tbody.textContent = '';
if (data.files.length === 0) {
browseSetMessage(tbody, 'Archive is empty', 'text-center text-muted');
} else {
for (var i = 0; i < data.files.length; i++) {
browseAddFileRow(tbody, data.files[i]);
}
}
var text = data.total_files + ' files, ' + formatFileSize(data.total_size) + ' uncompressed';
if (data.truncated) {
text += ' (showing first ' + data.files.length + ')';
}
summary.textContent = text;
})
.catch(function(err) {
browseSetMessage(tbody, 'Error: ' + err.message, 'text-danger');
});
});
document.addEventListener('click', function(e) {
if (e.target.id === 'mb-browse-modal' || e.target.classList.contains('mb-browse-close')) {
document.getElementById('mb-browse-modal').style.display = 'none';
}
});
})();
</script>
<!-- Restore Confirmation Modal -->
<div id="mb-restore-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:8% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?></h4>
<button type="button" class="btn-close mb-restore-close" aria-label="Close"></button>
<div class="modal fade" id="mb-restore-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.restore'); ?>" method="post" id="mb-restore-form">
<input type="hidden" name="id" id="mb-restore-record-id" value="">
<div class="modal-body">
<div class="alert alert-danger">
<span class="icon-warning-circle" aria-hidden="true"></span>
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_CONFIRM'); ?></strong>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="restore_files" value="1" id="mb-restore-files" checked>
<label class="form-check-label" for="mb-restore-files">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_FILES'); ?>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="restore_db" value="1" id="mb-restore-db" checked>
<label class="form-check-label" for="mb-restore-db">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_DATABASE'); ?>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="preserve_config" value="1" id="mb-restore-config" checked>
<label class="form-check-label" for="mb-restore-config">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG'); ?>
<small class="text-muted d-block"><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG_DESC'); ?></small>
</label>
</div>
</div>
<div class="mb-3">
<label for="mb-restore-password" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD'); ?></label>
<input type="password" class="form-control" id="mb-restore-password" name="encryption_password"
placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PASSWORD_PLACEHOLDER'); ?>" autocomplete="off">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger">
<span class="icon-upload" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.restore'); ?>" method="post" id="mb-restore-form">
<input type="hidden" name="id" id="mb-restore-record-id" value="">
<div style="padding:1.5rem;">
<div class="alert alert-danger">
<span class="icon-warning-circle" aria-hidden="true"></span>
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_CONFIRM'); ?></strong>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="restore_files" value="1" id="mb-restore-files" checked>
<label class="form-check-label" for="mb-restore-files">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_FILES'); ?>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="restore_db" value="1" id="mb-restore-db" checked>
<label class="form-check-label" for="mb-restore-db">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_DATABASE'); ?>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="preserve_config" value="1" id="mb-restore-config" checked>
<label class="form-check-label" for="mb-restore-config">
<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG'); ?>
<small class="text-muted d-block"><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PRESERVE_CONFIG_DESC'); ?></small>
</label>
</div>
</div>
<div class="mb-3">
<label for="mb-restore-password" class="form-label"><?php echo Text::_('COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD'); ?></label>
<input type="password" class="form-control" id="mb-restore-password" name="encryption_password"
placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PASSWORD_PLACEHOLDER'); ?>" autocomplete="off">
</div>
</div>
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
<button type="button" class="btn btn-secondary mb-restore-close"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger">
<span class="icon-upload" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
<!-- Restore Progress Modal -->
<div id="mb-restore-progress-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-restore-title" style="margin:0 0 1rem;">Restore in Progress</h3>
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
<div id="mb-restore-progress-bar" style="height:100%; background:#dc3545; 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-restore-status" style="color:#666; font-size:0.9rem; margin:0.5rem 0;">Initializing...</p>
<p id="mb-restore-phase" style="color:#999; font-size:0.8rem; margin:0;">Phase: init</p>
</div>
</div>
<!-- Log Viewer Modal -->
<div id="mb-log-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:700px; margin:5% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column; max-height:80vh;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?></h4>
<button type="button" class="btn-close mb-log-close" aria-label="Close"></button>
</div>
<div style="padding:1rem 1.5rem; overflow-y:auto; flex:1;">
<pre id="mb-log-body" style="white-space:pre-wrap; word-break:break-word; font-size:0.85rem; margin:0; background:#f8f9fa; padding:1rem; border-radius:4px;"></pre>
<div class="modal fade" id="mb-restore-progress-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mb-restore-title">Restore in Progress</h5>
</div>
<div class="modal-body">
<div class="progress mb-2" style="height:24px;">
<div id="mb-restore-progress-bar" class="progress-bar bg-danger" role="progressbar" style="width:0%;">0%</div>
</div>
<p id="mb-restore-status" class="text-muted mb-1" style="font-size:0.9rem;">Initializing...</p>
<p id="mb-restore-phase" class="text-muted mb-0" style="font-size:0.8rem;">Phase: init</p>
</div>
</div>
</div>
</div>
<!-- Archive Browser Modal -->
<div id="mb-browse-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:800px; margin:5% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column; max-height:80vh;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;">
<span class="icon-folder-open" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>
</h4>
<button type="button" class="btn-close mb-browse-close" aria-label="Close"></button>
</div>
<div style="padding:0.75rem 1.5rem; border-bottom:1px solid #dee2e6; background:#f8f9fa;">
<small id="mb-browse-summary" class="text-muted"></small>
</div>
<div style="padding:0; overflow-y:auto; flex:1;">
<table class="table table-sm table-striped mb-0">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_NAME'); ?></th>
<th class="text-end" style="width:100px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_SIZE'); ?></th>
<th class="text-end" style="width:120px;"><?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_COL_COMPRESSED'); ?></th>
</tr>
</thead>
<tbody id="mb-browse-tbody">
</tbody>
</table>
</div>
</div>
</div>
<!-- Purge Backups Modal -->
<?php $canDelete = $user->authorise('core.delete', 'com_mokosuitebackup'); ?>
<?php if ($canDelete) : ?>
<div id="mb-purge-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:8% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;">
<span class="icon-trash" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_TITLE'); ?>
</h4>
<button type="button" class="btn-close mb-purge-close" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.purge'); ?>" method="post" id="mb-purge-form">
<div style="padding:1.5rem;">
<p><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DESC'); ?></p>
<div class="mb-3">
<label for="mb-purge-date" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DATE_LABEL'); ?></label>
<input type="date" class="form-control" id="mb-purge-date" name="purge_date" required>
</div>
<div id="mb-purge-count-wrapper" style="display:none;">
<div class="alert alert-danger mb-0" id="mb-purge-count-msg"></div>
</div>
<div id="mb-purge-none-wrapper" style="display:none;">
<div class="alert alert-info mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND'); ?></div>
</div>
</div>
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
<button type="button" class="btn btn-secondary mb-purge-close"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger" id="mb-purge-submit" disabled>
<div class="modal fade" id="mb-purge-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<span class="icon-trash" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_SUBMIT'); ?>
</button>
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_TITLE'); ?>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.purge'); ?>" method="post" id="mb-purge-form">
<div class="modal-body">
<p><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DESC'); ?></p>
<div class="mb-3">
<label for="mb-purge-date" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DATE_LABEL'); ?></label>
<input type="date" class="form-control" id="mb-purge-date" name="purge_date" required>
</div>
<div id="mb-purge-count-wrapper" style="display:none;">
<div class="alert alert-danger mb-0" id="mb-purge-count-msg"></div>
</div>
<div id="mb-purge-none-wrapper" style="display:none;">
<div class="alert alert-info mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND'); ?></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger" id="mb-purge-submit" disabled>
<span class="icon-trash" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_SUBMIT'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
</div>
<?php endif; ?>
<!-- Backup Comparison Modal -->
<div id="mb-compare-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:800px; margin:5% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column; max-height:85vh;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;">
<span class="icon-copy" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TITLE'); ?>
</h4>
<button type="button" class="btn-close mb-compare-close" aria-label="Close"></button>
</div>
<div style="padding:1rem 1.5rem; overflow-y:auto; flex:1;">
<div id="mb-compare-loading" style="text-align:center; padding:2rem;">
<span class="icon-spinner icon-spin" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_LOADING'); ?>
<div class="modal fade" id="mb-compare-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<span class="icon-copy" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TITLE'); ?>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="max-height:65vh; overflow-y:auto;">
<div id="mb-compare-loading" class="text-center py-4">
<span class="icon-spinner icon-spin" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_LOADING'); ?>
</div>
<div id="mb-compare-error" style="display:none;" class="alert alert-danger"></div>
<table id="mb-compare-table" class="table table-striped" style="display:none;">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_FIELD'); ?></th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 1</th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 2</th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DELTA'); ?></th>
</tr>
</thead>
<tbody id="mb-compare-body"></tbody>
</table>
</div>
<div id="mb-compare-error" style="display:none;" class="alert alert-danger"></div>
<table id="mb-compare-table" class="table table-striped" style="display:none;">
<thead>
<tr>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_FIELD'); ?></th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 1</th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_BACKUP'); ?> 2</th>
<th><?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_DELTA'); ?></th>
</tr>
</thead>
<tbody id="mb-compare-body"></tbody>
</table>
</div>
</div>
</div>
@@ -807,7 +585,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
var table = document.getElementById('mb-compare-table');
var body = document.getElementById('mb-compare-body');
modal.style.display = 'block';
bootstrap.Modal.getOrCreateInstance(modal).show();
loading.style.display = 'block';
errorEl.style.display = 'none';
table.style.display = 'none';
@@ -874,12 +652,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
});
}
// Close compare modal
document.addEventListener('click', function(e) {
if (e.target.id === 'mb-compare-modal' || e.target.classList.contains('mb-compare-close')) {
document.getElementById('mb-compare-modal').style.display = 'none';
}
});
// Compare modal close handled by Bootstrap data-bs-dismiss
// Intercept Compare toolbar button
document.addEventListener('DOMContentLoaded', function() {
@@ -922,7 +695,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
document.getElementById('mb-purge-count-wrapper').style.display = 'none';
document.getElementById('mb-purge-none-wrapper').style.display = 'none';
document.getElementById('mb-purge-submit').disabled = true;
document.getElementById('mb-purge-modal').style.display = 'block';
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-purge-modal')).show();
return false;
}, true);
}
@@ -936,12 +709,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
});
}
// Close modal
document.addEventListener('click', function(e) {
if (e.target.id === 'mb-purge-modal' || e.target.classList.contains('mb-purge-close')) {
document.getElementById('mb-purge-modal').style.display = 'none';
}
});
// Purge modal close handled by Bootstrap data-bs-dismiss
// Confirm on submit
var purgeForm = document.getElementById('mb-purge-form');
@@ -238,6 +238,7 @@ document.querySelectorAll('.mb-tile').forEach(function(tile) {
<select id="mb-profile-select" class="form-select mb-2">
<?php foreach ($this->profiles as $profile) : ?>
<option value="<?php echo (int) $profile->id; ?>">
#<?php echo (int) $profile->id; ?> —
<?php echo $this->escape($profile->title); ?>
(<?php echo $this->escape($profile->backup_type); ?>)
</option>
@@ -52,9 +52,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.published', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10 text-center">
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
</th>
<th scope="col" class="w-5">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
@@ -87,16 +84,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<td>
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'profiles.'); ?>
</td>
<td class="text-center">
<?php if ($item->published == 1) : ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
class="btn btn-sm btn-outline-success"
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>">
<span class="icon-play" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>
</a>
<?php endif; ?>
</td>
<td>
<?php echo (int) $item->id; ?>
</td>
@@ -132,117 +132,121 @@ $listDirn = $this->escape($this->state->get('list.direction'));
</form>
<!-- Create Snapshot Modal -->
<div id="mb-snapshot-create-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:8% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE'); ?></h4>
<button type="button" class="btn-close mb-modal-close" aria-label="Close"></button>
<div class="modal fade" id="mb-snapshot-create-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.create'); ?>" method="post" id="mb-snapshot-create-form">
<div class="modal-body">
<div class="mb-3">
<label for="mb-snap-desc" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION'); ?></label>
<input type="text" class="form-control" id="mb-snap-desc" name="description" placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_DESC_PLACEHOLDER'); ?>">
</div>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_SELECT_TYPES'); ?></label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="content_types[]" value="articles" id="mb-snap-articles" checked>
<label class="form-check-label" for="mb-snap-articles">
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_ARTICLES'); ?>
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_ARTICLES_DESC'); ?>)</small>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="content_types[]" value="categories" id="mb-snap-categories" checked>
<label class="form-check-label" for="mb-snap-categories">
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CATEGORIES'); ?>
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CATEGORIES_DESC'); ?>)</small>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="content_types[]" value="modules" id="mb-snap-modules" checked>
<label class="form-check-label" for="mb-snap-modules">
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODULES'); ?>
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODULES_DESC'); ?>)</small>
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-primary">
<span class="icon-camera" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.create'); ?>" method="post" id="mb-snapshot-create-form">
<div style="padding:1.5rem;">
<div class="mb-3">
<label for="mb-snap-desc" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION'); ?></label>
<input type="text" class="form-control" id="mb-snap-desc" name="description" placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_DESC_PLACEHOLDER'); ?>">
</div>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_SELECT_TYPES'); ?></label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="content_types[]" value="articles" id="mb-snap-articles" checked>
<label class="form-check-label" for="mb-snap-articles">
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_ARTICLES'); ?>
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_ARTICLES_DESC'); ?>)</small>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="content_types[]" value="categories" id="mb-snap-categories" checked>
<label class="form-check-label" for="mb-snap-categories">
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CATEGORIES'); ?>
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CATEGORIES_DESC'); ?>)</small>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="content_types[]" value="modules" id="mb-snap-modules" checked>
<label class="form-check-label" for="mb-snap-modules">
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODULES'); ?>
<small class="text-muted">(<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODULES_DESC'); ?>)</small>
</label>
</div>
</div>
</div>
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
<button type="button" class="btn btn-secondary mb-modal-close"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-primary">
<span class="icon-camera" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
<!-- Restore Snapshot Modal -->
<div id="mb-snapshot-restore-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:8% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE'); ?></h4>
<button type="button" class="btn-close mb-modal-close" aria-label="Close"></button>
<div class="modal fade" id="mb-snapshot-restore-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.restore'); ?>" method="post" id="mb-snapshot-restore-form">
<input type="hidden" name="id" id="mb-restore-id" value="">
<div class="modal-body">
<p id="mb-restore-desc" class="fw-bold"></p>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_MODE'); ?></label>
<div class="form-check">
<input class="form-check-input" type="radio" name="restore_mode" value="replace" id="mb-mode-replace" checked>
<label class="form-check-label" for="mb-mode-replace">
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_REPLACE'); ?></strong>
<br><small class="text-muted"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_REPLACE_DESC'); ?></small>
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="radio" name="restore_mode" value="merge" id="mb-mode-merge">
<label class="form-check-label" for="mb-mode-merge">
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_MERGE'); ?></strong>
<br><small class="text-muted"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_MERGE_DESC'); ?></small>
</label>
</div>
</div>
<div class="mb-3" id="mb-restore-types-container">
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_TYPES'); ?></label>
</div>
<div class="alert alert-warning mb-0" id="mb-replace-warning">
<span class="icon-warning-circle" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_REPLACE_WARNING'); ?>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger">
<span class="icon-upload" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.restore'); ?>" method="post" id="mb-snapshot-restore-form">
<input type="hidden" name="id" id="mb-restore-id" value="">
<div style="padding:1.5rem;">
<p id="mb-restore-desc" class="fw-bold"></p>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_MODE'); ?></label>
<div class="form-check">
<input class="form-check-input" type="radio" name="restore_mode" value="replace" id="mb-mode-replace" checked>
<label class="form-check-label" for="mb-mode-replace">
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_REPLACE'); ?></strong>
<br><small class="text-muted"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_REPLACE_DESC'); ?></small>
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="radio" name="restore_mode" value="merge" id="mb-mode-merge">
<label class="form-check-label" for="mb-mode-merge">
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_MERGE'); ?></strong>
<br><small class="text-muted"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_MODE_MERGE_DESC'); ?></small>
</label>
</div>
</div>
<div class="mb-3" id="mb-restore-types-container">
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_TYPES'); ?></label>
<!-- Populated by JS from data-types -->
</div>
<div class="alert alert-warning mb-0" id="mb-replace-warning">
<span class="icon-warning-circle" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_REPLACE_WARNING'); ?>
</div>
</div>
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
<button type="button" class="btn btn-secondary mb-modal-close"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-danger">
<span class="icon-upload" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE'); ?>
</button>
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
<!-- Browse Snapshot Detail Modal -->
<div id="mb-snapshot-browse-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:800px; margin:5% auto; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.3); max-height:80vh; display:flex; flex-direction:column;">
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
<h4 style="margin:0;" id="mb-browse-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_BROWSE'); ?></h4>
<button type="button" class="btn-close mb-modal-close" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.restoreSelected'); ?>" method="post" id="mb-snapshot-browse-form">
<input type="hidden" name="id" id="mb-browse-id" value="">
<div style="padding:1rem 1.5rem; overflow-y:auto; flex:1;">
<div class="modal fade" id="mb-snapshot-browse-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mb-browse-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_BROWSE'); ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.restoreSelected'); ?>" method="post" id="mb-snapshot-browse-form">
<input type="hidden" name="id" id="mb-browse-id" value="">
<div class="modal-body" style="max-height:60vh; overflow-y:auto;">
<div id="mb-browse-loading" class="text-center py-4">
<span class="spinner-border spinner-border-sm" role="status"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_LOADING'); ?>
@@ -331,8 +335,8 @@ $listDirn = $this->escape($this->state->get('list.direction'));
</div>
</div>
</div>
<div style="padding:0.75rem 1.5rem; border-top:1px solid #dee2e6; text-align:right;">
<button type="button" class="btn btn-secondary mb-modal-close"><?php echo Text::_('JCANCEL'); ?></button>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
<button type="submit" class="btn btn-success" id="mb-browse-restore-btn" disabled>
<span class="icon-upload" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_SELECTED'); ?>
@@ -340,6 +344,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
</div>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
</div>
</div>
</div>
@@ -352,7 +357,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
createBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('mb-snapshot-create-modal').style.display = 'block';
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-snapshot-create-modal')).show();
return false;
}, true);
}
@@ -413,7 +418,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
// Show/hide replace warning based on mode
toggleReplaceWarning();
document.getElementById('mb-snapshot-restore-modal').style.display = 'block';
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-snapshot-restore-modal')).show();
});
// Toggle warning when mode changes
@@ -454,7 +459,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
tab.show();
}
document.getElementById('mb-snapshot-browse-modal').style.display = 'block';
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-snapshot-browse-modal')).show();
// Fetch snapshot content via AJAX
var token = <?php echo json_encode(Session::getFormToken()); ?>;
@@ -617,16 +622,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
: <?php echo json_encode(Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_SELECTED')); ?>;
}
// Close modals
document.addEventListener('click', function(e) {
if (e.target.classList.contains('mb-modal-close') ||
e.target.id === 'mb-snapshot-create-modal' ||
e.target.id === 'mb-snapshot-restore-modal' ||
e.target.id === 'mb-snapshot-browse-modal') {
document.getElementById('mb-snapshot-create-modal').style.display = 'none';
document.getElementById('mb-snapshot-restore-modal').style.display = 'none';
document.getElementById('mb-snapshot-browse-modal').style.display = 'none';
}
});
// Modal close handled by Bootstrap data-bs-dismiss
})();
</script>
@@ -8,7 +8,7 @@
-->
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokosuitebackup_cpanel</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-23</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -120,7 +120,7 @@ $moduleId = 'mod-msb-cpanel-' . $displayData['module']->id;
class="btn btn-sm btn-outline-primary msb-cpanel-backup-btn"
data-profile-id="<?php echo (int) $profile->id; ?>"
data-module-id="<?php echo $moduleId; ?>">
<?php echo htmlspecialchars($profile->title); ?>
#<?php echo (int) $profile->id; ?> <?php echo htmlspecialchars($profile->title); ?>
<span class="badge bg-secondary ms-1"><?php echo htmlspecialchars($profile->backup_type); ?></span>
</button>
<?php endforeach; ?>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="actionlog" method="upgrade">
<name>Action Log - MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="console" method="upgrade">
<name>Console - MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>Content - MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade">
<name>Quick Icon - MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>System - MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="task" method="upgrade">
<name>Task - MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -7,7 +7,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>Web Services - MokoSuiteBackup</name>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename>
<version>01.43.00</version>
<version>01.43.12</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>