diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..12adab7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "source/packages/MokoSuiteClient"] + path = source/packages/MokoSuiteClient + url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteClient.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e02f3..2d8435f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index edf4659..b949bbb 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c15d68b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,241 @@ + + +# 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 | diff --git a/source/packages/MokoSuiteClient b/source/packages/MokoSuiteClient new file mode 160000 index 0000000..f0a8ad6 --- /dev/null +++ b/source/packages/MokoSuiteClient @@ -0,0 +1 @@ +Subproject commit f0a8ad6c39cc9fa4190a7f1b06c63dda16c0c6c0 diff --git a/source/packages/com_mokosuitebackup/config.xml b/source/packages/com_mokosuitebackup/config.xml index ff63899..84db7fc 100644 --- a/source/packages/com_mokosuitebackup/config.xml +++ b/source/packages/com_mokosuitebackup/config.xml @@ -21,7 +21,7 @@ type="sql" label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE" description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC" - query="SELECT id AS value, CONCAT(title, ' (#', id, ')') AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC" + query="SELECT id AS value, CONCAT(title, ' (#', id, ')') AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY id ASC" default="1" > @@ -245,7 +245,7 @@ type="text" label="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER" description="COM_MOKOJOOMBACKUP_CONFIG_NTFY_SERVER_DESC" - default="https://ntfy.sh" + default="https://ntfy.mokoconsulting.tech" filter="url" /> - diff --git a/source/packages/com_mokosuitebackup/forms/profile.xml b/source/packages/com_mokosuitebackup/forms/profile.xml index 661673d..81a46de 100644 --- a/source/packages/com_mokosuitebackup/forms/profile.xml +++ b/source/packages/com_mokosuitebackup/forms/profile.xml @@ -93,6 +93,16 @@ + JPUBLISHED -
diff --git a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini index 8942f04..8547103 100644 --- a/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini +++ b/source/packages/com_mokosuitebackup/language/en-GB/com_mokosuitebackup.ini @@ -5,6 +5,7 @@ ; @license GPL-3.0-or-later COM_MOKOJOOMBACKUP="MokoSuiteBackup" +COM_MOKOJOOMBACKUP_CONFIGURATION="MokoSuiteBackup Options" COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla" ; Submenu @@ -41,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." @@ -139,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" @@ -275,9 +280,9 @@ COM_MOKOJOOMBACKUP_FIELD_SFTP_PORT_DESC="SSH port (default: 22)" COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME="SSH Username" COM_MOKOJOOMBACKUP_FIELD_SFTP_USERNAME_DESC="Username for SSH authentication" COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD="SSH Password" -COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC="Password for SSH authentication. Leave blank if using a key file." +COM_MOKOJOOMBACKUP_FIELD_SFTP_PASSWORD_DESC="Password for SSH authentication." COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY="SSH Private Key" -COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload your SSH private key (id_rsa, id_ed25519). Stored base64-encoded in DB, written to temp file during upload only. Leave blank for password auth." +COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_DESC="Upload your SSH private key (id_rsa, id_ed25519). Stored base64-encoded in DB, written to temp file during upload only." COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_UPLOAD="Upload Key File" COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_REPLACE="Replace Key" COM_MOKOJOOMBACKUP_FIELD_SFTP_KEY_LOADED="Key loaded" diff --git a/source/packages/com_mokosuitebackup/mokosuitebackup.xml b/source/packages/com_mokosuitebackup/mokosuitebackup.xml index a9d7540..e01585b 100644 --- a/source/packages/com_mokosuitebackup/mokosuitebackup.xml +++ b/source/packages/com_mokosuitebackup/mokosuitebackup.xml @@ -7,7 +7,7 @@ --> MokoSuiteBackup - 01.43.00 + 01.43.35 2026-06-02 Moko Consulting hello@mokoconsulting.tech diff --git a/source/packages/com_mokosuitebackup/sql/install.mysql.sql b/source/packages/com_mokosuitebackup/sql/install.mysql.sql index 8767c04..984ff45 100644 --- a/source/packages/com_mokosuitebackup/sql/install.mysql.sql +++ b/source/packages/com_mokosuitebackup/sql/install.mysql.sql @@ -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', @@ -54,7 +55,6 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` ( `ntfy_server` VARCHAR(512) NOT NULL DEFAULT 'https://ntfy.sh' COMMENT 'ntfy server URL', `ntfy_token` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ntfy access token (optional)', `published` TINYINT(1) NOT NULL DEFAULT 1, - `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`), @@ -113,14 +113,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) @@ -128,12 +127,12 @@ INSERT IGNORE INTO `#__mokosuitebackup_profiles` ( `id`, `title`, `description`, `backup_type`, `archive_format`, `compression_level`, `split_size`, `backup_dir`, `exclude_dirs`, `exclude_files`, `exclude_tables`, - `published`, `ordering`, `created`, `modified` + `published`, `created`, `modified` ) VALUES ( 1, 'Default Backup Profile', 'Full site backup with default settings', 'full', 'zip', 5, 0, '[DEFAULT_DIR]', 'administrator/components/com_mokosuitebackup/backups\ntmp\ncache\nlogs\nadministrator/logs', '.gitignore\n.htaccess.bak', '#__session', - 1, 1, NOW(), NOW() + 1, NOW(), NOW() ); diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.11.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.11.sql new file mode 100644 index 0000000..ef3d02e --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.11.sql @@ -0,0 +1 @@ +/* 01.43.11 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.19.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.19.sql new file mode 100644 index 0000000..09ac944 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.19.sql @@ -0,0 +1 @@ +/* 01.43.19 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.20.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.20.sql new file mode 100644 index 0000000..e9eb4e2 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.20.sql @@ -0,0 +1 @@ +/* 01.43.20 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.21.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.21.sql new file mode 100644 index 0000000..63dc486 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.21.sql @@ -0,0 +1 @@ +/* 01.43.21 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.22.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.22.sql new file mode 100644 index 0000000..01f1e95 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.22.sql @@ -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`; diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.23.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.23.sql new file mode 100644 index 0000000..08a0685 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.23.sql @@ -0,0 +1 @@ +/* 01.43.23 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.24.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.24.sql new file mode 100644 index 0000000..64643df --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.24.sql @@ -0,0 +1 @@ +/* 01.43.24 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.25.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.25.sql new file mode 100644 index 0000000..6ae1419 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.25.sql @@ -0,0 +1 @@ +/* 01.43.25 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.26.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.26.sql new file mode 100644 index 0000000..6c94f71 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.26.sql @@ -0,0 +1 @@ +/* 01.43.26 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.29.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.29.sql new file mode 100644 index 0000000..c98e9ba --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.29.sql @@ -0,0 +1 @@ +/* 01.43.29 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.30.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.30.sql new file mode 100644 index 0000000..f47dac0 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.30.sql @@ -0,0 +1 @@ +/* 01.43.30 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.31.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.31.sql new file mode 100644 index 0000000..c4a66a1 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.31.sql @@ -0,0 +1 @@ +/* 01.43.31 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.32.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.32.sql new file mode 100644 index 0000000..0bec470 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.32.sql @@ -0,0 +1 @@ +/* 01.43.32 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.33.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.33.sql new file mode 100644 index 0000000..872db35 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.33.sql @@ -0,0 +1 @@ +ALTER TABLE `#__mokosuitebackup_profiles` DROP COLUMN `ordering`; diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.34.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.34.sql new file mode 100644 index 0000000..6d39086 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.34.sql @@ -0,0 +1 @@ +/* 01.43.34 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.35.sql b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.35.sql new file mode 100644 index 0000000..30b93c1 --- /dev/null +++ b/source/packages/com_mokosuitebackup/sql/updates/mysql/01.43.35.sql @@ -0,0 +1 @@ +/* 01.43.35 — no schema changes */ diff --git a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php index 41c861a..159af3b 100644 --- a/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php +++ b/source/packages/com_mokosuitebackup/src/Controller/AjaxController.php @@ -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); diff --git a/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php b/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php index 8072e1a..7020926 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php +++ b/source/packages/com_mokosuitebackup/src/Engine/AkeebaImporter.php @@ -249,7 +249,6 @@ class AkeebaImporter 'remote_keep_local' => 1, 'include_mokorestore' => (int) (($config['akeeba.advanced.embedded_installer'] ?? 'none') !== 'none'), 'published' => 1, - 'ordering' => (int) $akProfile->id, 'created' => $now, 'modified' => $now, ]; diff --git a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php index 52f0a58..5eb994c 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php +++ b/source/packages/com_mokosuitebackup/src/Engine/BackupEngine.php @@ -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']); } } diff --git a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php index f72f513..78c4b2a 100644 --- a/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php +++ b/source/packages/com_mokosuitebackup/src/Engine/MokoRestore.php @@ -35,25 +35,36 @@ class MokoRestore * * @return string Path to the wrapped archive */ - public static function wrap(string $backupArchive, string $outputPath): string + public static function wrap(string $backupArchive, string $outputPath, string $scriptName = 'restore.php'): string { + $scriptName = self::sanitizeScriptName($scriptName); + $zip = new \ZipArchive(); if ($zip->open($outputPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { throw new \RuntimeException('Cannot create MokoRestore archive: ' . $outputPath); } - // Add the standalone restore script - $zip->addFromString('restore.php', self::generateRestoreScript()); - - // Add the original backup as a nested ZIP + $zip->addFromString($scriptName, self::generateRestoreScript()); $zip->addFile($backupArchive, 'site-backup.zip'); - $zip->close(); return $outputPath; } + public static function sanitizeScriptName(string $name): string + { + $name = basename(trim($name)); + + if ($name === '' || !str_ends_with(strtolower($name), '.php')) { + $name = 'restore.php'; + } + + $name = preg_replace('/[^a-zA-Z0-9._-]/', '', $name); + + return $name ?: 'restore.php'; + } + /** * Generate the standalone restore.php script as a separate file. * @@ -165,7 +176,38 @@ SCANNER; $php ); - /* Modify the pre-checks to use getSelectedBackupFile() */ + /* Replace the backup archive check with one that scans for ZIPs + (must run BEFORE the blanket file_exists replacement below) */ + $php = str_replace( + <<<'ORIG' + $checks[] = [ + 'label' => 'Backup Archive', + 'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found', + 'ok' => file_exists(BACKUP_FILE), + 'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']), + ]; +ORIG, + <<<'REPL' + $availableBackups = scanForBackups(); + $backupCount = count($availableBackups); + $selectedFile = getSelectedBackupFile(); + if ($selectedFile && file_exists($selectedFile)) { + $archiveValue = basename($selectedFile) . ' (' . number_format(filesize($selectedFile) / 1048576, 2) . ' MB)'; + } elseif ($backupCount > 0) { + $archiveValue = $backupCount . ' ZIP file(s) found'; + } else { + $archiveValue = 'No ZIP files found'; + } + $checks[] = [ + 'label' => 'Backup Archive', + 'value' => $archiveValue, + 'ok' => $backupCount > 0, + 'hint' => 'Place one or more backup ZIP files in the same directory as ' . basename($_SERVER['SCRIPT_NAME']), + ]; +REPL + ); + + /* Modify remaining pre-checks to use getSelectedBackupFile() */ $php = str_replace( "file_exists(BACKUP_FILE)", "(getSelectedBackupFile() !== '' || file_exists(BACKUP_FILE))", @@ -174,65 +216,83 @@ SCANNER; $html = self::generateFrontend(); - /* Add backup file selector to the frontend before the extract step */ + /* Inject backup file selector into the extract step (panel2) */ $selectorHtml = <<<'SELECTOR' - - - + if (backups.length === 0) { + var alert = document.createElement('div'); + alert.style.cssText = 'padding:12px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#dc2626;'; + alert.textContent = 'No ZIP files found in this directory. Upload a backup archive first.'; + list.appendChild(alert); + } else if (backups.length === 1) { + hiddenInput.value = backups[0].name; + var found = document.createElement('div'); + found.style.cssText = 'padding:12px;background:#dcfce7;border:1px solid #bbf7d0;border-radius:6px;color:#16a34a;'; + var strong = document.createElement('strong'); + strong.textContent = backups[0].name; + found.appendChild(document.createTextNode('Found: ')); + found.appendChild(strong); + found.appendChild(document.createTextNode(' (' + (backups[0].size / 1048576).toFixed(1) + ' MB)')); + list.appendChild(found); + } else { + var hint = document.createElement('div'); + hint.style.cssText = 'padding:8px 12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;color:#1d4ed8;margin-bottom:8px;font-size:0.9em;'; + hint.textContent = 'Multiple backup archives found \u2014 select which one to restore:'; + list.appendChild(hint); + backups.forEach(function(b, i) { + var label = document.createElement('label'); + label.style.cssText = 'display:flex;align-items:center;padding:10px 12px;margin:4px 0;border:1px solid #e2e8f0;border-radius:6px;cursor:pointer;transition:background 0.15s;'; + label.onmouseover = function() { this.style.background = '#f8fafc'; }; + label.onmouseout = function() { this.style.background = ''; }; + var radio = document.createElement('input'); + radio.type = 'radio'; + radio.name = 'backup_choice'; + radio.value = b.name; + radio.style.marginRight = '10px'; + if (i === 0) { radio.checked = true; hiddenInput.value = b.name; } + radio.addEventListener('change', function() { hiddenInput.value = this.value; }); + label.appendChild(radio); + var info = document.createElement('div'); + var nameStrong = document.createElement('strong'); + nameStrong.textContent = b.name; + info.appendChild(nameStrong); + var meta = document.createElement('div'); + meta.style.cssText = 'font-size:0.85em;color:#64748b;margin-top:2px;'; + meta.textContent = (b.size / 1048576).toFixed(1) + ' MB \u2014 ' + b.date; + info.appendChild(meta); + label.appendChild(info); + list.appendChild(label); + }); + } + })(); + SELECTOR; - /* Insert the selector before the extract step in the HTML */ + /* Insert the selector into the extract panel */ $html = str_replace( - '', - $selectorHtml . "\n", + '

Extract site-backup.zip into the current directory.

', + '

Select a backup archive and extract it into the current directory.

' . "\n" . $selectorHtml, + $html + ); + + /* Pass selected backup file to the extract action */ + $html = str_replace( + "const r = await post('extract', pw ? { archive_password: pw } : {});", + "var extraParams = {};\n" . + " if (pw) extraParams.archive_password = pw;\n" . + " var sel = document.getElementById('mr-backup-file');\n" . + " if (sel && sel.value) extraParams.backup_file = sel.value;\n" . + " const r = await post('extract', extraParams);", $html ); @@ -435,7 +495,7 @@ function actionPreflight(): array 'label' => 'Backup Archive', 'value' => file_exists(BACKUP_FILE) ? number_format(filesize(BACKUP_FILE) / 1048576, 2) . ' MB' : 'Not found', 'ok' => file_exists(BACKUP_FILE), - 'hint' => 'site-backup.zip must be in the same directory as restore.php', + 'hint' => 'site-backup.zip must be in the same directory as ' . basename($_SERVER['SCRIPT_NAME']), ]; $checks[] = [ @@ -462,15 +522,31 @@ function actionPreflight(): array 'hint' => 'Informational', ]; + $joomlaExists = file_exists(RESTORE_DIR . '/configuration.php') + || file_exists(RESTORE_DIR . '/libraries/src/Version.php'); + $checks[] = [ + 'label' => 'Existing Installation', + 'value' => $joomlaExists ? 'Joomla detected' : 'Clean directory', + 'ok' => true, + 'warn' => $joomlaExists, + 'hint' => $joomlaExists + ? 'WARNING: A Joomla installation already exists in this directory. Restoring will overwrite it.' + : 'No existing installation found — safe to proceed', + ]; + $allOk = true; + $warnings = []; foreach ($checks as $c) { if (!$c['ok']) { $allOk = false; } + if (!empty($c['warn'])) { + $warnings[] = $c['hint']; + } } - return ['success' => $allOk, 'checks' => $checks]; + return ['success' => $allOk, 'checks' => $checks, 'warnings' => $warnings]; } function actionExtract(array $data): array @@ -1425,6 +1501,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N .mr-checks li:last-child{border-bottom:none} .mr-check-icon{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0} .mr-check-ok{background:#dcfce7;color:#16a34a} +.mr-check-warn{background:#fef9c3;color:#a16207} .mr-check-fail{background:#fef2f2;color:#dc2626} .mr-check-info{background:#e0f2fe;color:#0284c7} .mr-check-label{flex:1;font-weight:500} @@ -1474,7 +1551,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
- Security: Delete restore.php immediately after installation is complete. + Security: Delete immediately after installation is complete.
@@ -1722,7 +1799,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N Success! The site restoration is complete.
- Important: Delete restore.php and site-backup.zip from your server immediately for security. + Important: Delete and site-backup.zip from your server immediately for security.
@@ -1746,6 +1823,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N -