Compare commits

..

43 Commits

Author SHA1 Message Date
gitea-actions[bot] 4a027d6245 chore(version): pre-release bump to 01.43.32-dev [skip ci] 2026-06-28 07:49:06 +00:00
jmiller 8af19f875c chore: add SECURITY.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Has been cancelled
2026-06-28 07:25:44 +00:00
jmiller b56e4060bf chore: add SECURITY.md from Template-Joomla
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 29s
2026-06-28 07:15:34 +00:00
gitea-actions[bot] 9757658c34 chore(version): pre-release bump to 01.43.31-dev [skip ci] 2026-06-27 02:33:25 +00:00
gitea-actions[bot] c82378128a chore(version): auto-bump patch 01.43.30-dev [skip ci] 2026-06-27 02:33:14 +00:00
jmiller f95505704a fix: add submodule checkout to pre-release workflow
Universal: Auto Version Bump / Version Bump (push) Successful in 11s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
The CI checkout step was missing submodules: recursive, causing
MokoSuiteClient to be an empty gitlink during builds. This resulted
in broken MokoSuiteClient.zip and "Install path does not exist" errors.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-26 21:32:49 -05:00
gitea-actions[bot] 6cdc9b04d0 chore(version): pre-release bump to 01.43.29-dev [skip ci] 2026-06-27 02:20:50 +00:00
jmiller bad73529ae Merge pull request 'fix: SSH key indicator, schema alignment, MokoSuiteClient bundle' (#155) from fix/ssh-key-indicator into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 11s
2026-06-27 02:20:30 +00:00
jmiller 288baf41d3 fix: remove duplicate version tags from 8 manifests, align AjaxController to params column
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Secret Scan (pull_request) Successful in 5s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 30s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
CI version_bump was creating duplicate <version> lines in all
sub-extension manifests. Also AjaxController still referenced the old
`config` column and removed `keep_local` column on the remotes table.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-26 21:19:21 -05:00
gitea-actions[bot] 7d1dcf3e1c chore(version): pre-release bump to 01.43.26-dev [skip ci] 2026-06-26 21:19:20 -05:00
jmiller 2002c1fcad fix: remove stray 't' in package manifest and duplicate version in component manifest
The CI version_bump wrote 't' instead of a tab before <version> in
pkg_mokosuitebackup.xml, and appended a duplicate <version> line in
mokosuitebackup.xml instead of replacing the existing one.

Claude-Session: https://claude.ai/code/session_01MbEjBtsSjPuTWhqqrMS2wG
2026-06-26 21:19:19 -05:00
gitea-actions[bot] 4abe81f916 chore(version): pre-release bump to 01.43.25-dev [skip ci] 2026-06-26 21:19:19 -05:00
jmiller 571b03743f docs: update README with multi-remote, MokoSuiteClient, sub-extension count 2026-06-26 21:19:18 -05:00
gitea-actions[bot] 7fc1cad305 chore(version): pre-release bump to 01.43.24-dev [skip ci] 2026-06-26 21:19:18 -05:00
jmiller 03a1dd75c9 feat: bundle MokoSuiteClient as nested package in release ZIP
- Add MokoSuiteClient as git submodule under source/packages/
- Add pkg_mokosuiteclient entry to pkg_mokosuitebackup.xml
- Fix duplicate <version> tag in package manifest
2026-06-26 21:19:17 -05:00
gitea-actions[bot] 02d8312d1b chore(version): pre-release bump to 01.43.23-dev [skip ci] 2026-06-26 21:19:16 -05:00
jmiller c508fcc8d5 fix: align remotes table schema, add restore_script_name column, profile ordering
- install.mysql.sql: rename `config` → `params` and drop `keep_local` from remotes
  table to match update file 01.41.00 and RemoteTable.php code (fixes Joomla
  database maintenance "one problem")
- install.mysql.sql: fix idx_enabled index to use composite (profile_id, enabled)
- install.mysql.sql: add restore_script_name column to profiles table
- 01.43.22.sql: ALTER TABLE to add restore_script_name for existing installs
- DashboardModel: order profile dropdown by ID instead of ordering column
- SteppedBackupEngine: add stack trace logging around MokoRestore standalone
  generation to debug str_replace FATAL on SFTP profiles
2026-06-26 21:18:47 -05:00
gitea-actions[bot] d104b7b936 chore(version): pre-release bump to 01.43.22-dev [skip ci] 2026-06-25 17:18:29 +00:00
jmiller 80110ac111 Merge pull request 'fix: SSH key indicator and missing delete language key' (#154) from fix/ssh-key-indicator into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
2026-06-25 17:18:21 +00:00
gitea-actions[bot] 3bd1f63833 chore(version): pre-release bump to 01.43.21-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
2026-06-25 17:18:03 +00:00
jmiller 93f0fa0a47 fix: SSH key indicator detection and missing delete language key
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 9s
- SshKeyField: detect base64-encoded keys from DB so the "Key loaded"
  badge displays correctly after initial upload
- Add COM_MOKOJOOMBACKUP_BACKUPS_N_ITEMS_DELETED language keys for
  Joomla's AdminController delete feedback message
2026-06-25 12:17:45 -05:00
gitea-actions[bot] 268b3d54d7 chore(version): pre-release bump to 01.43.20-dev [skip ci] 2026-06-25 16:27:48 +00:00
jmiller 1cfe7c6c6e Merge pull request 'fix: add SQL update file to match manifest version' (#153) from fix/schema-version-file-2 into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 19s
fix: add SQL update file to match manifest version
2026-06-25 16:26:38 +00:00
jmiller f0da0c02b4 fix: add SQL update file to match manifest version
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Failing after 7s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 46s
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Joomla's Database view requires a SQL update file matching the manifest
version. Missing file causes persistent schema version mismatch warning.
2026-06-25 11:25:56 -05:00
gitea-actions[bot] 2f8a65388c chore(version): pre-release bump to 01.43.19-dev [skip ci] 2026-06-25 16:13:23 +00:00
gitea-actions[bot] 9978622960 chore(version): pre-release bump to 01.43.18-dev [skip ci] 2026-06-25 16:13:03 +00:00
jmiller 35e5fc1503 Merge pull request 'fix(db): add 01.43.11 schema update file' (#152) from fix/schema-version-file into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 23s
2026-06-25 16:12:46 +00:00
gitea-actions[bot] 2338ba5197 chore(version): pre-release bump to 01.43.17-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
2026-06-25 16:12:33 +00:00
jmiller e67eedbc93 fix(db): add 01.43.11 schema update file to resolve version mismatch
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
Joomla's database checker requires a SQL update file matching the manifest
version. Missing file caused schema version to stay at 01.41.00.
2026-06-25 11:12:22 -05:00
gitea-actions[bot] d812aca832 chore(version): pre-release bump to 01.43.15-dev [skip ci] 2026-06-25 16:00:54 +00:00
gitea-actions[bot] 4315f36c6a chore(version): pre-release bump to 01.43.14-dev [skip ci] 2026-06-25 15:59:41 +00:00
jmiller 10467835ac Merge pull request 'fix: UI cleanup, custom restore script name, version bump 01.43.11-dev' (#150) from fix/ui-cleanup-restore-name into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
2026-06-25 15:59:30 +00:00
gitea-actions[bot] f26d58504e chore(version): pre-release bump to 01.43.13-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-06-25 15:58:38 +00:00
jmiller 07fb4dcc24 fix: remove run/backup buttons, move actions to detail view, custom restore script name, version bump 01.43.11-dev
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
- Remove Run Backup / Backup Now buttons from profiles list, profile edit toolbar, and backup records view
- Move download, browse archive, and view log from backup list rows into individual backup record detail view
- Add download button to backup detail toolbar
- Link profile column in backup records list to profile edit
- Complete restore script filename customization across BackupEngine, SteppedBackupEngine, and MokoRestore
- Remove ordering field from profiles, default sort by ID ascending
- Fix untranslated JFIELD language keys
- Bump all manifests to 01.43.11-dev
2026-06-25 10:54:35 -05:00
gitea-actions[bot] 21a4352b3b chore(version): pre-release bump to 01.43.10-dev [skip ci] 2026-06-25 15:02:09 +00:00
gitea-actions[bot] 9d26f59f98 chore(version): pre-release bump to 01.43.09-dev [skip ci] 2026-06-25 15:01:45 +00:00
jmiller 3488434f28 Merge pull request 'fix(mokorestore): Joomla detection, multi-zip selector, standalone backup scan' (#148) from fix/mokorestore-improvements into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-25 15:01:14 +00:00
gitea-actions[bot] f97cd30c95 chore(version): pre-release bump to 01.43.08-dev [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
2026-06-25 15:00:33 +00:00
jmiller 836d1bc8b7 fix(mokorestore): add Joomla detection warning, multi-zip selector, and standalone backup scan
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 13s
- Preflight now detects existing Joomla installation (configuration.php / Version.php)
  and shows a yellow warning — does not block, but alerts the user
- Standalone mode: backup archive check scans for all ZIPs instead of hardcoded name
- Multi-zip selector integrated into extract step with radio buttons
- Selected backup file passed through to extract action
- Added warn-style CSS class (yellow) for preflight warnings
2026-06-25 10:00:07 -05:00
gitea-actions[bot] 79b3caa35a chore(version): pre-release bump to 01.43.05-dev [skip ci] 2026-06-25 13:39:28 +00:00
gitea-actions[bot] 6102c8f590 chore(version): pre-release bump to 01.43.04-dev [skip ci] 2026-06-25 13:39:01 +00:00
jmiller 88e53c5698 Merge pull request 'fix: Bootstrap 5 modals, language keys, ntfy default, MokoRestore error handling' (#146) from fix/bootstrap-modals into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 18s
2026-06-25 13:38:43 +00:00
gitea-actions[bot] ec1c3486c5 chore(version): pre-release bump to 01.43.03-dev [skip ci]
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
2026-06-25 13:38:28 +00:00
47 changed files with 435 additions and 341 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "source/packages/MokoSuiteClient"]
path = source/packages/MokoSuiteClient
url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient.git
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Automation
# VERSION: 01.43.07
# VERSION: 01.43.32
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+7
View File
@@ -59,6 +59,13 @@ 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:
+23 -1
View File
@@ -2,6 +2,28 @@
## [Unreleased]
### Added
- Customizable restore script filename per backup profile (reduces discoverability on remote servers)
- MokoRestore standalone mode: multi-ZIP selector when multiple backup archives are present
- MokoRestore preflight: Joomla installation detection warning before overwriting an existing site
- MokoRestore error handling: try/catch on fetch calls, HTTP status checks, JSON parse recovery
- Download button on individual backup record detail toolbar
- Profile column in backup records list links to the profile edit view
### Changed
- Moved download, browse archive, and view log actions from backup list rows into the individual backup record view
- Removed "Run Backup" / "Backup Now" buttons from profiles list, profile edit toolbar, and backup records view (backups are triggered from the dashboard only)
- Removed ordering field from profiles; default sort is now by ID ascending
- MokoRestore cleanup and security messages now reference the actual script filename instead of hardcoded "restore.php"
### Fixed
- Bootstrap 5 modal conversion for snapshots view (data-bs-dismiss, modal-footer, getOrCreateInstance)
- ntfy default URL changed from ntfy.sh to ntfy.mokoconsulting.tech
- Untranslated JFIELD_ORDERING_ASC / JFIELD_ORDERING_LABEL language keys replaced with component-specific keys
- Options page title now shows "MokoSuiteBackup Options" instead of raw language key
- Profile dropdown IDs in backup records and dashboard show "#ID — Title (type)" format
- MokoRestore stalling: unhandled promise rejections from network errors or non-JSON responses left UI in loading state
## [01.43.00] --- 2026-06-24
@@ -71,7 +93,7 @@
- Backup comparison: select two backups for side-by-side diff
- Archive browser: view files inside backup without extracting
- Manual purge: delete backups older than a date with count preview
- Run Backup button on profile list and edit views with backup count badges
- Backup count badges on profile list
- "Do not navigate away" warning in backup/restore progress modals
- Clickable placeholder pills for backup directory and archive name fields
- Comprehensive help modal with absolute/relative/placeholder path documentation
+7 -2
View File
@@ -5,7 +5,7 @@ Full-site backup and restore for Joomla — database, files, and configuration.
| Field | Value |
|---|---|
| **Package** | `pkg_mokosuitebackup` |
| **Type** | Joomla Package (8 sub-extensions) |
| **Type** | Joomla Package (9 sub-extensions + MokoSuiteClient) |
| **Joomla** | 6.x+ |
| **PHP** | 8.1+ |
| **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
### 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)
- Google Drive with OAuth2 and resumable uploads
- 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
- 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
View File
@@ -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 |
@@ -24,10 +24,9 @@
name="fullordering"
type="list"
label="JGLOBAL_SORT_BY"
default="a.ordering ASC"
default="a.id ASC"
onchange="this.form.submit();"
>
<option value="a.ordering ASC">JFIELD_ORDERING_ASC</option>
<option value="a.title ASC">COM_MOKOJOOMBACKUP_HEADING_TITLE_ASC</option>
<option value="a.title DESC">COM_MOKOJOOMBACKUP_HEADING_TITLE_DESC</option>
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
@@ -93,6 +93,16 @@
<option value="1">COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED</option>
<option value="standalone">COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE</option>
</field>
<field
name="restore_script_name"
type="text"
label="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME"
description="COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC"
default="restore.php"
maxlength="128"
filter="string"
showon="include_mokorestore!:0"
/>
<field
name="encryption_password"
type="password"
@@ -164,12 +174,6 @@
<option value="1">JPUBLISHED</option>
<option value="0">JUNPUBLISHED</option>
</field>
<field
name="ordering"
type="number"
label="JFIELD_ORDERING_LABEL"
default="0"
/>
</fieldset>
<fieldset name="filters" label="COM_MOKOJOOMBACKUP_FIELDSET_FILTERS">
@@ -42,6 +42,8 @@ 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."
@@ -140,6 +142,8 @@ COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="None: no restore script. Wrap
COM_MOKOJOOMBACKUP_MOKORESTORE_NONE="None"
COM_MOKOJOOMBACKUP_MOKORESTORE_WRAPPED="Wrapped (inside backup ZIP)"
COM_MOKOJOOMBACKUP_MOKORESTORE_STANDALONE="Standalone (separate restore.php)"
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME="Restore Script Filename"
COM_MOKOJOOMBACKUP_FIELD_RESTORE_SCRIPT_NAME_DESC="Custom filename for the restore script. Must end in .php. Use a non-obvious name to reduce discoverability on remote servers (e.g. moko-install-xyz.php)."
; Data Sanitization
COM_MOKOJOOMBACKUP_FIELDSET_SANITIZATION="Data Sanitization"
@@ -7,7 +7,7 @@
-->
<extension type="component" method="upgrade">
<name>MokoSuiteBackup</name>
<version>01.43.07</version>
<version>01.43.32</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<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',
`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',
@@ -113,14 +114,13 @@ 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,
`keep_local` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Keep local copy after upload',
`config` MEDIUMTEXT NOT NULL COMMENT 'JSON — type-specific settings',
`params` MEDIUMTEXT 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` (`enabled`)
KEY `idx_enabled` (`profile_id`, `enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 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;
}
// Decode JSON config and mask secrets
// Decode JSON params and mask secrets
$items = [];
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
$masked = $this->maskSecrets($config, $row->type);
@@ -939,8 +939,7 @@ class AjaxController extends BaseController
'title' => $row->title,
'type' => $row->type,
'enabled' => (int) $row->enabled,
'keep_local' => (int) $row->keep_local,
'config' => $masked,
'params' => $masked,
'ordering' => (int) $row->ordering,
];
}
@@ -971,7 +970,6 @@ 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) {
@@ -1019,9 +1017,7 @@ class AjaxController extends BaseController
$table->title = $title;
$table->type = $type;
$table->enabled = $enabled ? 1 : 0;
$table->keep_local = $keepLocal ? 1 : 0;
$table->config = json_encode($config);
$table->params = json_encode($config);
if (!$table->check() || !$table->store()) {
$this->sendJson(['error' => true, 'message' => $table->getError() ?: 'Save failed']);
@@ -1190,7 +1186,7 @@ class AjaxController extends BaseController
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('config'))
->select($db->quoteName('params'))
->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);
MokoRestore::wrap($archivePath, $mokoRestorePath, $restoreScriptName);
if (is_file($archivePath) && !unlink($archivePath)) {
$this->log('WARNING: Could not remove pre-wrap archive');
@@ -278,11 +278,11 @@ class BackupEngine
$this->log('MokoRestore archive created: ' . $sizeHuman);
$this->log('SHA-256 (wrapped): ' . $checksum);
} elseif ($mokoRestoreMode === 'standalone') {
// Standalone mode: restore.php as a separate file next to the backup ZIP
$this->log('Generating standalone restore.php...');
$restoreScriptPath = $this->backupDir . '/restore.php';
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
$this->log('Generating standalone ' . $restoreScriptName . '...');
$restoreScriptPath = $this->backupDir . '/' . $restoreScriptName;
MokoRestore::generateStandalone($restoreScriptPath);
$this->log('Standalone restore.php generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
$this->log('Standalone ' . $restoreScriptName . ' generated (' . number_format(filesize($restoreScriptPath)) . ' bytes)');
}
$remoteFilename = '';
@@ -303,9 +303,8 @@ class BackupEngine
$remoteFilename = $result['remote_path'] ?? $archiveName;
$this->log(' Upload complete: ' . $result['message']);
/* Upload standalone restore.php if in standalone mode */
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
$uploader->upload($restoreScriptPath, 'restore.php');
$uploader->upload($restoreScriptPath, basename($restoreScriptPath));
}
} else {
$uploadFailed = true;
@@ -336,15 +335,15 @@ class BackupEngine
$remoteFilename = $uploadResult['remote_path'] ?? $archiveName;
$this->log('Remote upload complete: ' . $uploadResult['message']);
// Upload standalone restore.php alongside the backup if in standalone mode
if (!empty($restoreScriptPath) && is_file($restoreScriptPath)) {
$this->log('Uploading standalone restore.php...');
$restoreUpload = $uploader->upload($restoreScriptPath, 'restore.php');
$restoreBasename = basename($restoreScriptPath);
$this->log('Uploading standalone ' . $restoreBasename . '...');
$restoreUpload = $uploader->upload($restoreScriptPath, $restoreBasename);
if ($restoreUpload['success']) {
$this->log('Standalone restore.php uploaded');
$this->log('Standalone ' . $restoreBasename . ' uploaded');
} else {
$this->log('WARNING: restore.php upload failed: ' . $restoreUpload['message']);
$this->log('WARNING: ' . $restoreBasename . ' upload failed: ' . $restoreUpload['message']);
}
}
@@ -35,25 +35,36 @@ class MokoRestore
*
* @return string Path to the wrapped archive
*/
public static function wrap(string $backupArchive, string $outputPath): string
public static function wrap(string $backupArchive, string $outputPath, string $scriptName = 'restore.php'): string
{
$scriptName = self::sanitizeScriptName($scriptName);
$zip = new \ZipArchive();
if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath);
}
// Add the standalone restore script
$zip->addFromString('restore.php', self::generateRestoreScript());
// Add the original backup as a nested ZIP
$zip->addFromString($scriptName, self::generateRestoreScript());
$zip->addFile($backupArchive, 'site-backup.zip');
$zip->close();
return $outputPath;
}
public static function sanitizeScriptName(string $name): string
{
$name = basename(trim($name));
if ($name === '' || !str_ends_with(strtolower($name), '.php')) {
$name = 'restore.php';
}
$name = preg_replace('/[^a-zA-Z0-9._-]/', '', $name);
return $name ?: 'restore.php';
}
/**
* Generate the standalone restore.php script as a separate file.
*
@@ -173,7 +184,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 restore.php',
'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
];
ORIG,
<<<'REPL'
@@ -191,7 +202,7 @@ ORIG,
'label' => 'Backup Archive',
'value' => $archiveValue,
'ok' => $backupCount > 0,
'hint' => 'Place one or more backup ZIP files in the same directory as restore.php',
'hint' => 'Place one or more backup ZIP files in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
];
REPL
);
@@ -484,7 +495,7 @@ function actionPreflight(): array
'label' => 'Backup Archive',
'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found',
'ok' => file_exists(BACKUP_FILE),
'hint' => 'site-backup.zip must be in the same directory as restore.php',
'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']),
];
$checks[] = [
@@ -1540,7 +1551,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<div class="mr-container">
<div class="mr-alert mr-alert-danger">
<strong>Security:</strong> Delete restore.php immediately after installation is complete.
<strong>Security:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> immediately after installation is complete.
</div>
<!-- Step Progress -->
@@ -1788,7 +1799,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<strong>Success!</strong> The site restoration is complete.
</div>
<div class="mr-alert mr-alert-danger">
<strong>Important:</strong> Delete <code>restore.php</code> and <code>site-backup.zip</code> from your server immediately for security.
<strong>Important:</strong> Delete <code><?php echo htmlspecialchars(basename($_SERVER['SCRIPT_NAME'])); ?></code> and <code>site-backup.zip</code> from your server immediately for security.
</div>
<div style="margin-top:1rem">
<button class="mr-btn mr-btn-danger" onclick="runCleanup()">Remove Restore Files</button>
@@ -1812,6 +1823,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<script>
const TOKEN = <?php echo json_encode($token); ?>;
const SCRIPT_URL = <?php echo json_encode(basename($_SERVER['SCRIPT_NAME'])); ?>;
let currentStep = 1;
let dbConfig = {};
@@ -1837,7 +1849,7 @@ async function post(action, extra) {
}
var res;
try {
res = await fetch('restore.php', { method: 'POST', body: form });
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: [] };
@@ -70,7 +70,8 @@ class SteppedBackupEngine
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
$session->remoteStorage = $profile->remote_storage ?? 'none';
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
$session->includeMokoRestore = $profile->include_mokorestore ?? '0';
$session->restoreScriptName = $profile->restore_script_name ?? 'restore.php';
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
// Load multi-remote destinations from the remotes table
@@ -377,15 +378,30 @@ class SteppedBackupEngine
$this->verifyArchive($session->archivePath, $session->backupType);
$session->log('Archive integrity verified');
// MokoRestore wrapper
if ($session->includeMokoRestore) {
// MokoRestore
$mokoRestoreMode = $session->includeMokoRestore ?? '0';
$restoreScriptName = $session->restoreScriptName ?? 'restore.php';
if ($mokoRestoreMode === '1') {
$session->log('Wrapping with MokoRestore script...');
$mokoRestorePath = $session->archivePath . '.mokorestore.zip';
MokoRestore::wrap($session->archivePath, $mokoRestorePath);
MokoRestore::wrap($session->archivePath, $mokoRestorePath, $restoreScriptName);
@unlink($session->archivePath);
rename($mokoRestorePath, $session->archivePath);
$totalSize = filesize($session->archivePath);
$session->log('MokoRestore archive created');
} elseif ($mokoRestoreMode === 'standalone') {
$restoreScriptName = MokoRestore::sanitizeScriptName($restoreScriptName);
$restoreDir = dirname($session->archivePath);
$session->restoreScriptPath = $restoreDir . '/' . $restoreScriptName;
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
@@ -463,6 +479,10 @@ class SteppedBackupEngine
if ($result['success']) {
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
$session->log(' Upload complete: ' . $result['message']);
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
$uploader->upload($session->restoreScriptPath, basename($session->restoreScriptPath));
}
} else {
$uploadFailed = true;
$session->log(' WARNING: Upload failed: ' . $result['message']);
@@ -525,6 +545,12 @@ class SteppedBackupEngine
$remoteFilename = $result['remote_path'] ?? $session->archiveName;
$session->log('Remote upload complete: ' . $result['message']);
if (!empty($session->restoreScriptPath) && is_file($session->restoreScriptPath)) {
$restoreBasename = basename($session->restoreScriptPath);
$session->log('Uploading standalone ' . $restoreBasename . '...');
$uploader->upload($session->restoreScriptPath, $restoreBasename);
}
if (!$session->remoteKeepLocal && is_file($session->archivePath)) {
@unlink($session->archivePath);
$session->log('Local copy removed');
@@ -51,7 +51,9 @@ class SteppedSession
public array $excludeFiles = [];
public array $excludeTables = [];
public string $remoteStorage = 'none';
public bool $includeMokoRestore = false;
public string $includeMokoRestore = '0';
public string $restoreScriptName = 'restore.php';
public string $restoreScriptPath = '';
public bool $remoteKeepLocal = true;
public string $encryptionPassword = '';
@@ -29,7 +29,10 @@ class SshKeyField extends FormField
$id = $this->id;
$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">';
@@ -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('ordering') . ' ASC');
->order($db->quoteName('id') . ' 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.ordering');
$orderCol = $this->state->get('list.ordering', 'a.id');
$orderDir = $this->state->get('list.direction', 'ASC');
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
return $query;
}
protected function populateState($ordering = 'a.ordering', $direction = 'ASC'): void
protected function populateState($ordering = 'a.id', $direction = 'ASC'): void
{
parent::populateState($ordering, $direction);
}
@@ -12,8 +12,12 @@ namespace Joomla\Component\MokoSuiteBackup\Administrator\View\Backup;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
class HtmlView extends BaseHtmlView
@@ -34,6 +38,24 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
$user = Factory::getApplication()->getIdentity();
if ($this->item->status === 'complete'
&& !empty($this->item->filesexist)
&& $user->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')
) {
$toolbar = Toolbar::getInstance();
$downloadUrl = Route::_(
'index.php?option=com_mokosuitebackup&task=backups.download&id='
. (int) $this->item->id . '&' . Session::getFormToken() . '=1'
);
$toolbar->linkButton('download', 'COM_MOKOJOOMBACKUP_DOWNLOAD')
->url($downloadUrl)
->icon('icon-download')
->buttonClass('btn btn-success');
}
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitebackup&view=backups');
}
}
@@ -25,7 +25,6 @@ class HtmlView extends BaseHtmlView
protected $state;
public $filterForm;
public $activeFilters = [];
public $profiles = [];
public function display($tpl = null): void
{
@@ -35,16 +34,6 @@ class HtmlView extends BaseHtmlView
$this->filterForm = $this->get('FilterForm');
$this->activeFilters = $this->get('ActiveFilters');
// Load published profiles for the backup selector
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'title', 'backup_type']))
->from($db->quoteName('#__mokosuitebackup_profiles'))
->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query);
$this->profiles = $db->loadObjectList() ?: [];
$this->checkUpdateSite();
$this->addToolbar();
@@ -112,10 +101,6 @@ class HtmlView extends BaseHtmlView
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUPS_TITLE'), 'database');
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
ToolbarHelper::custom('backups.start', 'download', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW', false);
}
if ($user->authorise('mokosuitebackup.backup.restore', 'com_mokosuitebackup')) {
ToolbarHelper::custom('backups.restore', 'upload', '', 'COM_MOKOJOOMBACKUP_TOOLBAR_RESTORE', true);
}
@@ -55,16 +55,6 @@ class HtmlView extends BaseHtmlView
$toolbar = Toolbar::getInstance();
$profileId = (int) $this->item->id;
// "Run Backup Now" button — links to backup start with CSRF token
if ($user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) {
$runUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $profileId . '&' . Session::getFormToken() . '=1');
$toolbar->linkButton('run-backup', 'COM_MOKOJOOMBACKUP_RUN_BACKUP_NOW')
->url($runUrl)
->icon('icon-play')
->buttonClass('btn btn-success');
}
// "View Backups" link button
$backupsUrl = Route::_('index.php?option=com_mokosuitebackup&view=backups&filter[PROFILE_ID]=' . $profileId);
$toolbar->linkButton('view-backups', 'COM_MOKOJOOMBACKUP_VIEW_BACKUPS')
->url($backupsUrl)
@@ -31,31 +31,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<div class="row">
<div class="col-md-12">
<div id="j-main-container" class="j-main-container">
<!-- Profile selector for Backup Now -->
<?php $canRun = $user->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup'); ?>
<?php if (!empty($this->profiles) && $canRun) : ?>
<div class="card mb-3">
<div class="card-body d-flex align-items-center gap-3">
<label for="mb-profile-select" class="form-label mb-0 fw-bold">
<?php echo Text::_('COM_MOKOJOOMBACKUP_BACKUP_PROFILE'); ?>:
</label>
<select id="mb-profile-select" class="form-select" style="max-width:300px;">
<?php foreach ($this->profiles as $profile) : ?>
<option value="<?php echo (int) $profile->id; ?>">
#<?php echo (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)) : ?>
@@ -89,9 +64,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'COM_MOKOJOOMBACKUP_HEADING_DATE', 'a.backupstart', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-5">
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
</th>
<th scope="col" class="w-5">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
@@ -112,7 +84,9 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<?php endif; ?>
</td>
<td>
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=profile.edit&id=' . (int) $item->profile_id); ?>">
<?php echo $this->escape($item->profile_title ?? 'Profile #' . $item->profile_id); ?>
</a>
</td>
<td>
<?php
@@ -140,35 +114,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<td>
<?php echo HTMLHelper::_('date', $item->backupstart, Text::_('DATE_FORMAT_LC4')); ?>
</td>
<td class="d-flex gap-1">
<?php if ($item->status === 'complete' && $item->filesexist && $canDownload) : ?>
<?php
$isWebAccessible = !empty($item->absolute_path)
&& strpos(realpath($item->absolute_path) ?: $item->absolute_path, realpath(JPATH_ROOT) ?: JPATH_ROOT) === 0;
?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&task=backups.download&id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
class="btn btn-sm btn-outline-primary" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_DOWNLOAD'); ?>">
<span class="icon-download"></span>
</a>
<?php if ($isWebAccessible) : ?>
<span class="badge bg-warning text-dark" title="<?php echo Text::_('COM_MOKOJOOMBACKUP_WEB_ACCESSIBLE_WARNING'); ?>">
<span class="icon-warning-circle" aria-hidden="true"></span>
</span>
<?php endif; ?>
<?php endif; ?>
<?php if ($item->status === 'complete' && $item->filesexist) : ?>
<button type="button" class="btn btn-sm btn-outline-info mb-browse-archive"
data-id="<?php echo (int) $item->id; ?>"
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_BROWSE_ARCHIVE'); ?>">
<span class="icon-folder-open"></span>
</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-secondary mb-view-log"
data-id="<?php echo (int) $item->id; ?>"
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_VIEW_LOG'); ?>">
<span class="icon-file-alt"></span>
</button>
</td>
<td>
<?php echo (int) $item->id; ?>
</td>
@@ -215,19 +160,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
const AJAX_URL = <?php echo json_encode($ajaxUrl); ?>;
const TOKEN_NAME = <?php echo json_encode($ajaxToken); ?>;
// Override the toolbar "Backup Now" button to use stepped backup
document.addEventListener('DOMContentLoaded', function() {
// Find the backup toolbar button and override it
const toolbarBtn = document.querySelector('[onclick*="backups.start"], .button-download');
if (toolbarBtn) {
toolbarBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
startSteppedBackup();
return false;
}, true);
}
});
var backupRunning = false;
@@ -459,124 +391,6 @@ $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>
@@ -654,52 +468,6 @@ $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,9 +52,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<th scope="col" class="w-10">
<?php echo HTMLHelper::_('searchtools.sort', 'JSTATUS', 'a.published', $listDirn, $listOrder); ?>
</th>
<th scope="col" class="w-10 text-center">
<?php echo Text::_('COM_MOKOJOOMBACKUP_HEADING_ACTIONS'); ?>
</th>
<th scope="col" class="w-5">
<?php echo HTMLHelper::_('searchtools.sort', 'JGRID_HEADING_ID', 'a.id', $listDirn, $listOrder); ?>
</th>
@@ -87,16 +84,6 @@ $listDirn = $this->escape($this->state->get('list.direction'));
<td>
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'profiles.'); ?>
</td>
<td class="text-center">
<?php if ($item->published == 1) : ?>
<a href="<?php echo Route::_('index.php?option=com_mokosuitebackup&view=backups&task=backups.start&profile_id=' . $item->id . '&' . Session::getFormToken() . '=1'); ?>"
class="btn btn-sm btn-outline-success"
title="<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>">
<span class="icon-play" aria-hidden="true"></span>
<?php echo Text::_('COM_MOKOJOOMBACKUP_RUN_BACKUP'); ?>
</a>
<?php endif; ?>
</td>
<td>
<?php echo (int) $item->id; ?>
</td>
@@ -8,7 +8,7 @@
-->
<extension type="module" client="administrator" method="upgrade">
<name>mod_mokosuitebackup_cpanel</name>
<version>01.43.07</version>
<version>01.43.32</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.07</version>
<version>01.43.32</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.07</version>
<version>01.43.32</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.07</version>
<version>01.43.32</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.07</version>
<version>01.43.32</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.07</version>
<version>01.43.32</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.07</version>
<version>01.43.32</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.07</version>
<version>01.43.32</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+2 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoSuiteBackup</name>
<packagename>mokosuitebackup</packagename>
<version>01.43.07</version>
<version>01.43.32</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<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="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>