Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a027d6245 | |||
| 8af19f875c | |||
| b56e4060bf | |||
| 9757658c34 | |||
| c82378128a | |||
| f95505704a | |||
| 6cdc9b04d0 | |||
| bad73529ae | |||
| 288baf41d3 | |||
| 7d1dcf3e1c | |||
| 2002c1fcad | |||
| 4abe81f916 | |||
| 571b03743f | |||
| 7fc1cad305 | |||
| 03a1dd75c9 | |||
| 02d8312d1b | |||
| c508fcc8d5 | |||
| d104b7b936 | |||
| 80110ac111 | |||
| 3bd1f63833 | |||
| 93f0fa0a47 | |||
| 268b3d54d7 | |||
| 1cfe7c6c6e | |||
| f0da0c02b4 | |||
| 2f8a65388c | |||
| 9978622960 | |||
| 35e5fc1503 | |||
| 2338ba5197 | |||
| e67eedbc93 | |||
| d812aca832 | |||
| 4315f36c6a | |||
| 10467835ac | |||
| f26d58504e | |||
| 07fb4dcc24 | |||
| 21a4352b3b | |||
| 9d26f59f98 | |||
| 3488434f28 | |||
| f97cd30c95 | |||
| 836d1bc8b7 | |||
| 79b3caa35a | |||
| 6102c8f590 | |||
| 88e53c5698 | |||
| ec1c3486c5 | |||
| 3742477aef | |||
| bb8e4a258a | |||
| e6d646011a | |||
| 726291995c | |||
| 2ac4923d74 | |||
| adc4935587 | |||
| 8f7b747c59 | |||
| 42b7503d7b | |||
| 9ac8757a8c | |||
| ef3fde1c39 | |||
| 5750e71d15 | |||
| c8e022d46b | |||
| 21f2ba0eff | |||
| 821c4bae11 | |||
| e86c104276 | |||
| 4df70531e2 | |||
| 845b856cda | |||
| 633e9b7f1e | |||
| ec0b7eb8a4 | |||
| 7d119565da | |||
| 9db7331a72 | |||
| 32931c1e37 |
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "source/packages/MokoSuiteClient"]
|
||||||
|
path = source/packages/MokoSuiteClient
|
||||||
|
url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient.git
|
||||||
@@ -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:
|
|
||||||
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
|
||||||
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_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,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.42.00
|
# VERSION: 01.43.32
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Update submodules to main
|
||||||
|
run: |
|
||||||
|
if [ -f .gitmodules ]; then
|
||||||
|
git submodule foreach 'git checkout main && git pull origin main' 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup mokocli tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
|
|||||||
+28
-1
@@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
||||||
|
|
||||||
|
|
||||||
|
## [01.43.00] --- 2026-06-24
|
||||||
|
|
||||||
## [01.42.00] --- 2026-06-23
|
## [01.42.00] --- 2026-06-23
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +93,7 @@
|
|||||||
- Backup comparison: select two backups for side-by-side diff
|
- Backup comparison: select two backups for side-by-side diff
|
||||||
- Archive browser: view files inside backup without extracting
|
- Archive browser: view files inside backup without extracting
|
||||||
- Manual purge: delete backups older than a date with count preview
|
- 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
|
- "Do not navigate away" warning in backup/restore progress modals
|
||||||
- Clickable placeholder pills for backup directory and archive name fields
|
- Clickable placeholder pills for backup directory and archive name fields
|
||||||
- Comprehensive help modal with absolute/relative/placeholder path documentation
|
- Comprehensive help modal with absolute/relative/placeholder path documentation
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Package** | `pkg_mokosuitebackup` |
|
| **Package** | `pkg_mokosuitebackup` |
|
||||||
| **Type** | Joomla Package (8 sub-extensions) |
|
| **Type** | Joomla Package (9 sub-extensions + MokoSuiteClient) |
|
||||||
| **Joomla** | 6.x+ |
|
| **Joomla** | 6.x+ |
|
||||||
| **PHP** | 8.1+ |
|
| **PHP** | 8.1+ |
|
||||||
| **License** | GPL-3.0-or-later |
|
| **License** | GPL-3.0-or-later |
|
||||||
@@ -30,7 +30,8 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
|||||||
- Scheduled snapshot task via com_scheduler
|
- Scheduled snapshot task via com_scheduler
|
||||||
|
|
||||||
### Remote Storage
|
### Remote Storage
|
||||||
- SFTP with SSH key file authentication (key stored base64-encoded in database)
|
- Multi-remote — upload to multiple destinations per profile simultaneously
|
||||||
|
- SFTP with SSH key file auth + remote directory browser
|
||||||
- Amazon S3 and S3-compatible (DigitalOcean Spaces, Wasabi, MinIO)
|
- Amazon S3 and S3-compatible (DigitalOcean Spaces, Wasabi, MinIO)
|
||||||
- Google Drive with OAuth2 and resumable uploads
|
- Google Drive with OAuth2 and resumable uploads
|
||||||
- Graceful degradation — local backup preserved if upload fails
|
- Graceful degradation — local backup preserved if upload fails
|
||||||
@@ -66,6 +67,10 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
|||||||
- Snapshots: create, list, restore, delete, download
|
- Snapshots: create, list, restore, delete, download
|
||||||
- Profile credentials masked in API responses
|
- Profile credentials masked in API responses
|
||||||
|
|
||||||
|
### Bundled: MokoSuiteClient
|
||||||
|
- Full MokoSuiteClient package installed automatically alongside MokoSuiteBackup
|
||||||
|
- Provides admin dashboard, security firewall, tenant management, and developer tools
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Download from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/releases)
|
1. Download from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/releases)
|
||||||
|
|||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
|
This file is part of a Moko Consulting project.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# FILE INFORMATION
|
||||||
|
DEFGROUP: Template-Joomla
|
||||||
|
INGROUP: Template-Joomla.Documentation
|
||||||
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||||
|
PATH: /SECURITY.md
|
||||||
|
VERSION: 01.43.32
|
||||||
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Purpose and Scope
|
||||||
|
|
||||||
|
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Security updates are provided for the following versions:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 01.x.x | :white_check_mark: |
|
||||||
|
| < 01.0 | :x: |
|
||||||
|
|
||||||
|
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
### Where to Report
|
||||||
|
|
||||||
|
**DO NOT** create public GitHub issues for security vulnerabilities.
|
||||||
|
|
||||||
|
Report security vulnerabilities privately to:
|
||||||
|
|
||||||
|
**Email**: `security@mokoconsulting.tech`
|
||||||
|
|
||||||
|
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
|
||||||
|
|
||||||
|
### What to Include
|
||||||
|
|
||||||
|
A complete vulnerability report should include:
|
||||||
|
|
||||||
|
1. **Description**: Clear explanation of the vulnerability
|
||||||
|
2. **Impact**: Potential security impact and severity assessment
|
||||||
|
3. **Affected Versions**: Which versions are vulnerable
|
||||||
|
4. **Reproduction Steps**: Detailed steps to reproduce the issue
|
||||||
|
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
|
||||||
|
6. **Suggested Fix**: Proposed remediation (if known)
|
||||||
|
7. **Disclosure Timeline**: Your expectations for public disclosure
|
||||||
|
|
||||||
|
### Response Timeline
|
||||||
|
|
||||||
|
* **Initial Response**: Within 3 business days
|
||||||
|
* **Assessment Complete**: Within 7 business days
|
||||||
|
* **Fix Timeline**: Depends on severity (see below)
|
||||||
|
* **Disclosure**: Coordinated with reporter
|
||||||
|
|
||||||
|
## Severity Classification
|
||||||
|
|
||||||
|
Vulnerabilities are classified using the following severity levels:
|
||||||
|
|
||||||
|
### Critical
|
||||||
|
* Remote code execution
|
||||||
|
* Authentication bypass
|
||||||
|
* Data breach or exposure of sensitive information
|
||||||
|
* **Fix Timeline**: 7 days
|
||||||
|
|
||||||
|
### High
|
||||||
|
* Privilege escalation
|
||||||
|
* SQL injection or command injection
|
||||||
|
* Cross-site scripting (XSS) with significant impact
|
||||||
|
* **Fix Timeline**: 14 days
|
||||||
|
|
||||||
|
### Medium
|
||||||
|
* Information disclosure (limited scope)
|
||||||
|
* Denial of service
|
||||||
|
* Security misconfigurations with moderate impact
|
||||||
|
* **Fix Timeline**: 30 days
|
||||||
|
|
||||||
|
### Low
|
||||||
|
* Security best practice violations
|
||||||
|
* Minor information leaks
|
||||||
|
* Issues requiring user interaction or complex preconditions
|
||||||
|
* **Fix Timeline**: 60 days or next release
|
||||||
|
|
||||||
|
## Remediation Process
|
||||||
|
|
||||||
|
1. **Acknowledgment**: Security team confirms receipt and begins investigation
|
||||||
|
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
|
||||||
|
3. **Development**: Security patch is developed and tested
|
||||||
|
4. **Review**: Patch undergoes security review and validation
|
||||||
|
5. **Release**: Fixed version is released with security advisory
|
||||||
|
6. **Disclosure**: Public disclosure follows coordinated timeline
|
||||||
|
|
||||||
|
## Security Advisories
|
||||||
|
|
||||||
|
Security advisories are published via:
|
||||||
|
|
||||||
|
* GitHub Security Advisories
|
||||||
|
* Release notes and CHANGELOG.md
|
||||||
|
* Email notification to project users (if mailing list is established)
|
||||||
|
|
||||||
|
Advisories include:
|
||||||
|
|
||||||
|
* CVE identifier (if applicable)
|
||||||
|
* Severity rating
|
||||||
|
* Affected versions
|
||||||
|
* Fixed versions
|
||||||
|
* Mitigation steps
|
||||||
|
* Attribution (with reporter consent)
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
For projects using this template:
|
||||||
|
|
||||||
|
### Required Controls
|
||||||
|
|
||||||
|
* Enable GitHub security features (Dependabot, code scanning)
|
||||||
|
* Implement branch protection on `main`
|
||||||
|
* Require code review for all changes
|
||||||
|
* Enforce signed commits (recommended)
|
||||||
|
* Use secrets management (never commit credentials)
|
||||||
|
* Maintain security documentation
|
||||||
|
* Follow secure coding standards defined in MokoStandards
|
||||||
|
|
||||||
|
### Joomla Plugin Security
|
||||||
|
|
||||||
|
* Follow Joomla security best practices
|
||||||
|
* Validate and sanitize all user input
|
||||||
|
* Use Joomla's database API to prevent SQL injection
|
||||||
|
* Properly escape output to prevent XSS
|
||||||
|
* Implement proper access control checks
|
||||||
|
* Use Joomla's session and authentication APIs
|
||||||
|
* Keep Joomla and dependencies up to date
|
||||||
|
|
||||||
|
### CI/CD Security
|
||||||
|
|
||||||
|
* Validate all inputs
|
||||||
|
* Sanitize outputs
|
||||||
|
* Use least privilege access
|
||||||
|
* Pin dependencies with hash verification
|
||||||
|
* Scan for vulnerabilities in dependencies
|
||||||
|
* Audit third-party actions and tools
|
||||||
|
|
||||||
|
#### Automated Security Scanning
|
||||||
|
|
||||||
|
All repositories SHOULD implement:
|
||||||
|
|
||||||
|
**CodeQL Analysis**:
|
||||||
|
* Enabled for PHP and other supported languages
|
||||||
|
* Runs on: push to main, pull requests, weekly schedule
|
||||||
|
* Query sets: `security-extended` and `security-and-quality`
|
||||||
|
* Configuration: `.github/workflows/codeql-analysis.yml`
|
||||||
|
|
||||||
|
**Dependabot Security Updates**:
|
||||||
|
* Weekly scans for vulnerable dependencies
|
||||||
|
* Automated pull requests for security patches
|
||||||
|
* Configuration: `.github/dependabot.yml`
|
||||||
|
|
||||||
|
**Secret Scanning**:
|
||||||
|
* Enabled by default with push protection
|
||||||
|
* Prevents accidental credential commits
|
||||||
|
|
||||||
|
### Dependency Management
|
||||||
|
|
||||||
|
* Keep dependencies up to date
|
||||||
|
* Monitor security advisories for dependencies
|
||||||
|
* Remove unused dependencies
|
||||||
|
* Audit new dependencies before adoption
|
||||||
|
* Document security-critical dependencies
|
||||||
|
|
||||||
|
## Compliance and Governance
|
||||||
|
|
||||||
|
This security policy is aligned with MokoStandards. Deviations require documented justification.
|
||||||
|
|
||||||
|
Security policies are reviewed and updated at least annually or following significant security incidents.
|
||||||
|
|
||||||
|
## Attribution and Recognition
|
||||||
|
|
||||||
|
We acknowledge and appreciate responsible disclosure. With your permission, we will:
|
||||||
|
|
||||||
|
* Credit you in security advisories
|
||||||
|
* List you in CHANGELOG.md for the fix release
|
||||||
|
* Recognize your contribution publicly (if desired)
|
||||||
|
|
||||||
|
## Contact and Escalation
|
||||||
|
|
||||||
|
* **Security Team**: security@mokoconsulting.tech
|
||||||
|
* **Primary Contact**: hello@mokoconsulting.tech
|
||||||
|
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
The following are explicitly out of scope:
|
||||||
|
|
||||||
|
* Issues in third-party dependencies (report directly to maintainers)
|
||||||
|
* Social engineering attacks
|
||||||
|
* Physical security issues
|
||||||
|
* Denial of service via resource exhaustion without amplification
|
||||||
|
* Issues requiring physical access to systems
|
||||||
|
* Theoretical vulnerabilities without proof of exploitability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| Document | Security Policy |
|
||||||
|
| Path | /SECURITY.md |
|
||||||
|
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
|
||||||
|
| Owner | Moko Consulting |
|
||||||
|
| Scope | Security vulnerability handling |
|
||||||
|
| Status | Active |
|
||||||
|
| Effective | 2026-01-16 |
|
||||||
|
|
||||||
|
## Revision History
|
||||||
|
|
||||||
|
| Date | Change Description | Author |
|
||||||
|
| ---------- | ------------------------------------------------- | --------------- |
|
||||||
|
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
|
||||||
Submodule
+1
Submodule source/packages/MokoSuiteClient added at 64482e59cd
@@ -245,7 +245,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
label="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER"
|
label="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER"
|
||||||
description="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER_DESC"
|
description="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER_DESC"
|
||||||
default="https://ntfy.sh"
|
default="https://ntfy.mokoconsulting.tech"
|
||||||
filter="url"
|
filter="url"
|
||||||
/>
|
/>
|
||||||
<field
|
<field
|
||||||
|
|||||||
@@ -24,10 +24,9 @@
|
|||||||
name="fullordering"
|
name="fullordering"
|
||||||
type="list"
|
type="list"
|
||||||
label="JGLOBAL_SORT_BY"
|
label="JGLOBAL_SORT_BY"
|
||||||
default="a.ordering ASC"
|
default="a.id ASC"
|
||||||
onchange="this.form.submit();"
|
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 ASC">COM_MOKOJOOMBACKUP_HEADING_TITLE_ASC</option>
|
||||||
<option value="a.title DESC">COM_MOKOJOOMBACKUP_HEADING_TITLE_DESC</option>
|
<option value="a.title DESC">COM_MOKOJOOMBACKUP_HEADING_TITLE_DESC</option>
|
||||||
<option value="a.id DESC">JGRID_HEADING_ID_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="1">COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED</option>
|
||||||
<option value="standalone">COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE</option>
|
<option value="standalone">COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE</option>
|
||||||
</field>
|
</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
|
<field
|
||||||
name="encryption_password"
|
name="encryption_password"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -164,12 +174,6 @@
|
|||||||
<option value="1">JPUBLISHED</option>
|
<option value="1">JPUBLISHED</option>
|
||||||
<option value="0">JUNPUBLISHED</option>
|
<option value="0">JUNPUBLISHED</option>
|
||||||
</field>
|
</field>
|
||||||
<field
|
|
||||||
name="ordering"
|
|
||||||
type="number"
|
|
||||||
label="JFIELD_ORDERING_LABEL"
|
|
||||||
default="0"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset name="filters" label="COM_MOKOJOOMBACKUP_FIELDSET_FILTERS">
|
<fieldset name="filters" label="COM_MOKOJOOMBACKUP_FIELDSET_FILTERS">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
; @license GPL-3.0-or-later
|
; @license GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
COM_MOKOJOOMBACKUP="MokoSuiteBackup"
|
||||||
|
COM_MOKOJOOMBACKUP_CONFIGURATION="MokoSuiteBackup Options"
|
||||||
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
|
||||||
|
|
||||||
; Submenu
|
; Submenu
|
||||||
@@ -41,6 +42,8 @@ COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN="Storage by Profile"
|
|||||||
COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
|
COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
|
||||||
|
|
||||||
; Backups view
|
; Backups view
|
||||||
|
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED="%d backup records deleted."
|
||||||
|
COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED_1="%d backup record deleted."
|
||||||
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
COM_MOKOJOOMBACKUP_BACKUPS_TITLE="Backup Records"
|
||||||
COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
|
COM_MOKOJOOMBACKUP_BACKUPS_TABLE_CAPTION="Table of backup records"
|
||||||
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
||||||
@@ -139,6 +142,8 @@ COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="None: no restore script. Wrap
|
|||||||
COM_MOKOJOOMBACKUP_MOKORESTORE_NONE="None"
|
COM_MOKOJOOMBACKUP_MOKORESTORE_NONE="None"
|
||||||
COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED="Wrapped (inside backup ZIP)"
|
COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED="Wrapped (inside backup ZIP)"
|
||||||
COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE="Standalone (separate restore.php)"
|
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
|
; Data Sanitization
|
||||||
COM_MOKOJOOMBACKUP_FIELDSET_SANITIZATION="Data Sanitization"
|
COM_MOKOJOOMBACKUP_FIELDSET_SANITIZATION="Data Sanitization"
|
||||||
@@ -275,9 +280,9 @@ COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT_DESC="SSH port (default: 22)"
|
|||||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME="SSH Username"
|
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME="SSH Username"
|
||||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC="Username for SSH authentication"
|
COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC="Username for SSH authentication"
|
||||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD="SSH Password"
|
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="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_UPLOAD="Upload Key File"
|
||||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE="Replace Key"
|
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE="Replace Key"
|
||||||
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED="Key loaded"
|
COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED="Key loaded"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>MokoSuiteBackup</name>
|
<name>MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
|||||||
`remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
`remote_keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
||||||
`encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)',
|
`encryption_password` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AES-256 archive encryption password (blank = no encryption)',
|
||||||
`include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0' COMMENT 'MokoRestore mode: 0=none, 1=wrapped, standalone',
|
`include_mokorestore` VARCHAR(20) NOT NULL DEFAULT '0' COMMENT 'MokoRestore mode: 0=none, 1=wrapped, standalone',
|
||||||
|
`restore_script_name` VARCHAR(100) NOT NULL DEFAULT 'restore.php' COMMENT 'Custom restore script filename',
|
||||||
`sanitize_passwords` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Replace user password hashes with invalid value',
|
`sanitize_passwords` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Replace user password hashes with invalid value',
|
||||||
`preserve_super_admin` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep super admin password when sanitizing',
|
`preserve_super_admin` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep super admin password when sanitizing',
|
||||||
`sanitize_emails` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Replace user emails with dummy values',
|
`sanitize_emails` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Replace user emails with dummy values',
|
||||||
@@ -113,14 +114,13 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_remotes` (
|
|||||||
`title` VARCHAR(255) NOT NULL DEFAULT '',
|
`title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
`type` VARCHAR(20) NOT NULL DEFAULT 'sftp' COMMENT 'sftp, s3, google_drive',
|
`type` VARCHAR(20) NOT NULL DEFAULT 'sftp' COMMENT 'sftp, s3, google_drive',
|
||||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
`keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
`params` MEDIUMTEXT COMMENT 'JSON: type-specific settings',
|
||||||
`config` MEDIUMTEXT NOT NULL COMMENT 'JSON — type-specific settings',
|
|
||||||
`ordering` INT(11) NOT NULL DEFAULT 0,
|
`ordering` INT(11) NOT NULL DEFAULT 0,
|
||||||
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||||
`modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
`modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_profile` (`profile_id`),
|
KEY `idx_profile` (`profile_id`),
|
||||||
KEY `idx_enabled` (`enabled`)
|
KEY `idx_enabled` (`profile_id`, `enabled`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Insert default backup profile (IGNORE prevents duplicate key error on update)
|
-- Insert default backup profile (IGNORE prevents duplicate key error on update)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.11 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.19 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.20 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.21 — no schema changes */
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 01.43.22 — Add restore_script_name to profiles, align remotes schema
|
||||||
|
|
||||||
|
ALTER TABLE `#__mokosuitebackup_profiles`
|
||||||
|
ADD COLUMN `restore_script_name` VARCHAR(100) NOT NULL DEFAULT 'restore.php' COMMENT 'Custom restore script filename'
|
||||||
|
AFTER `include_mokorestore`;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.23 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.24 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.25 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.26 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.29 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.30 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.31 — no schema changes */
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* 01.43.32 — no schema changes */
|
||||||
@@ -924,11 +924,11 @@ class AjaxController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode JSON config and mask secrets
|
// Decode JSON params and mask secrets
|
||||||
$items = [];
|
$items = [];
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$config = json_decode($row->config, true) ?: [];
|
$config = json_decode($row->params, true) ?: [];
|
||||||
|
|
||||||
// Mask sensitive fields so they never leave the server in list views
|
// Mask sensitive fields so they never leave the server in list views
|
||||||
$masked = $this->maskSecrets($config, $row->type);
|
$masked = $this->maskSecrets($config, $row->type);
|
||||||
@@ -939,8 +939,7 @@ class AjaxController extends BaseController
|
|||||||
'title' => $row->title,
|
'title' => $row->title,
|
||||||
'type' => $row->type,
|
'type' => $row->type,
|
||||||
'enabled' => (int) $row->enabled,
|
'enabled' => (int) $row->enabled,
|
||||||
'keep_local' => (int) $row->keep_local,
|
'params' => $masked,
|
||||||
'config' => $masked,
|
|
||||||
'ordering' => (int) $row->ordering,
|
'ordering' => (int) $row->ordering,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -971,7 +970,6 @@ class AjaxController extends BaseController
|
|||||||
$title = trim($this->input->getString('remote_title', ''));
|
$title = trim($this->input->getString('remote_title', ''));
|
||||||
$type = $this->input->getCmd('remote_type', 'sftp');
|
$type = $this->input->getCmd('remote_type', 'sftp');
|
||||||
$enabled = $this->input->getInt('remote_enabled', 1);
|
$enabled = $this->input->getInt('remote_enabled', 1);
|
||||||
$keepLocal = $this->input->getInt('remote_keep_local', 1);
|
|
||||||
$configRaw = $this->input->getString('remote_config', '{}');
|
$configRaw = $this->input->getString('remote_config', '{}');
|
||||||
|
|
||||||
if (!$profileId) {
|
if (!$profileId) {
|
||||||
@@ -1019,9 +1017,7 @@ class AjaxController extends BaseController
|
|||||||
$table->title = $title;
|
$table->title = $title;
|
||||||
$table->type = $type;
|
$table->type = $type;
|
||||||
$table->enabled = $enabled ? 1 : 0;
|
$table->enabled = $enabled ? 1 : 0;
|
||||||
$table->keep_local = $keepLocal ? 1 : 0;
|
$table->params = json_encode($config);
|
||||||
$table->config = json_encode($config);
|
|
||||||
|
|
||||||
if (!$table->check() || !$table->store()) {
|
if (!$table->check() || !$table->store()) {
|
||||||
$this->sendJson(['error' => true, 'message' => $table->getError() ?: 'Save failed']);
|
$this->sendJson(['error' => true, 'message' => $table->getError() ?: 'Save failed']);
|
||||||
|
|
||||||
@@ -1190,7 +1186,7 @@ class AjaxController extends BaseController
|
|||||||
try {
|
try {
|
||||||
$db = Factory::getDbo();
|
$db = Factory::getDbo();
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName('config'))
|
->select($db->quoteName('params'))
|
||||||
->from($db->quoteName('#__mokosuitebackup_remotes'))
|
->from($db->quoteName('#__mokosuitebackup_remotes'))
|
||||||
->where($db->quoteName('id') . ' = ' . $id);
|
->where($db->quoteName('id') . ' = ' . $id);
|
||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
|
|||||||
@@ -259,14 +259,14 @@ class BackupEngine
|
|||||||
|
|
||||||
// Step 2.5: MokoRestore script (if enabled)
|
// Step 2.5: MokoRestore script (if enabled)
|
||||||
$mokoRestoreMode = $profile->include_mokorestore ?? '0';
|
$mokoRestoreMode = $profile->include_mokorestore ?? '0';
|
||||||
|
$restoreScriptName = $profile->restore_script_name ?? 'restore.php';
|
||||||
$restoreScriptPath = '';
|
$restoreScriptPath = '';
|
||||||
|
|
||||||
if ($mokoRestoreMode === '1') {
|
if ($mokoRestoreMode === '1') {
|
||||||
// Wrapped mode: backup ZIP inside an outer ZIP with restore.php
|
|
||||||
$this->log('Wrapping with MokoRestore script...');
|
$this->log('Wrapping with MokoRestore script...');
|
||||||
$mokoRestoreName = str_replace('.zip', '-mokorestore.zip', $archiveName);
|
$mokoRestoreName = str_replace('.zip', '-mokorestore.zip', $archiveName);
|
||||||
$mokoRestorePath = $this->backupDir . '/' . $mokoRestoreName;
|
$mokoRestorePath = $this->backupDir . '/' . $mokoRestoreName;
|
||||||
MokoRestore::wrap($archivePath, $mokoRestorePath);
|
MokoRestore::wrap($archivePath, $mokoRestorePath, $restoreScriptName);
|
||||||
|
|
||||||
if (is_file($archivePath) && !unlink($archivePath)) {
|
if (is_file($archivePath) && !unlink($archivePath)) {
|
||||||
$this->log('WARNING: Could not remove pre-wrap archive');
|
$this->log('WARNING: Could not remove pre-wrap archive');
|
||||||
@@ -278,11 +278,11 @@ class BackupEngine
|
|||||||
$this->log('MokoRestore archive created: ' . $sizeHuman);
|
$this->log('MokoRestore archive created: ' . $sizeHuman);
|
||||||
$this->log('SHA-256 (wrapped): ' . $checksum);
|
$this->log('SHA-256 (wrapped): ' . $checksum);
|
||||||
} elseif ($mokoRestoreMode === 'standalone') {
|
} elseif ($mokoRestoreMode === 'standalone') {
|
||||||
// Standalone mode: restore.php as a separate file next to the backup ZIP
|
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
|
||||||
$this->log('Generating standalone restore.php...');
|
$this->log('Generating standalone ' . $restoreScriptName . '...');
|
||||||
$restoreScriptPath = $this->backupDir . '/restore.php';
|
$restoreScriptPath = $this->backupDir . '/' . $restoreScriptName;
|
||||||
MokoRestore::generateStandalone($restoreScriptPath);
|
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 = '';
|
$remoteFilename = '';
|
||||||
@@ -303,9 +303,8 @@ class BackupEngine
|
|||||||
$remoteFilename = $result['remote_path'] ?? $archiveName;
|
$remoteFilename = $result['remote_path'] ?? $archiveName;
|
||||||
$this->log(' Upload complete: ' . $result['message']);
|
$this->log(' Upload complete: ' . $result['message']);
|
||||||
|
|
||||||
/* Upload standalone restore.php if in standalone mode */
|
|
||||||
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
||||||
$uploader->upload($restoreScriptPath, 'restore.php');
|
$uploader->upload($restoreScriptPath, basename($restoreScriptPath));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$uploadFailed = true;
|
$uploadFailed = true;
|
||||||
@@ -336,15 +335,15 @@ class BackupEngine
|
|||||||
$remoteFilename = $uploadResult['remote_path'] ?? $archiveName;
|
$remoteFilename = $uploadResult['remote_path'] ?? $archiveName;
|
||||||
$this->log('Remote upload complete: ' . $uploadResult['message']);
|
$this->log('Remote upload complete: ' . $uploadResult['message']);
|
||||||
|
|
||||||
// Upload standalone restore.php alongside the backup if in standalone mode
|
|
||||||
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
||||||
$this->log('Uploading standalone restore.php...');
|
$restoreBasename = basename($restoreScriptPath);
|
||||||
$restoreUpload = $uploader->upload($restoreScriptPath, 'restore.php');
|
$this->log('Uploading standalone ' . $restoreBasename . '...');
|
||||||
|
$restoreUpload = $uploader->upload($restoreScriptPath, $restoreBasename);
|
||||||
|
|
||||||
if ($restoreUpload['success']) {
|
if ($restoreUpload['success']) {
|
||||||
$this->log('Standalone restore.php uploaded');
|
$this->log('Standalone ' . $restoreBasename . ' uploaded');
|
||||||
} else {
|
} 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
|
* @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();
|
$zip = new \ZipArchive();
|
||||||
|
|
||||||
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath);
|
throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the standalone restore script
|
$zip->addFromString($scriptName, self::generateRestoreScript());
|
||||||
$zip->addFromString('restore.php', self::generateRestoreScript());
|
|
||||||
|
|
||||||
// Add the original backup as a nested ZIP
|
|
||||||
$zip->addFile($backupArchive, 'site-backup.zip');
|
$zip->addFile($backupArchive, 'site-backup.zip');
|
||||||
|
|
||||||
$zip->close();
|
$zip->close();
|
||||||
|
|
||||||
return $outputPath;
|
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.
|
* Generate the standalone restore.php script as a separate file.
|
||||||
*
|
*
|
||||||
@@ -165,7 +176,38 @@ SCANNER;
|
|||||||
$php
|
$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(
|
$php = str_replace(
|
||||||
"file_exists(BACKUP_FILE)",
|
"file_exists(BACKUP_FILE)",
|
||||||
"(getSelectedBackupFile() !== '' || file_exists(BACKUP_FILE))",
|
"(getSelectedBackupFile() !== '' || file_exists(BACKUP_FILE))",
|
||||||
@@ -174,30 +216,28 @@ SCANNER;
|
|||||||
|
|
||||||
$html = self::generateFrontend();
|
$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'
|
$selectorHtml = <<<'SELECTOR'
|
||||||
<!-- Backup File Selector (standalone mode) -->
|
<div id="mr-backup-selector" class="mb-3">
|
||||||
<div id="mr-step-select" class="mr-step" style="display:none;">
|
<label class="mr-field-label" style="font-weight:600;margin-bottom:8px;display:block;">Backup Archive</label>
|
||||||
<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>
|
<div id="mr-backup-list"></div>
|
||||||
<input type="hidden" name="backup_file" id="mr-backup-file" value="">
|
<input type="hidden" name="backup_file" id="mr-backup-file" value="">
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var backups = <?php echo json_encode(scanForBackups()); ?>;
|
var backups = <?php echo json_encode(scanForBackups()); ?>;
|
||||||
var list = document.getElementById('mr-backup-list');
|
var list = document.getElementById('mr-backup-list');
|
||||||
var hiddenInput = document.getElementById('mr-backup-file');
|
var hiddenInput = document.getElementById('mr-backup-file');
|
||||||
|
|
||||||
if (backups.length === 0) {
|
if (backups.length === 0) {
|
||||||
var alert = document.createElement('div');
|
var alert = document.createElement('div');
|
||||||
alert.className = 'mr-alert mr-alert-danger';
|
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.';
|
alert.textContent = 'No ZIP files found in this directory. Upload a backup archive first.';
|
||||||
list.appendChild(alert);
|
list.appendChild(alert);
|
||||||
} else if (backups.length === 1) {
|
} else if (backups.length === 1) {
|
||||||
hiddenInput.value = backups[0].name;
|
hiddenInput.value = backups[0].name;
|
||||||
var found = document.createElement('div');
|
var found = document.createElement('div');
|
||||||
found.className = 'mr-alert mr-alert-success';
|
found.style.cssText = 'padding:12px;background:#dcfce7;border:1px solid #bbf7d0;border-radius:6px;color:#16a34a;';
|
||||||
var strong = document.createElement('strong');
|
var strong = document.createElement('strong');
|
||||||
strong.textContent = backups[0].name;
|
strong.textContent = backups[0].name;
|
||||||
found.appendChild(document.createTextNode('Found: '));
|
found.appendChild(document.createTextNode('Found: '));
|
||||||
@@ -205,34 +245,54 @@ SCANNER;
|
|||||||
found.appendChild(document.createTextNode(' (' + (backups[0].size / 1048576).toFixed(1) + ' MB)'));
|
found.appendChild(document.createTextNode(' (' + (backups[0].size / 1048576).toFixed(1) + ' MB)'));
|
||||||
list.appendChild(found);
|
list.appendChild(found);
|
||||||
} else {
|
} else {
|
||||||
var group = document.createElement('div');
|
var hint = document.createElement('div');
|
||||||
group.className = 'mr-field-group';
|
hint.style.cssText = 'padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;color:#1d4ed8;margin-bottom:8px;font-size:0.9em;';
|
||||||
backups.forEach(function(b) {
|
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');
|
var label = document.createElement('label');
|
||||||
label.style.cssText = 'display:block; padding:8px; margin:4px 0; border:1px solid #ddd; border-radius:4px; cursor:pointer;';
|
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');
|
var radio = document.createElement('input');
|
||||||
radio.type = 'radio';
|
radio.type = 'radio';
|
||||||
radio.name = 'backup_choice';
|
radio.name = 'backup_choice';
|
||||||
radio.value = b.name;
|
radio.value = b.name;
|
||||||
radio.style.marginRight = '8px';
|
radio.style.marginRight = '10px';
|
||||||
|
if (i === 0) { radio.checked = true; hiddenInput.value = b.name; }
|
||||||
radio.addEventListener('change', function() { hiddenInput.value = this.value; });
|
radio.addEventListener('change', function() { hiddenInput.value = this.value; });
|
||||||
label.appendChild(radio);
|
label.appendChild(radio);
|
||||||
|
var info = document.createElement('div');
|
||||||
var nameStrong = document.createElement('strong');
|
var nameStrong = document.createElement('strong');
|
||||||
nameStrong.textContent = b.name;
|
nameStrong.textContent = b.name;
|
||||||
label.appendChild(nameStrong);
|
info.appendChild(nameStrong);
|
||||||
label.appendChild(document.createTextNode(' \u2014 ' + (b.size / 1048576).toFixed(1) + ' MB \u2014 ' + b.date));
|
var meta = document.createElement('div');
|
||||||
group.appendChild(label);
|
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);
|
||||||
});
|
});
|
||||||
list.appendChild(group);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
SELECTOR;
|
SELECTOR;
|
||||||
|
|
||||||
/* Insert the selector before the extract step in the HTML */
|
/* Insert the selector into the extract panel */
|
||||||
$html = str_replace(
|
$html = str_replace(
|
||||||
'<!-- Step: Extract -->',
|
'<p class="mr-desc">Extract site-backup.zip into the current directory.</p>',
|
||||||
$selectorHtml . "\n<!-- Step: Extract -->",
|
'<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
|
$html
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -435,7 +495,7 @@ function actionPreflight(): array
|
|||||||
'label' => 'Backup Archive',
|
'label' => 'Backup Archive',
|
||||||
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
|
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
|
||||||
'ok' => file_exists(BACKUP_FILE),
|
'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[] = [
|
$checks[] = [
|
||||||
@@ -462,15 +522,31 @@ function actionPreflight(): array
|
|||||||
'hint' => 'Informational',
|
'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;
|
$allOk = true;
|
||||||
|
$warnings = [];
|
||||||
|
|
||||||
foreach ($checks as $c) {
|
foreach ($checks as $c) {
|
||||||
if (!$c['ok']) {
|
if (!$c['ok']) {
|
||||||
$allOk = false;
|
$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
|
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-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-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-ok{background:#dcfce7;color:#16a34a}
|
||||||
|
.mr-check-warn{background:#fef9c3;color:#a16207}
|
||||||
.mr-check-fail{background:#fef2f2;color:#dc2626}
|
.mr-check-fail{background:#fef2f2;color:#dc2626}
|
||||||
.mr-check-info{background:#e0f2fe;color:#0284c7}
|
.mr-check-info{background:#e0f2fe;color:#0284c7}
|
||||||
.mr-check-label{flex:1;font-weight:500}
|
.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-container">
|
||||||
<div class="mr-alert mr-alert-danger">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Step Progress -->
|
<!-- 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.
|
<strong>Success!</strong> The site restoration is complete.
|
||||||
</div>
|
</div>
|
||||||
<div class="mr-alert mr-alert-danger">
|
<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>
|
||||||
<div style="margin-top:1rem">
|
<div style="margin-top:1rem">
|
||||||
<button class="mr-btn mr-btn-danger" onclick="runCleanup()">Remove Restore Files</button>
|
<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>
|
<script>
|
||||||
const TOKEN = <?php echo json_encode($token); ?>;
|
const TOKEN = <?php echo json_encode($token); ?>;
|
||||||
|
const SCRIPT_URL = <?php echo json_encode(basename($_SERVER['SCRIPT_NAME'])); ?>;
|
||||||
let currentStep = 1;
|
let currentStep = 1;
|
||||||
let dbConfig = {};
|
let dbConfig = {};
|
||||||
|
|
||||||
@@ -1769,8 +1847,23 @@ async function post(action, extra) {
|
|||||||
form.append(k, v);
|
form.append(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await fetch('restore.php', { method: 'POST', body: form });
|
var res;
|
||||||
return res.json();
|
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) {
|
function goStep(n) {
|
||||||
@@ -1845,15 +1938,27 @@ async function runPreflight() {
|
|||||||
setBtnLoading(btn, true);
|
setBtnLoading(btn, true);
|
||||||
log('Running pre-flight checks...');
|
log('Running pre-flight checks...');
|
||||||
|
|
||||||
|
try {
|
||||||
const r = await post('preflight');
|
const r = await post('preflight');
|
||||||
|
|
||||||
|
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 list = document.getElementById('checkList');
|
const list = document.getElementById('checkList');
|
||||||
while (list.firstChild) list.removeChild(list.firstChild);
|
while (list.firstChild) list.removeChild(list.firstChild);
|
||||||
|
|
||||||
r.checks.forEach(function(c) {
|
r.checks.forEach(function(c) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
const icon = document.createElement('span');
|
const icon = document.createElement('span');
|
||||||
icon.className = 'mr-check-icon ' + (c.ok ? 'mr-check-ok' : 'mr-check-fail');
|
var iconClass = c.ok ? 'mr-check-ok' : 'mr-check-fail';
|
||||||
icon.textContent = c.ok ? '\u2713' : '\u2717';
|
if (c.warn) iconClass = 'mr-check-warn';
|
||||||
|
icon.className = 'mr-check-icon ' + iconClass;
|
||||||
|
icon.textContent = c.warn ? '\u26a0' : (c.ok ? '\u2713' : '\u2717');
|
||||||
|
|
||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.className = 'mr-check-label';
|
label.className = 'mr-check-label';
|
||||||
@@ -1866,9 +1971,16 @@ async function runPreflight() {
|
|||||||
li.appendChild(icon);
|
li.appendChild(icon);
|
||||||
li.appendChild(label);
|
li.appendChild(label);
|
||||||
li.appendChild(val);
|
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);
|
list.appendChild(li);
|
||||||
|
|
||||||
log(' ' + (c.ok ? 'OK' : 'FAIL') + ': ' + c.label + ' = ' + c.value);
|
var logPrefix = c.warn ? 'WARN' : (c.ok ? 'OK' : 'FAIL');
|
||||||
|
log(' ' + logPrefix + ': ' + c.label + ' = ' + c.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
setBtnLoading(btn, false);
|
setBtnLoading(btn, false);
|
||||||
@@ -1882,6 +1994,11 @@ async function runPreflight() {
|
|||||||
btn.textContent = 'Re-check';
|
btn.textContent = 'Re-check';
|
||||||
log('Some checks failed');
|
log('Some checks failed');
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log('Pre-flight error: ' + e.message);
|
||||||
|
setBtnLoading(btn, false);
|
||||||
|
btn.textContent = 'Re-check';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2
|
// Step 2
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ class SteppedBackupEngine
|
|||||||
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
|
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
|
||||||
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
|
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
|
||||||
$session->remoteStorage = $profile->remote_storage ?? 'none';
|
$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);
|
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
|
||||||
|
|
||||||
// Load multi-remote destinations from the remotes table
|
// Load multi-remote destinations from the remotes table
|
||||||
@@ -377,15 +378,30 @@ class SteppedBackupEngine
|
|||||||
$this->verifyArchive($session->archivePath, $session->backupType);
|
$this->verifyArchive($session->archivePath, $session->backupType);
|
||||||
$session->log('Archive integrity verified');
|
$session->log('Archive integrity verified');
|
||||||
|
|
||||||
// MokoRestore wrapper
|
// MokoRestore
|
||||||
if ($session->includeMokoRestore) {
|
$mokoRestoreMode = $session->includeMokoRestore ?? '0';
|
||||||
|
$restoreScriptName = $session->restoreScriptName ?? 'restore.php';
|
||||||
|
|
||||||
|
if ($mokoRestoreMode === '1') {
|
||||||
$session->log('Wrapping with MokoRestore script...');
|
$session->log('Wrapping with MokoRestore script...');
|
||||||
$mokoRestorePath = $session->archivePath . '.mokorestore.zip';
|
$mokoRestorePath = $session->archivePath . '.mokorestore.zip';
|
||||||
MokoRestore::wrap($session->archivePath, $mokoRestorePath);
|
MokoRestore::wrap($session->archivePath, $mokoRestorePath, $restoreScriptName);
|
||||||
@unlink($session->archivePath);
|
@unlink($session->archivePath);
|
||||||
rename($mokoRestorePath, $session->archivePath);
|
rename($mokoRestorePath, $session->archivePath);
|
||||||
$totalSize = filesize($session->archivePath);
|
$totalSize = filesize($session->archivePath);
|
||||||
$session->log('MokoRestore archive created');
|
$session->log('MokoRestore archive created');
|
||||||
|
} elseif ($mokoRestoreMode === 'standalone') {
|
||||||
|
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
|
||||||
|
$restoreDir = dirname($session->archivePath);
|
||||||
|
$session->restoreScriptPath = $restoreDir . '/' . $restoreScriptName;
|
||||||
|
|
||||||
|
try {
|
||||||
|
MokoRestore::generateStandalone($session->restoreScriptPath);
|
||||||
|
$session->log('Standalone ' . $restoreScriptName . ' generated');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$session->log('MokoRestore error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||||
|
$session->log('Stack trace: ' . $e->getTraceAsString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update record
|
// Update record
|
||||||
@@ -463,6 +479,10 @@ class SteppedBackupEngine
|
|||||||
if ($result['success']) {
|
if ($result['success']) {
|
||||||
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
||||||
$session->log(' Upload complete: ' . $result['message']);
|
$session->log(' Upload complete: ' . $result['message']);
|
||||||
|
|
||||||
|
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
|
||||||
|
$uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$uploadFailed = true;
|
$uploadFailed = true;
|
||||||
$session->log(' WARNING: Upload failed: ' . $result['message']);
|
$session->log(' WARNING: Upload failed: ' . $result['message']);
|
||||||
@@ -525,6 +545,12 @@ class SteppedBackupEngine
|
|||||||
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
||||||
$session->log('Remote upload complete: ' . $result['message']);
|
$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)) {
|
if (!$session->remoteKeepLocal && is_file($session->archivePath)) {
|
||||||
@unlink($session->archivePath);
|
@unlink($session->archivePath);
|
||||||
$session->log('Local copy removed');
|
$session->log('Local copy removed');
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ class SteppedSession
|
|||||||
public array $excludeFiles = [];
|
public array $excludeFiles = [];
|
||||||
public array $excludeTables = [];
|
public array $excludeTables = [];
|
||||||
public string $remoteStorage = 'none';
|
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 bool $remoteKeepLocal = true;
|
||||||
public string $encryptionPassword = '';
|
public string $encryptionPassword = '';
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ class SshKeyField extends FormField
|
|||||||
$id = $this->id;
|
$id = $this->id;
|
||||||
$name = $this->name;
|
$name = $this->name;
|
||||||
|
|
||||||
$hasKey = !empty($value) && str_contains($value, 'PRIVATE KEY');
|
$decoded = !empty($value) ? (base64_decode($value, true) ?: '') : '';
|
||||||
|
$hasKey = !empty($value) && ($value === '__KEEP_EXISTING__'
|
||||||
|
|| str_contains($value, 'PRIVATE KEY')
|
||||||
|
|| str_contains($decoded, 'PRIVATE KEY'));
|
||||||
|
|
||||||
$html = '<div id="' . htmlspecialchars($id) . '-wrapper">';
|
$html = '<div id="' . htmlspecialchars($id) . '-wrapper">';
|
||||||
|
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ class DashboardModel extends BaseDatabaseModel
|
|||||||
->select($db->quoteName(['id', 'title', 'backup_type']))
|
->select($db->quoteName(['id', 'title', 'backup_type']))
|
||||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||||
->where($db->quoteName('published') . ' = 1')
|
->where($db->quoteName('published') . ' = 1')
|
||||||
->order($db->quoteName('ordering') . ' ASC');
|
->order($db->quoteName('id') . ' ASC');
|
||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
|
|
||||||
return $db->loadObjectList() ?: [];
|
return $db->loadObjectList() ?: [];
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ class ProfilesModel extends ListModel
|
|||||||
$query->where('(' . $db->quoteName('a.title') . ' LIKE ' . $search . ')');
|
$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');
|
$orderDir = $this->state->get('list.direction', 'ASC');
|
||||||
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
|
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function populateState($ordering = 'a.ordering', $direction = 'ASC'): void
|
protected function populateState($ordering = 'a.id', $direction = 'ASC'): void
|
||||||
{
|
{
|
||||||
parent::populateState($ordering, $direction);
|
parent::populateState($ordering, $direction);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,12 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\View\Backup;
|
|||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
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;
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
class HtmlView extends BaseHtmlView
|
class HtmlView extends BaseHtmlView
|
||||||
@@ -34,6 +38,24 @@ class HtmlView extends BaseHtmlView
|
|||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
|
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');
|
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitebackup&view=backups');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class HtmlView extends BaseHtmlView
|
|||||||
protected $state;
|
protected $state;
|
||||||
public $filterForm;
|
public $filterForm;
|
||||||
public $activeFilters = [];
|
public $activeFilters = [];
|
||||||
public $profiles = [];
|
|
||||||
|
|
||||||
public function display($tpl = null): void
|
public function display($tpl = null): void
|
||||||
{
|
{
|
||||||
@@ -35,16 +34,6 @@ class HtmlView extends BaseHtmlView
|
|||||||
$this->filterForm = $this->get('FilterForm');
|
$this->filterForm = $this->get('FilterForm');
|
||||||
$this->activeFilters = $this->get('ActiveFilters');
|
$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->checkUpdateSite();
|
||||||
$this->addToolbar();
|
$this->addToolbar();
|
||||||
|
|
||||||
@@ -112,10 +101,6 @@ class HtmlView extends BaseHtmlView
|
|||||||
|
|
||||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
|
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')) {
|
if ($user->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
|
||||||
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
|
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,16 +55,6 @@ class HtmlView extends BaseHtmlView
|
|||||||
$toolbar = Toolbar::getInstance();
|
$toolbar = Toolbar::getInstance();
|
||||||
$profileId = (int) $this->item->id;
|
$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);
|
$backupsUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[PROFILE_ID]=' . $profileId);
|
||||||
$toolbar->linkButton('view-backups', 'COM_MOKOJOOMBACKUP_VIEW_BACKUPS')
|
$toolbar->linkButton('view-backups', 'COM_MOKOJOOMBACKUP_VIEW_BACKUPS')
|
||||||
->url($backupsUrl)
|
->url($backupsUrl)
|
||||||
|
|||||||
@@ -31,30 +31,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="j-main-container" class="j-main-container">
|
<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 echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
|
||||||
|
|
||||||
<?php if (empty($this->items)) : ?>
|
<?php if (empty($this->items)) : ?>
|
||||||
@@ -88,9 +64,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<th scope="col" class="w-10">
|
<th scope="col" class="w-10">
|
||||||
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
|
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="w-5">
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-5">
|
<th scope="col" class="w-5">
|
||||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
||||||
</th>
|
</th>
|
||||||
@@ -111,7 +84,9 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<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); ?>
|
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php
|
<?php
|
||||||
@@ -139,35 +114,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<td>
|
<td>
|
||||||
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
|
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<?php echo (int) $item->id; ?>
|
<?php echo (int) $item->id; ?>
|
||||||
</td>
|
</td>
|
||||||
@@ -188,18 +134,24 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Stepped Backup Modal (for shared hosting) -->
|
<!-- 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 class="modal fade" id="mokosuitebackup-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||||
<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);">
|
<div class="modal-dialog">
|
||||||
<h3 id="mb-modal-title" style="margin:0 0 1rem;">Backup in Progress</h3>
|
<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;">
|
<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>
|
<span class="icon-warning-circle" aria-hidden="true"></span>
|
||||||
<strong>Do not navigate away or close this window</strong> while the backup is running.
|
<strong>Do not navigate away or close this window</strong> while the backup is running.
|
||||||
</div>
|
</div>
|
||||||
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
|
<div class="progress mb-2" style="height:24px;">
|
||||||
<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 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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -208,19 +160,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
||||||
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
|
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;
|
var backupRunning = false;
|
||||||
|
|
||||||
@@ -235,12 +174,12 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
|
|
||||||
function showModal() {
|
function showModal() {
|
||||||
backupRunning = true;
|
backupRunning = true;
|
||||||
document.getElementById('mokosuitebackup-modal').style.display = 'block';
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('mokosuitebackup-modal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideModal() {
|
function hideModal() {
|
||||||
backupRunning = false;
|
backupRunning = false;
|
||||||
document.getElementById('mokosuitebackup-modal').style.display = 'none';
|
bootstrap.Modal.getInstance(document.getElementById('mokosuitebackup-modal'))?.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProgress(progress, message, phase) {
|
function updateProgress(progress, message, phase) {
|
||||||
@@ -344,31 +283,26 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
document.getElementById('mb-restore-record-id').value = checked[0].value;
|
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;
|
return false;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close restore modal
|
// Close restore modal handled by Bootstrap data-bs-dismiss
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// AJAX stepped restore
|
// AJAX stepped restore
|
||||||
var restoreRunning = false;
|
var restoreRunning = false;
|
||||||
|
|
||||||
function showRestoreProgress() {
|
function showRestoreProgress() {
|
||||||
restoreRunning = true;
|
restoreRunning = true;
|
||||||
document.getElementById('mb-restore-modal').style.display = 'none';
|
bootstrap.Modal.getInstance(document.getElementById('mb-restore-modal'))?.hide();
|
||||||
document.getElementById('mb-restore-progress-modal').style.display = 'block';
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-restore-progress-modal')).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideRestoreProgress() {
|
function hideRestoreProgress() {
|
||||||
restoreRunning = false;
|
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) {
|
function updateRestoreProgress(progress, message, phase) {
|
||||||
@@ -457,145 +391,20 @@ $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>
|
</script>
|
||||||
|
|
||||||
<!-- Restore Confirmation Modal -->
|
<!-- 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 class="modal fade" id="mb-restore-modal" tabindex="-1" aria-hidden="true">
|
||||||
<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 class="modal-dialog">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
<div class="modal-content">
|
||||||
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?></h4>
|
<div class="modal-header">
|
||||||
<button type="button" class="btn-close mb-restore-close" aria-label="Close"></button>
|
<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>
|
</div>
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.restore'); ?>" method="post" id="mb-restore-form">
|
<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="">
|
<input type="hidden" name="id" id="mb-restore-record-id" value="">
|
||||||
<div style="padding:1.5rem;">
|
<div class="modal-body">
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<span class="icon-warning-circle" aria-hidden="true"></span>
|
<span class="icon-warning-circle" aria-hidden="true"></span>
|
||||||
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_CONFIRM'); ?></strong>
|
<strong><?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_CONFIRM'); ?></strong>
|
||||||
@@ -629,8 +438,8 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PASSWORD_PLACEHOLDER'); ?>" autocomplete="off">
|
placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_RESTORE_PASSWORD_PLACEHOLDER'); ?>" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary mb-restore-close"><?php echo Text::_('JCANCEL'); ?></button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
|
||||||
<button type="submit" class="btn btn-danger">
|
<button type="submit" class="btn btn-danger">
|
||||||
<span class="icon-upload" aria-hidden="true"></span>
|
<span class="icon-upload" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE'); ?>
|
||||||
@@ -642,73 +451,39 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Restore Progress Modal -->
|
<!-- 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 class="modal fade" id="mb-restore-progress-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||||
<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);">
|
<div class="modal-dialog">
|
||||||
<h3 id="mb-restore-title" style="margin:0 0 1rem;">Restore in Progress</h3>
|
<div class="modal-content">
|
||||||
<div style="background:#e9ecef; border-radius:4px; overflow:hidden; height:24px; margin-bottom:0.5rem;">
|
<div class="modal-header">
|
||||||
<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>
|
<h5 class="modal-title" id="mb-restore-title">Restore in Progress</h5>
|
||||||
</div>
|
</div>
|
||||||
<p id="mb-restore-status" style="color:#666; font-size:0.9rem; margin:0.5rem 0;">Initializing...</p>
|
<div class="modal-body">
|
||||||
<p id="mb-restore-phase" style="color:#999; font-size:0.8rem; margin:0;">Phase: init</p>
|
<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>
|
</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>
|
||||||
<!-- 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>
|
||||||
<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>
|
</div>
|
||||||
</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 -->
|
<!-- Purge Backups Modal -->
|
||||||
<?php $canDelete = $user->authorise('core.delete', 'com_mokosuitebackup'); ?>
|
<?php $canDelete = $user->authorise('core.delete', 'com_mokosuitebackup'); ?>
|
||||||
<?php if ($canDelete) : ?>
|
<?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 class="modal fade" id="mb-purge-modal" tabindex="-1" aria-hidden="true">
|
||||||
<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 class="modal-dialog">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
<div class="modal-content">
|
||||||
<h4 style="margin:0;">
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
<span class="icon-trash" aria-hidden="true"></span>
|
<span class="icon-trash" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_TITLE'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_TITLE'); ?>
|
||||||
</h4>
|
</h5>
|
||||||
<button type="button" class="btn-close mb-purge-close" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.purge'); ?>" method="post" id="mb-purge-form">
|
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.purge'); ?>" method="post" id="mb-purge-form">
|
||||||
<div style="padding:1.5rem;">
|
<div class="modal-body">
|
||||||
<p><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DESC'); ?></p>
|
<p><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DESC'); ?></p>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="mb-purge-date" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DATE_LABEL'); ?></label>
|
<label for="mb-purge-date" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_DATE_LABEL'); ?></label>
|
||||||
@@ -721,8 +496,8 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<div class="alert alert-info mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND'); ?></div>
|
<div class="alert alert-info mb-0"><?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_NONE_FOUND'); ?></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary mb-purge-close"><?php echo Text::_('JCANCEL'); ?></button>
|
<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>
|
<button type="submit" class="btn btn-danger" id="mb-purge-submit" disabled>
|
||||||
<span class="icon-trash" aria-hidden="true"></span>
|
<span class="icon-trash" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_SUBMIT'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_PURGE_SUBMIT'); ?>
|
||||||
@@ -731,21 +506,23 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
<?php echo HTMLHelper::_('form.token'); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Backup Comparison Modal -->
|
<!-- 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 class="modal fade" id="mb-compare-modal" tabindex="-1" aria-hidden="true">
|
||||||
<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 class="modal-dialog modal-lg">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
<div class="modal-content">
|
||||||
<h4 style="margin:0;">
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
<span class="icon-copy" aria-hidden="true"></span>
|
<span class="icon-copy" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TITLE'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_TITLE'); ?>
|
||||||
</h4>
|
</h5>
|
||||||
<button type="button" class="btn-close mb-compare-close" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:1rem 1.5rem; overflow-y:auto; flex:1;">
|
<div class="modal-body" style="max-height:65vh; overflow-y:auto;">
|
||||||
<div id="mb-compare-loading" style="text-align:center; padding:2rem;">
|
<div id="mb-compare-loading" class="text-center py-4">
|
||||||
<span class="icon-spinner icon-spin" aria-hidden="true"></span>
|
<span class="icon-spinner icon-spin" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_LOADING'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_COMPARE_LOADING'); ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -763,6 +540,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -807,7 +585,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
var table = document.getElementById('mb-compare-table');
|
var table = document.getElementById('mb-compare-table');
|
||||||
var body = document.getElementById('mb-compare-body');
|
var body = document.getElementById('mb-compare-body');
|
||||||
|
|
||||||
modal.style.display = 'block';
|
bootstrap.Modal.getOrCreateInstance(modal).show();
|
||||||
loading.style.display = 'block';
|
loading.style.display = 'block';
|
||||||
errorEl.style.display = 'none';
|
errorEl.style.display = 'none';
|
||||||
table.style.display = 'none';
|
table.style.display = 'none';
|
||||||
@@ -874,12 +652,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close compare modal
|
// Compare modal close handled by Bootstrap data-bs-dismiss
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Intercept Compare toolbar button
|
// Intercept Compare toolbar button
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
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-count-wrapper').style.display = 'none';
|
||||||
document.getElementById('mb-purge-none-wrapper').style.display = 'none';
|
document.getElementById('mb-purge-none-wrapper').style.display = 'none';
|
||||||
document.getElementById('mb-purge-submit').disabled = true;
|
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;
|
return false;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
@@ -936,12 +709,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal
|
// Purge modal close handled by Bootstrap data-bs-dismiss
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Confirm on submit
|
// Confirm on submit
|
||||||
var purgeForm = document.getElementById('mb-purge-form');
|
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">
|
<select id="mb-profile-select" class="form-select mb-2">
|
||||||
<?php foreach ($this->profiles as $profile) : ?>
|
<?php foreach ($this->profiles as $profile) : ?>
|
||||||
<option value="<?php echo (int) $profile->id; ?>">
|
<option value="<?php echo (int) $profile->id; ?>">
|
||||||
|
#<?php echo (int) $profile->id; ?> —
|
||||||
<?php echo $this->escape($profile->title); ?>
|
<?php echo $this->escape($profile->title); ?>
|
||||||
(<?php echo $this->escape($profile->backup_type); ?>)
|
(<?php echo $this->escape($profile->backup_type); ?>)
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -52,9 +52,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<th scope="col" class="w-10">
|
<th scope="col" class="w-10">
|
||||||
<?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.published', $listDirn, $listOrder); ?>
|
<?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.published', $listDirn, $listOrder); ?>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="w-10 text-center">
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-5">
|
<th scope="col" class="w-5">
|
||||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
||||||
</th>
|
</th>
|
||||||
@@ -87,16 +84,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<td>
|
<td>
|
||||||
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'profiles.'); ?>
|
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'profiles.'); ?>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<?php echo (int) $item->id; ?>
|
<?php echo (int) $item->id; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -132,14 +132,15 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Create Snapshot Modal -->
|
<!-- 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 class="modal fade" id="mb-snapshot-create-modal" tabindex="-1" aria-hidden="true">
|
||||||
<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 class="modal-dialog">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
<div class="modal-content">
|
||||||
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE'); ?></h4>
|
<div class="modal-header">
|
||||||
<button type="button" class="btn-close mb-modal-close" aria-label="Close"></button>
|
<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>
|
</div>
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.create'); ?>" method="post" id="mb-snapshot-create-form">
|
<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="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="mb-snap-desc" class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_DESCRIPTION'); ?></label>
|
<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'); ?>">
|
<input type="text" class="form-control" id="mb-snap-desc" name="description" placeholder="<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_DESC_PLACEHOLDER'); ?>">
|
||||||
@@ -169,8 +170,8 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary mb-modal-close"><?php echo Text::_('JCANCEL'); ?></button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<span class="icon-camera" aria-hidden="true"></span>
|
<span class="icon-camera" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_CREATE'); ?>
|
||||||
@@ -179,18 +180,20 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
<?php echo HTMLHelper::_('form.token'); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Restore Snapshot Modal -->
|
<!-- 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 class="modal fade" id="mb-snapshot-restore-modal" tabindex="-1" aria-hidden="true">
|
||||||
<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 class="modal-dialog">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
<div class="modal-content">
|
||||||
<h4 style="margin:0;"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE'); ?></h4>
|
<div class="modal-header">
|
||||||
<button type="button" class="btn-close mb-modal-close" aria-label="Close"></button>
|
<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>
|
</div>
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.restore'); ?>" method="post" id="mb-snapshot-restore-form">
|
<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="">
|
<input type="hidden" name="id" id="mb-restore-id" value="">
|
||||||
<div style="padding:1.5rem;">
|
<div class="modal-body">
|
||||||
<p id="mb-restore-desc" class="fw-bold"></p>
|
<p id="mb-restore-desc" class="fw-bold"></p>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -213,7 +216,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
|
|
||||||
<div class="mb-3" id="mb-restore-types-container">
|
<div class="mb-3" id="mb-restore-types-container">
|
||||||
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_TYPES'); ?></label>
|
<label class="form-label fw-bold"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_TYPES'); ?></label>
|
||||||
<!-- Populated by JS from data-types -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning mb-0" id="mb-replace-warning">
|
<div class="alert alert-warning mb-0" id="mb-replace-warning">
|
||||||
@@ -221,8 +223,8 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_REPLACE_WARNING'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_REPLACE_WARNING'); ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:0 1.5rem 1.5rem; text-align:right;">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary mb-modal-close"><?php echo Text::_('JCANCEL'); ?></button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?php echo Text::_('JCANCEL'); ?></button>
|
||||||
<button type="submit" class="btn btn-danger">
|
<button type="submit" class="btn btn-danger">
|
||||||
<span class="icon-upload" aria-hidden="true"></span>
|
<span class="icon-upload" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE'); ?>
|
||||||
@@ -231,18 +233,20 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
<?php echo HTMLHelper::_('form.token'); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Browse Snapshot Detail Modal -->
|
<!-- 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 class="modal fade" id="mb-snapshot-browse-modal" tabindex="-1" aria-hidden="true">
|
||||||
<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 class="modal-dialog modal-xl">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.5rem; border-bottom:1px solid #dee2e6;">
|
<div class="modal-content">
|
||||||
<h4 style="margin:0;" id="mb-browse-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_BROWSE'); ?></h4>
|
<div class="modal-header">
|
||||||
<button type="button" class="btn-close mb-modal-close" aria-label="Close"></button>
|
<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>
|
</div>
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=snapshots.restoreSelected'); ?>" method="post" id="mb-snapshot-browse-form">
|
<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="">
|
<input type="hidden" name="id" id="mb-browse-id" value="">
|
||||||
<div style="padding:1rem 1.5rem; overflow-y:auto; flex:1;">
|
<div class="modal-body" style="max-height:60vh; overflow-y:auto;">
|
||||||
<div id="mb-browse-loading" class="text-center py-4">
|
<div id="mb-browse-loading" class="text-center py-4">
|
||||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_LOADING'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_LOADING'); ?>
|
||||||
@@ -331,8 +335,8 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:0.75rem 1.5rem; border-top:1px solid #dee2e6; text-align:right;">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary mb-modal-close"><?php echo Text::_('JCANCEL'); ?></button>
|
<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>
|
<button type="submit" class="btn btn-success" id="mb-browse-restore-btn" disabled>
|
||||||
<span class="icon-upload" aria-hidden="true"></span>
|
<span class="icon-upload" aria-hidden="true"></span>
|
||||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_SELECTED'); ?>
|
<?php echo Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_SELECTED'); ?>
|
||||||
@@ -341,6 +345,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
<?php echo HTMLHelper::_('form.token'); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -352,7 +357,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
createBtn.addEventListener('click', function(e) {
|
createBtn.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
document.getElementById('mb-snapshot-create-modal').style.display = 'block';
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('mb-snapshot-create-modal')).show();
|
||||||
return false;
|
return false;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
@@ -413,7 +418,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
// Show/hide replace warning based on mode
|
// Show/hide replace warning based on mode
|
||||||
toggleReplaceWarning();
|
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
|
// Toggle warning when mode changes
|
||||||
@@ -454,7 +459,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
|||||||
tab.show();
|
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
|
// Fetch snapshot content via AJAX
|
||||||
var token = <?php echo json_encode(Session::getFormToken()); ?>;
|
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')); ?>;
|
: <?php echo json_encode(Text::_('COM_MOKOJOOMBACKUP_SNAPSHOT_RESTORE_SELECTED')); ?>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modals
|
// Modal close handled by Bootstrap data-bs-dismiss
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoSuiteBackup
|
||||||
|
* @subpackage mod_mokosuitebackup_cpanel
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="module" client="administrator" method="upgrade">
|
<extension type="module" client="administrator" method="upgrade">
|
||||||
<name>mod_mokosuitebackup_cpanel</name>
|
<name>mod_mokosuitebackup_cpanel</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-23</creationDate>
|
<creationDate>2026-06-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<namespace path="src">Joomla\Module\MokoSuiteBackupCpanel</namespace>
|
<namespace path="src">Joomla\Module\MokoSuiteBackupCpanel</namespace>
|
||||||
|
|
||||||
<files>
|
<files>
|
||||||
|
<filename module="mod_mokosuitebackup_cpanel">mod_mokosuitebackup_cpanel.php</filename>
|
||||||
<folder>language</folder>
|
<folder>language</folder>
|
||||||
<folder>services</folder>
|
<folder>services</folder>
|
||||||
<folder>src</folder>
|
<folder>src</folder>
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ $moduleId = 'mod-msb-cpanel-' . $displayData['module']->id;
|
|||||||
class="btn btn-sm btn-outline-primary msb-cpanel-backup-btn"
|
class="btn btn-sm btn-outline-primary msb-cpanel-backup-btn"
|
||||||
data-profile-id="<?php echo (int) $profile->id; ?>"
|
data-profile-id="<?php echo (int) $profile->id; ?>"
|
||||||
data-module-id="<?php echo $moduleId; ?>">
|
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>
|
<span class="badge bg-secondary ms-1"><?php echo htmlspecialchars($profile->backup_type); ?></span>
|
||||||
</button>
|
</button>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="actionlog" method="upgrade">
|
<extension type="plugin" group="actionlog" method="upgrade">
|
||||||
<name>Action Log - MokoSuiteBackup</name>
|
<name>Action Log - MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="console" method="upgrade">
|
<extension type="plugin" group="console" method="upgrade">
|
||||||
<name>Console - MokoSuiteBackup</name>
|
<name>Console - MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>Content - MokoSuiteBackup</name>
|
<name>Content - MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="quickicon" method="upgrade">
|
<extension type="plugin" group="quickicon" method="upgrade">
|
||||||
<name>Quick Icon - MokoSuiteBackup</name>
|
<name>Quick Icon - MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>System - MokoSuiteBackup</name>
|
<name>System - MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="task" method="upgrade">
|
<extension type="plugin" group="task" method="upgrade">
|
||||||
<name>Task - MokoSuiteBackup</name>
|
<name>Task - MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>Web Services - MokoSuiteBackup</name>
|
<name>Web Services - MokoSuiteBackup</name>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoSuiteBackup</name>
|
<name>Package - MokoSuiteBackup</name>
|
||||||
<packagename>mokosuitebackup</packagename>
|
<packagename>mokosuitebackup</packagename>
|
||||||
<version>01.42.00</version>
|
<version>01.43.32</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<file type="plugin" id="mokosuitebackup" group="content">plg_content_mokosuitebackup.zip</file>
|
<file type="plugin" id="mokosuitebackup" group="content">plg_content_mokosuitebackup.zip</file>
|
||||||
<file type="plugin" id="mokosuitebackup" group="actionlog">plg_actionlog_mokosuitebackup.zip</file>
|
<file type="plugin" id="mokosuitebackup" group="actionlog">plg_actionlog_mokosuitebackup.zip</file>
|
||||||
<file type="module" id="mod_mokosuitebackup_cpanel" client="administrator">mod_mokosuitebackup_cpanel.zip</file>
|
<file type="module" id="mod_mokosuitebackup_cpanel" client="administrator">mod_mokosuitebackup_cpanel.zip</file>
|
||||||
|
<file type="package" id="pkg_mokosuiteclient">MokoSuiteClient.zip</file>
|
||||||
</files>
|
</files>
|
||||||
|
|
||||||
<languages>
|
<languages>
|
||||||
|
|||||||
Reference in New Issue
Block a user