Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 950f4bb58c | |||
| dbbacc98a7 | |||
| 6b42fefe09 | |||
| a3dbd1f89a |
@@ -1,3 +0,0 @@
|
||||
[submodule "source/packages/MokoSuiteClient"]
|
||||
path = source/packages/MokoSuiteClient
|
||||
url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient.git
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.43.32
|
||||
# VERSION: 01.43.07
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
@@ -59,13 +59,6 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
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
|
||||
env:
|
||||
|
||||
+1
-23
@@ -2,28 +2,6 @@
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
@@ -93,7 +71,7 @@
|
||||
- Backup comparison: select two backups for side-by-side diff
|
||||
- Archive browser: view files inside backup without extracting
|
||||
- Manual purge: delete backups older than a date with count preview
|
||||
- Backup count badges on profile list
|
||||
- Run Backup button on profile list and edit views with backup count badges
|
||||
- "Do not navigate away" warning in backup/restore progress modals
|
||||
- Clickable placeholder pills for backup directory and archive name fields
|
||||
- Comprehensive help modal with absolute/relative/placeholder path documentation
|
||||
|
||||
@@ -5,7 +5,7 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Package** | `pkg_mokosuitebackup` |
|
||||
| **Type** | Joomla Package (9 sub-extensions + MokoSuiteClient) |
|
||||
| **Type** | Joomla Package (8 sub-extensions) |
|
||||
| **Joomla** | 6.x+ |
|
||||
| **PHP** | 8.1+ |
|
||||
| **License** | GPL-3.0-or-later |
|
||||
@@ -30,8 +30,7 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
||||
- Scheduled snapshot task via com_scheduler
|
||||
|
||||
### Remote Storage
|
||||
- Multi-remote — upload to multiple destinations per profile simultaneously
|
||||
- SFTP with SSH key file auth + remote directory browser
|
||||
- SFTP with SSH key file authentication (key stored base64-encoded in database)
|
||||
- Amazon S3 and S3-compatible (DigitalOcean Spaces, Wasabi, MinIO)
|
||||
- Google Drive with OAuth2 and resumable uploads
|
||||
- Graceful degradation — local backup preserved if upload fails
|
||||
@@ -67,10 +66,6 @@ Full-site backup and restore for Joomla — database, files, and configuration.
|
||||
- Snapshots: create, list, restore, delete, download
|
||||
- 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
|
||||
|
||||
1. Download from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/releases)
|
||||
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
<!--
|
||||
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 source/packages/MokoSuiteClient deleted from 64482e59cd
@@ -24,9 +24,10 @@
|
||||
name="fullordering"
|
||||
type="list"
|
||||
label="JGLOBAL_SORT_BY"
|
||||
default="a.id ASC"
|
||||
default="a.ordering ASC"
|
||||
onchange="this.form.submit();"
|
||||
>
|
||||
<option value="a.ordering ASC">JFIELD_ORDERING_ASC</option>
|
||||
<option value="a.title ASC">COM_MOKOJOOMBACKUP_HEADING_TITLE_ASC</option>
|
||||
<option value="a.title DESC">COM_MOKOJOOMBACKUP_HEADING_TITLE_DESC</option>
|
||||
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
|
||||
|
||||
@@ -93,16 +93,6 @@
|
||||
<option value="1">COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED</option>
|
||||
<option value="standalone">COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE</option>
|
||||
</field>
|
||||
<field
|
||||
name="restore_script_name"
|
||||
type="text"
|
||||
label="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME"
|
||||
description="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC"
|
||||
default="restore.php"
|
||||
maxlength="128"
|
||||
filter="string"
|
||||
showon="include_mokorestore!:0"
|
||||
/>
|
||||
<field
|
||||
name="encryption_password"
|
||||
type="password"
|
||||
@@ -174,6 +164,12 @@
|
||||
<option value="1">JPUBLISHED</option>
|
||||
<option value="0">JUNPUBLISHED</option>
|
||||
</field>
|
||||
<field
|
||||
name="ordering"
|
||||
type="number"
|
||||
label="JFIELD_ORDERING_LABEL"
|
||||
default="0"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<fieldset name="filters" label="COM_MOKOJOOMBACKUP_FIELDSET_FILTERS">
|
||||
|
||||
@@ -42,8 +42,6 @@ COM_MOKOJOOMBACKUP_DASHBOARD_STORAGE_BREAKDOWN="Storage by Profile"
|
||||
COM_MOKOJOOMBACKUP_DASHBOARD_BACKUP_TREND="Backup Trend (30 days)"
|
||||
|
||||
; 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_TABLE_CAPTION="Table of backup records"
|
||||
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
|
||||
@@ -142,8 +140,6 @@ COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="None: no restore script. Wrap
|
||||
COM_MOKOJOOMBACKUP_MOKORESTORE_NONE="None"
|
||||
COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED="Wrapped (inside backup ZIP)"
|
||||
COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE="Standalone (separate restore.php)"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME="Restore Script Filename"
|
||||
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC="Custom filename for the restore script. Must end in .php. Use a non-obvious name to reduce discoverability on remote servers (e.g. moko-install-xyz.php)."
|
||||
|
||||
; Data Sanitization
|
||||
COM_MOKOJOOMBACKUP_FIELDSET_SANITIZATION="Data Sanitization"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="component" method="upgrade">
|
||||
<name>MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -40,7 +40,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
|
||||
`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)',
|
||||
`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',
|
||||
`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',
|
||||
@@ -114,13 +113,14 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_remotes` (
|
||||
`title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`type` VARCHAR(20) NOT NULL DEFAULT 'sftp' COMMENT 'sftp, s3, google_drive',
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`params` MEDIUMTEXT COMMENT 'JSON: type-specific settings',
|
||||
`keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
|
||||
`config` MEDIUMTEXT NOT NULL COMMENT 'JSON — type-specific settings',
|
||||
`ordering` INT(11) NOT NULL DEFAULT 0,
|
||||
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_profile` (`profile_id`),
|
||||
KEY `idx_enabled` (`profile_id`, `enabled`)
|
||||
KEY `idx_enabled` (`enabled`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Insert default backup profile (IGNORE prevents duplicate key error on update)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.11 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.19 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.20 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.21 — no schema changes */
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 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`;
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.23 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.24 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.25 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.26 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.29 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.30 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.31 — no schema changes */
|
||||
@@ -1 +0,0 @@
|
||||
/* 01.43.32 — no schema changes */
|
||||
@@ -924,11 +924,11 @@ class AjaxController extends BaseController
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode JSON params and mask secrets
|
||||
// Decode JSON config and mask secrets
|
||||
$items = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$config = json_decode($row->params, true) ?: [];
|
||||
$config = json_decode($row->config, true) ?: [];
|
||||
|
||||
// Mask sensitive fields so they never leave the server in list views
|
||||
$masked = $this->maskSecrets($config, $row->type);
|
||||
@@ -939,7 +939,8 @@ class AjaxController extends BaseController
|
||||
'title' => $row->title,
|
||||
'type' => $row->type,
|
||||
'enabled' => (int) $row->enabled,
|
||||
'params' => $masked,
|
||||
'keep_local' => (int) $row->keep_local,
|
||||
'config' => $masked,
|
||||
'ordering' => (int) $row->ordering,
|
||||
];
|
||||
}
|
||||
@@ -970,6 +971,7 @@ class AjaxController extends BaseController
|
||||
$title = trim($this->input->getString('remote_title', ''));
|
||||
$type = $this->input->getCmd('remote_type', 'sftp');
|
||||
$enabled = $this->input->getInt('remote_enabled', 1);
|
||||
$keepLocal = $this->input->getInt('remote_keep_local', 1);
|
||||
$configRaw = $this->input->getString('remote_config', '{}');
|
||||
|
||||
if (!$profileId) {
|
||||
@@ -1017,7 +1019,9 @@ class AjaxController extends BaseController
|
||||
$table->title = $title;
|
||||
$table->type = $type;
|
||||
$table->enabled = $enabled ? 1 : 0;
|
||||
$table->params = json_encode($config);
|
||||
$table->keep_local = $keepLocal ? 1 : 0;
|
||||
$table->config = json_encode($config);
|
||||
|
||||
if (!$table->check() || !$table->store()) {
|
||||
$this->sendJson(['error' => true, 'message' => $table->getError() ?: 'Save failed']);
|
||||
|
||||
@@ -1186,7 +1190,7 @@ class AjaxController extends BaseController
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('params'))
|
||||
->select($db->quoteName('config'))
|
||||
->from($db->quoteName('#__mokosuitebackup_remotes'))
|
||||
->where($db->quoteName('id') . ' = ' . $id);
|
||||
$db->setQuery($query);
|
||||
|
||||
@@ -259,14 +259,14 @@ class BackupEngine
|
||||
|
||||
// Step 2.5: MokoRestore script (if enabled)
|
||||
$mokoRestoreMode = $profile->include_mokorestore ?? '0';
|
||||
$restoreScriptName = $profile->restore_script_name ?? 'restore.php';
|
||||
$restoreScriptPath = '';
|
||||
|
||||
if ($mokoRestoreMode === '1') {
|
||||
// Wrapped mode: backup ZIP inside an outer ZIP with restore.php
|
||||
$this->log('Wrapping with MokoRestore script...');
|
||||
$mokoRestoreName = str_replace('.zip', '-mokorestore.zip', $archiveName);
|
||||
$mokoRestorePath = $this->backupDir . '/' . $mokoRestoreName;
|
||||
MokoRestore::wrap($archivePath, $mokoRestorePath, $restoreScriptName);
|
||||
MokoRestore::wrap($archivePath, $mokoRestorePath);
|
||||
|
||||
if (is_file($archivePath) && !unlink($archivePath)) {
|
||||
$this->log('WARNING: Could not remove pre-wrap archive');
|
||||
@@ -278,11 +278,11 @@ class BackupEngine
|
||||
$this->log('MokoRestore archive created: ' . $sizeHuman);
|
||||
$this->log('SHA-256 (wrapped): ' . $checksum);
|
||||
} elseif ($mokoRestoreMode === 'standalone') {
|
||||
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
|
||||
$this->log('Generating standalone ' . $restoreScriptName . '...');
|
||||
$restoreScriptPath = $this->backupDir . '/' . $restoreScriptName;
|
||||
// Standalone mode: restore.php as a separate file next to the backup ZIP
|
||||
$this->log('Generating standalone restore.php...');
|
||||
$restoreScriptPath = $this->backupDir . '/restore.php';
|
||||
MokoRestore::generateStandalone($restoreScriptPath);
|
||||
$this->log('Standalone ' . $restoreScriptName . ' generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
|
||||
$this->log('Standalone restore.php generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
|
||||
}
|
||||
|
||||
$remoteFilename = '';
|
||||
@@ -303,8 +303,9 @@ class BackupEngine
|
||||
$remoteFilename = $result['remote_path'] ?? $archiveName;
|
||||
$this->log(' Upload complete: ' . $result['message']);
|
||||
|
||||
/* Upload standalone restore.php if in standalone mode */
|
||||
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
||||
$uploader->upload($restoreScriptPath, basename($restoreScriptPath));
|
||||
$uploader->upload($restoreScriptPath, 'restore.php');
|
||||
}
|
||||
} else {
|
||||
$uploadFailed = true;
|
||||
@@ -335,15 +336,15 @@ class BackupEngine
|
||||
$remoteFilename = $uploadResult['remote_path'] ?? $archiveName;
|
||||
$this->log('Remote upload complete: ' . $uploadResult['message']);
|
||||
|
||||
// Upload standalone restore.php alongside the backup if in standalone mode
|
||||
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
|
||||
$restoreBasename = basename($restoreScriptPath);
|
||||
$this->log('Uploading standalone ' . $restoreBasename . '...');
|
||||
$restoreUpload = $uploader->upload($restoreScriptPath, $restoreBasename);
|
||||
$this->log('Uploading standalone restore.php...');
|
||||
$restoreUpload = $uploader->upload($restoreScriptPath, 'restore.php');
|
||||
|
||||
if ($restoreUpload['success']) {
|
||||
$this->log('Standalone ' . $restoreBasename . ' uploaded');
|
||||
$this->log('Standalone restore.php uploaded');
|
||||
} else {
|
||||
$this->log('WARNING: ' . $restoreBasename . ' upload failed: ' . $restoreUpload['message']);
|
||||
$this->log('WARNING: restore.php upload failed: ' . $restoreUpload['message']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,36 +35,25 @@ class MokoRestore
|
||||
*
|
||||
* @return string Path to the wrapped archive
|
||||
*/
|
||||
public static function wrap(string $backupArchive, string $outputPath, string $scriptName = 'restore.php'): string
|
||||
public static function wrap(string $backupArchive, string $outputPath): string
|
||||
{
|
||||
$scriptName = self::sanitizeScriptName($scriptName);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||
throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath);
|
||||
}
|
||||
|
||||
$zip->addFromString($scriptName, self::generateRestoreScript());
|
||||
// Add the standalone restore script
|
||||
$zip->addFromString('restore.php', self::generateRestoreScript());
|
||||
|
||||
// Add the original backup as a nested ZIP
|
||||
$zip->addFile($backupArchive, 'site-backup.zip');
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $outputPath;
|
||||
}
|
||||
|
||||
public static function sanitizeScriptName(string $name): string
|
||||
{
|
||||
$name = basename(trim($name));
|
||||
|
||||
if ($name === '' || !str_ends_with(strtolower($name), '.php')) {
|
||||
$name = 'restore.php';
|
||||
}
|
||||
|
||||
$name = preg_replace('/[^a-zA-Z0-9._-]/', '', $name);
|
||||
|
||||
return $name ?: 'restore.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the standalone restore.php script as a separate file.
|
||||
*
|
||||
@@ -184,7 +173,7 @@ SCANNER;
|
||||
'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']),
|
||||
'hint' => 'site-backup.zip must be in the same directory as restore.php',
|
||||
];
|
||||
ORIG,
|
||||
<<<'REPL'
|
||||
@@ -202,7 +191,7 @@ ORIG,
|
||||
'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']),
|
||||
'hint' => 'Place one or more backup ZIP files in the same directory as restore.php',
|
||||
];
|
||||
REPL
|
||||
);
|
||||
@@ -495,7 +484,7 @@ function actionPreflight(): array
|
||||
'label' => 'Backup Archive',
|
||||
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
|
||||
'ok' => file_exists(BACKUP_FILE),
|
||||
'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
|
||||
'hint' => 'site-backup.zip must be in the same directory as restore.php',
|
||||
];
|
||||
|
||||
$checks[] = [
|
||||
@@ -1551,7 +1540,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
|
||||
<div class="mr-container">
|
||||
<div class="mr-alert mr-alert-danger">
|
||||
<strong>Security:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> immediately after installation is complete.
|
||||
<strong>Security:</strong> Delete restore.php immediately after installation is complete.
|
||||
</div>
|
||||
|
||||
<!-- Step Progress -->
|
||||
@@ -1799,7 +1788,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<strong>Success!</strong> The site restoration is complete.
|
||||
</div>
|
||||
<div class="mr-alert mr-alert-danger">
|
||||
<strong>Important:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> and <code>site-backup.zip</code> from your server immediately for security.
|
||||
<strong>Important:</strong> Delete <code>restore.php</code> and <code>site-backup.zip</code> from your server immediately for security.
|
||||
</div>
|
||||
<div style="margin-top:1rem">
|
||||
<button class="mr-btn mr-btn-danger" onclick="runCleanup()">Remove Restore Files</button>
|
||||
@@ -1823,7 +1812,6 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
|
||||
<script>
|
||||
const TOKEN = <?php echo json_encode($token); ?>;
|
||||
const SCRIPT_URL = <?php echo json_encode(basename($_SERVER['SCRIPT_NAME'])); ?>;
|
||||
let currentStep = 1;
|
||||
let dbConfig = {};
|
||||
|
||||
@@ -1849,7 +1837,7 @@ async function post(action, extra) {
|
||||
}
|
||||
var res;
|
||||
try {
|
||||
res = await fetch(SCRIPT_URL, { method: 'POST', body: form });
|
||||
res = await fetch('restore.php', { method: 'POST', body: form });
|
||||
} catch (e) {
|
||||
log('Network error: ' + e.message);
|
||||
return { success: false, message: 'Network error: ' + e.message, checks: [] };
|
||||
|
||||
@@ -70,8 +70,7 @@ class SteppedBackupEngine
|
||||
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
|
||||
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
|
||||
$session->remoteStorage = $profile->remote_storage ?? 'none';
|
||||
$session->includeMokoRestore = $profile->include_mokorestore ?? '0';
|
||||
$session->restoreScriptName = $profile->restore_script_name ?? 'restore.php';
|
||||
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
|
||||
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
|
||||
|
||||
// Load multi-remote destinations from the remotes table
|
||||
@@ -378,30 +377,15 @@ class SteppedBackupEngine
|
||||
$this->verifyArchive($session->archivePath, $session->backupType);
|
||||
$session->log('Archive integrity verified');
|
||||
|
||||
// MokoRestore
|
||||
$mokoRestoreMode = $session->includeMokoRestore ?? '0';
|
||||
$restoreScriptName = $session->restoreScriptName ?? 'restore.php';
|
||||
|
||||
if ($mokoRestoreMode === '1') {
|
||||
// MokoRestore wrapper
|
||||
if ($session->includeMokoRestore) {
|
||||
$session->log('Wrapping with MokoRestore script...');
|
||||
$mokoRestorePath = $session->archivePath . '.mokorestore.zip';
|
||||
MokoRestore::wrap($session->archivePath, $mokoRestorePath, $restoreScriptName);
|
||||
MokoRestore::wrap($session->archivePath, $mokoRestorePath);
|
||||
@unlink($session->archivePath);
|
||||
rename($mokoRestorePath, $session->archivePath);
|
||||
$totalSize = filesize($session->archivePath);
|
||||
$session->log('MokoRestore archive created');
|
||||
} elseif ($mokoRestoreMode === 'standalone') {
|
||||
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
|
||||
$restoreDir = dirname($session->archivePath);
|
||||
$session->restoreScriptPath = $restoreDir . '/' . $restoreScriptName;
|
||||
|
||||
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
|
||||
@@ -479,10 +463,6 @@ class SteppedBackupEngine
|
||||
if ($result['success']) {
|
||||
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
||||
$session->log(' Upload complete: ' . $result['message']);
|
||||
|
||||
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
|
||||
$uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath));
|
||||
}
|
||||
} else {
|
||||
$uploadFailed = true;
|
||||
$session->log(' WARNING: Upload failed: ' . $result['message']);
|
||||
@@ -545,12 +525,6 @@ class SteppedBackupEngine
|
||||
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
|
||||
$session->log('Remote upload complete: ' . $result['message']);
|
||||
|
||||
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
|
||||
$restoreBasename = basename($session->restoreScriptPath);
|
||||
$session->log('Uploading standalone ' . $restoreBasename . '...');
|
||||
$uploader->upload($session->restoreScriptPath, $restoreBasename);
|
||||
}
|
||||
|
||||
if (!$session->remoteKeepLocal && is_file($session->archivePath)) {
|
||||
@unlink($session->archivePath);
|
||||
$session->log('Local copy removed');
|
||||
|
||||
@@ -51,9 +51,7 @@ class SteppedSession
|
||||
public array $excludeFiles = [];
|
||||
public array $excludeTables = [];
|
||||
public string $remoteStorage = 'none';
|
||||
public string $includeMokoRestore = '0';
|
||||
public string $restoreScriptName = 'restore.php';
|
||||
public string $restoreScriptPath = '';
|
||||
public bool $includeMokoRestore = false;
|
||||
public bool $remoteKeepLocal = true;
|
||||
public string $encryptionPassword = '';
|
||||
|
||||
|
||||
@@ -29,10 +29,7 @@ class SshKeyField extends FormField
|
||||
$id = $this->id;
|
||||
$name = $this->name;
|
||||
|
||||
$decoded = !empty($value) ? (base64_decode($value, true) ?: '') : '';
|
||||
$hasKey = !empty($value) && ($value === '__KEEP_EXISTING__'
|
||||
|| str_contains($value, 'PRIVATE KEY')
|
||||
|| str_contains($decoded, 'PRIVATE KEY'));
|
||||
$hasKey = !empty($value) && str_contains($value, 'PRIVATE KEY');
|
||||
|
||||
$html = '<div id="' . htmlspecialchars($id) . '-wrapper">';
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ class DashboardModel extends BaseDatabaseModel
|
||||
->select($db->quoteName(['id', 'title', 'backup_type']))
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order($db->quoteName('id') . ' ASC');
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
|
||||
@@ -60,14 +60,14 @@ class ProfilesModel extends ListModel
|
||||
$query->where('(' . $db->quoteName('a.title') . ' LIKE ' . $search . ')');
|
||||
}
|
||||
|
||||
$orderCol = $this->state->get('list.ordering', 'a.id');
|
||||
$orderCol = $this->state->get('list.ordering', 'a.ordering');
|
||||
$orderDir = $this->state->get('list.direction', 'ASC');
|
||||
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function populateState($ordering = 'a.id', $direction = 'ASC'): void
|
||||
protected function populateState($ordering = 'a.ordering', $direction = 'ASC'): void
|
||||
{
|
||||
parent::populateState($ordering, $direction);
|
||||
}
|
||||
|
||||
@@ -12,12 +12,8 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\View\Backup;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\CMS\Toolbar\Toolbar;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
@@ -38,24 +34,6 @@ class HtmlView extends BaseHtmlView
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
|
||||
|
||||
$user = Factory::getApplication()->getIdentity();
|
||||
|
||||
if ($this->item->status === 'complete'
|
||||
&& !empty($this->item->filesexist)
|
||||
&& $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')
|
||||
) {
|
||||
$toolbar = Toolbar::getInstance();
|
||||
$downloadUrl = Route::_(
|
||||
'index.php?option=com_mokosuitebackup&task=backups.download&id='
|
||||
. (int) $this->item->id . '&' . Session::getFormToken() . '=1'
|
||||
);
|
||||
$toolbar->linkButton('download', 'COM_MOKOJOOMBACKUP_DOWNLOAD')
|
||||
->url($downloadUrl)
|
||||
->icon('icon-download')
|
||||
->buttonClass('btn btn-success');
|
||||
}
|
||||
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitebackup&view=backups');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class HtmlView extends BaseHtmlView
|
||||
protected $state;
|
||||
public $filterForm;
|
||||
public $activeFilters = [];
|
||||
public $profiles = [];
|
||||
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
@@ -34,6 +35,16 @@ class HtmlView extends BaseHtmlView
|
||||
$this->filterForm = $this->get('FilterForm');
|
||||
$this->activeFilters = $this->get('ActiveFilters');
|
||||
|
||||
// Load published profiles for the backup selector
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName(['id', 'title', 'backup_type']))
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
->order($db->quoteName('ordering') . ' ASC');
|
||||
$db->setQuery($query);
|
||||
$this->profiles = $db->loadObjectList() ?: [];
|
||||
|
||||
$this->checkUpdateSite();
|
||||
$this->addToolbar();
|
||||
|
||||
@@ -101,6 +112,10 @@ class HtmlView extends BaseHtmlView
|
||||
|
||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
|
||||
|
||||
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
ToolbarHelper::custom('backups.start', 'download', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW', false);
|
||||
}
|
||||
|
||||
if ($user->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
|
||||
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,16 @@ class HtmlView extends BaseHtmlView
|
||||
$toolbar = Toolbar::getInstance();
|
||||
$profileId = (int) $this->item->id;
|
||||
|
||||
// "Run Backup Now" button — links to backup start with CSRF token
|
||||
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
|
||||
$runUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $profileId . '&' . Session::getFormToken() . '=1');
|
||||
$toolbar->linkButton('run-backup', 'COM_MOKOJOOMBACKUP_RUN_BACKUP_NOW')
|
||||
->url($runUrl)
|
||||
->icon('icon-play')
|
||||
->buttonClass('btn btn-success');
|
||||
}
|
||||
|
||||
// "View Backups" link button
|
||||
$backupsUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[PROFILE_ID]=' . $profileId);
|
||||
$toolbar->linkButton('view-backups', 'COM_MOKOJOOMBACKUP_VIEW_BACKUPS')
|
||||
->url($backupsUrl)
|
||||
|
||||
@@ -31,6 +31,31 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="j-main-container" class="j-main-container">
|
||||
<!-- Profile selector for Backup Now -->
|
||||
<?php $canRun = $user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup'); ?>
|
||||
<?php if (!empty($this->profiles) && $canRun) : ?>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body d-flex align-items-center gap-3">
|
||||
<label for="mb-profile-select" class="form-label mb-0 fw-bold">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_BACKUP_PROFILE'); ?>:
|
||||
</label>
|
||||
<select id="mb-profile-select" class="form-select" style="max-width:300px;">
|
||||
<?php foreach ($this->profiles as $profile) : ?>
|
||||
<option value="<?php echo (int) $profile->id; ?>">
|
||||
#<?php echo (int) $profile->id; ?> —
|
||||
<?php echo $this->escape($profile->title); ?>
|
||||
(<?php echo $this->escape($profile->backup_type); ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="btn btn-primary" onclick="window.mokosuitebackupStart()">
|
||||
<span class="icon-download" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
|
||||
|
||||
<?php if (empty($this->items)) : ?>
|
||||
@@ -64,6 +89,9 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<th scope="col" class="w-10">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
@@ -84,9 +112,7 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<?php endif; ?>
|
||||
</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); ?>
|
||||
</a>
|
||||
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
@@ -114,6 +140,35 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<td>
|
||||
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
|
||||
</td>
|
||||
<td class="d-flex gap-1">
|
||||
<?php if ($item->status === 'complete' && $item->filesexist && $canDownload) : ?>
|
||||
<?php
|
||||
$isWebAccessible = !empty($item->absolute_path)
|
||||
&& strpos(realpath($item->absolute_path) ?: $item->absolute_path, realpath(JPATH_ROOT) ?: JPATH_ROOT) === 0;
|
||||
?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.download&id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
|
||||
class="btn btn-sm btn-outline-primary" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_DOWNLOAD'); ?>">
|
||||
<span class="icon-download"></span>
|
||||
</a>
|
||||
<?php if ($isWebAccessible) : ?>
|
||||
<span class="badge bg-warning text-dark" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING'); ?>">
|
||||
<span class="icon-warning-circle" aria-hidden="true"></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->status === 'complete' && $item->filesexist) : ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-info mb-browse-archive"
|
||||
data-id="<?php echo (int) $item->id; ?>"
|
||||
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>">
|
||||
<span class="icon-folder-open"></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mb-view-log"
|
||||
data-id="<?php echo (int) $item->id; ?>"
|
||||
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?>">
|
||||
<span class="icon-file-alt"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo (int) $item->id; ?>
|
||||
</td>
|
||||
@@ -160,6 +215,19 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
|
||||
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
|
||||
|
||||
// Override the toolbar "Backup Now" button to use stepped backup
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Find the backup toolbar button and override it
|
||||
const toolbarBtn = document.querySelector('[onclick*="backups.start"], .button-download');
|
||||
if (toolbarBtn) {
|
||||
toolbarBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startSteppedBackup();
|
||||
return false;
|
||||
}, true);
|
||||
}
|
||||
});
|
||||
|
||||
var backupRunning = false;
|
||||
|
||||
@@ -391,6 +459,124 @@ $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...';
|
||||
bootstrap.Modal.getOrCreateInstance(modal).show();
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
// Log modal close handled by Bootstrap data-bs-dismiss
|
||||
|
||||
// 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 = '';
|
||||
bootstrap.Modal.getOrCreateInstance(modal).show();
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
// Browse modal close handled by Bootstrap data-bs-dismiss
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -468,6 +654,52 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Viewer Modal -->
|
||||
<div class="modal fade" id="mb-log-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" style="max-height:60vh; overflow-y:auto;">
|
||||
<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>
|
||||
|
||||
<!-- Archive Browser Modal -->
|
||||
<div class="modal fade" id="mb-browse-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<span class="icon-folder-open" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div class="px-3 py-2 bg-light border-bottom">
|
||||
<small id="mb-browse-summary" class="text-muted"></small>
|
||||
</div>
|
||||
<div style="max-height:60vh; overflow-y:auto;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purge Backups Modal -->
|
||||
<?php $canDelete = $user->authorise('core.delete', 'com_mokosuitebackup'); ?>
|
||||
|
||||
@@ -52,6 +52,9 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<th scope="col" class="w-10">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.published', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-10 text-center">
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
|
||||
</th>
|
||||
<th scope="col" class="w-5">
|
||||
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
|
||||
</th>
|
||||
@@ -84,6 +87,16 @@ $listDirn = $this->escape($this->state->get('list.direction'));
|
||||
<td>
|
||||
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'profiles.'); ?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<?php if ($item->published == 1) : ?>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
|
||||
class="btn btn-sm btn-outline-success"
|
||||
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>">
|
||||
<span class="icon-play" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo (int) $item->id; ?>
|
||||
</td>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-->
|
||||
<extension type="module" client="administrator" method="upgrade">
|
||||
<name>mod_mokosuitebackup_cpanel</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="actionlog" method="upgrade">
|
||||
<name>Action Log - MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="console" method="upgrade">
|
||||
<name>Console - MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="content" method="upgrade">
|
||||
<name>Content - MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-04</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<extension type="plugin" group="quickicon" method="upgrade">
|
||||
<name>Quick Icon - MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="system" method="upgrade">
|
||||
<name>System - MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
<extension type="plugin" group="webservices" method="upgrade">
|
||||
<name>Web Services - MokoSuiteBackup</name>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuiteBackup</name>
|
||||
<packagename>mokosuitebackup</packagename>
|
||||
<version>01.43.32</version>
|
||||
<version>01.43.07</version>
|
||||
<creationDate>2026-06-02</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -29,7 +29,6 @@
|
||||
<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="module" id="mod_mokosuitebackup_cpanel" client="administrator">mod_mokosuitebackup_cpanel.zip</file>
|
||||
<file type="package" id="pkg_mokosuiteclient">MokoSuiteClient.zip</file>
|
||||
</files>
|
||||
|
||||
<languages>
|
||||
|
||||
Reference in New Issue
Block a user