12 Commits

Author SHA1 Message Date
gitea-actions[bot] b4aa3ab0f9 chore(version): pre-release bump to 01.08.05-dev [skip ci] 2026-06-11 19:23:10 +00:00
Jonathan Miller c401608222 fix: resolve merge conflicts, keep pretty names at 01.08.04-dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
2026-06-11 14:22:51 -05:00
Jonathan Miller 5c697b9eeb fix: pretty names for all sub-extensions, default backup dir to ./backups
- Component: MokoSuiteBackup
- Plugins: System/Task/Console/Content/QuickIcon/ActionLog/WebServices - MokoSuiteBackup
- Default backup_dir changed to ./backups (relative to site root)
- Auto-migrate old defaults on upgrade
2026-06-11 12:48:11 -05:00
gitea-actions[bot] fe71aff5a3 chore(version): pre-release bump to 01.08.04-dev [skip ci] 2026-06-11 17:40:40 +00:00
Jonathan Miller 9a20f23c12 Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Successful in 8s
2026-06-11 12:40:24 -05:00
Jonathan Miller 49f3d9fdcf feat: change default backup dir to ../backups (outside web root)
- Default backup_dir is now ../backups (relative to JPATH_ROOT),
  which resolves outside public_html on most hosting setups
- Added BackupDirectory::normalizePath() to resolve ../ segments
  without requiring the path to exist on disk
- Added BackupDirectory::portablize() to auto-detect absolute paths
  and replace them with portable placeholders ([HOME], ../backups)
- ProfileTable::check() auto-normalizes backup_dir on save
- Install postflight auto-migrates old in-webroot defaults to ../backups
- Dashboard warning now checks resolved path instead of string matching
- .htaccess protection only applied when directory is inside web root
2026-06-11 12:39:59 -05:00
gitea-actions[bot] 1badb45c74 chore(version): pre-release bump to 01.08.03-dev [skip ci] 2026-06-11 17:31:11 +00:00
Jonathan Miller 8d78cae60b Merge branch 'dev' of https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: Auto Version Bump / Version Bump (push) Successful in 15s
2026-06-11 12:30:48 -05:00
Jonathan Miller e8b40d9389 fix: resolve relative paths in folder browser, add placeholders help modal
- browseDir() now resolves paths via BackupDirectory::resolve() before
  permission check, fixing "access denied" when browsing from [DEFAULT_DIR]
- SQL default for backup_dir changed to [DEFAULT_DIR] placeholder
- Added help button (?) next to Browse that opens a modal listing all
  available placeholders with examples and recommended paths
2026-06-11 12:30:23 -05:00
gitea-actions[bot] 925ad07d7c chore(version): pre-release bump to 01.08.02-dev [skip ci] 2026-06-11 17:25:04 +00:00
Jonathan Miller ace33b60fe feat: rename mokojoombackup → mokosuitebackup, add [HOME] placeholder for backup directory
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Successful in 10s
Renames all sub-extensions from mokojoombackup to mokosuitebackup
(package, component, 7 plugins, language files, manifests).

Adds [HOME] placeholder to BackupDirectory and PlaceholderResolver
so users can set backup_dir to [HOME]/backups (outside web root).
Fixes folder browser "access denied" on PHP-FPM shared hosting
where getenv('HOME') returns empty by adding POSIX and JPATH_ROOT
fallback detection.
2026-06-11 12:24:27 -05:00
gitea-actions[bot] 8bcd5c058c chore(version): pre-release bump to 01.08.01-dev [skip ci] 2026-06-10 05:42:14 +00:00
231 changed files with 1281 additions and 1058 deletions
+16 -16
View File
@@ -1,4 +1,4 @@
# MokoJoomBackup # MokoSuiteBackup
Full-site backup and restore for Joomla — database, files, and configuration. Replaces Akeeba Backup Pro. Full-site backup and restore for Joomla — database, files, and configuration. Replaces Akeeba Backup Pro.
@@ -6,10 +6,10 @@ Full-site backup and restore for Joomla — database, files, and configuration.
| Field | Value | | Field | Value |
|---|---| |---|---|
| **Package** | `pkg_mokojoombackup` | | **Package** | `pkg_mokosuitebackup` |
| **Language** | PHP 8.1+ | | **Language** | PHP 8.1+ |
| **Branch** | develop on `dev`, merge to `main` (protected) | | **Branch** | develop on `dev`, merge to `main` (protected) |
| **Wiki** | [MokoJoomBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/wiki) | | **Wiki** | [MokoSuiteBackup Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/wiki) |
## Commands ## Commands
@@ -26,32 +26,32 @@ composer install # Install PHP dependencies
Joomla **package** with four sub-extensions: Joomla **package** with four sub-extensions:
### com_mokojoombackup (Component) ### com_mokosuitebackup (Component)
- Admin backend for managing backup profiles and records - Admin backend for managing backup profiles and records
- Backup engine: `Engine/BackupEngine`, `Engine/DatabaseDumper`, `Engine/FileScanner`, `Engine/Archiver` - Backup engine: `Engine/BackupEngine`, `Engine/DatabaseDumper`, `Engine/FileScanner`, `Engine/Archiver`
- Joomla 4/5 MVC: Controllers, Models, Views, Tables - Joomla 4/5 MVC: Controllers, Models, Views, Tables
- Namespace: `Joomla\Component\MokoJoomBackup\Administrator` - Namespace: `Joomla\Component\MokoSuiteBackup\Administrator`
- DB tables: `#__mokojoombackup_profiles`, `#__mokojoombackup_records` - DB tables: `#__mokosuitebackup_profiles`, `#__mokosuitebackup_records`
- CLI: `cli/mokojoombackup.php` for cron-based backups - CLI: `cli/mokosuitebackup.php` for cron-based backups
### plg_system_mokojoombackup (System Plugin) ### plg_system_mokosuitebackup (System Plugin)
- Cleanup of expired backup archives (age + count limits) - Cleanup of expired backup archives (age + count limits)
- Namespace: `Joomla\Plugin\System\MokoJoomBackup` - Namespace: `Joomla\Plugin\System\MokoSuiteBackup`
### plg_task_mokojoombackup (Task Plugin) ### plg_task_mokosuitebackup (Task Plugin)
- Integrates with Joomla's Scheduled Tasks (com_scheduler) - Integrates with Joomla's Scheduled Tasks (com_scheduler)
- Registers "Run Backup Profile" task type - Registers "Run Backup Profile" task type
- Namespace: `Joomla\Plugin\Task\MokoJoomBackup` - Namespace: `Joomla\Plugin\Task\MokoSuiteBackup`
### plg_webservices_mokojoombackup (WebServices Plugin) ### plg_webservices_mokosuitebackup (WebServices Plugin)
- REST API for remote backup management (wire-compatible with mcp_mokojoombackup) - REST API for remote backup management (wire-compatible with mcp_mokosuitebackup)
- Endpoints: backup, backups, profiles, download, delete - Endpoints: backup, backups, profiles, download, delete
- Namespace: `Joomla\Plugin\WebServices\MokoJoomBackup` - Namespace: `Joomla\Plugin\WebServices\MokoSuiteBackup`
### Database Schema ### Database Schema
- `#__mokojoombackup_profiles` — backup profiles (name, description, config JSON, filters JSON) - `#__mokosuitebackup_profiles` — backup profiles (name, description, config JSON, filters JSON)
- `#__mokojoombackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps) - `#__mokosuitebackup_records` — backup records (profile_id, status, origin, archive path, sizes, timestamps)
## Rules ## Rules
+3 -3
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0"> <mokoplatform xmlns="https://standards.mokoconsulting.tech/mokoplatform/1.0" schema-version="1.0">
<identity> <identity>
<name>MokoJoomBackup</name> <name>MokoSuiteBackup</name>
<display-name>Package - MokoJoomBackup</display-name> <display-name>Package - MokoSuiteBackup</display-name>
<org>MokoConsulting</org> <org>MokoConsulting</org>
<description>Full-site backup and restore for Joomla — database, files, and configuration</description> <description>Full-site backup and restore for Joomla — database, files, and configuration</description>
<version>01.08.00-dev</version> <version>01.08.05-dev</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license> <license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity> </identity>
<governance> <governance>
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation # INGROUP: mokoplatform.Automation
# VERSION: 01.08.00 # VERSION: 01.08.05
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+66
View File
@@ -0,0 +1,66 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoPlatform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
name: "RC Revert"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
revert:
name: Rename rc/ back to dev/
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- name: Rename branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
SUFFIX="${BRANCH#rc/}"
DEV_BRANCH="dev/${SUFFIX}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Create dev/ branch from rc/ branch
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
"${API}" 2>/dev/null || true)
if [ "$STATUS" = "201" ]; then
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
exit 1
fi
# Delete rc/ branch
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${TOKEN}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
fi
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
+2 -2
View File
@@ -12,13 +12,13 @@
### Added ### Added
- Dashboard submenu entry as default landing page with `class:home` icon - Dashboard submenu entry as default landing page with `class:home` icon
- `[DEFAULT_DIR]` placeholder for portable backup directory configuration — resolves to `administrator/components/com_mokojoombackup/backups` at runtime - `[DEFAULT_DIR]` placeholder for portable backup directory configuration — resolves to `administrator/components/com_mokosuitebackup/backups` at runtime
- Live AJAX directory validation on backup_dir field — checks existence, writability, and placeholder resolution as user types (debounced 400ms) - Live AJAX directory validation on backup_dir field — checks existence, writability, and placeholder resolution as user types (debounced 400ms)
- `checkDir` AJAX endpoint for real-time directory permission checking - `checkDir` AJAX endpoint for real-time directory permission checking
- Web-accessible warning badge on backup download buttons when archive is inside web root - Web-accessible warning badge on backup download buttons when archive is inside web root
- Inline security warning in FolderPicker when default directory is selected - Inline security warning in FolderPicker when default directory is selected
- Auto `.htaccess` and `index.html` protection for web-accessible backup directories on profile save and at backup time - Auto `.htaccess` and `index.html` protection for web-accessible backup directories on profile save and at backup time
- Font Awesome 6 submenu icons via CSS injection in `MokoJoomBackupComponent::boot()` - Font Awesome 6 submenu icons via CSS injection in `MokoSuiteBackupComponent::boot()`
- `syncMenuIcons()` installer postflight — syncs icon classes to `#__menu` on install and update - `syncMenuIcons()` installer postflight — syncs icon classes to `#__menu` on install and update
- `encryptionPassword` property on `SteppedSession` for upcoming stepped backup encryption support - `encryptionPassword` property on `SteppedSession` for upcoming stepped backup encryption support
+3 -3
View File
@@ -1,6 +1,6 @@
# Contributing to MokoJoomBackup # Contributing to MokoSuiteBackup
Thank you for your interest in contributing to MokoJoomBackup. Thank you for your interest in contributing to MokoSuiteBackup.
## Getting Started ## Getting Started
@@ -27,7 +27,7 @@ Thank you for your interest in contributing to MokoJoomBackup.
## Reporting Issues ## Reporting Issues
Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/issues). Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/issues).
## License ## License
+5 -5
View File
@@ -2,7 +2,7 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# MokoJoomBackup — Full-site backup and restore for Joomla # MokoSuiteBackup — Full-site backup and restore for Joomla
# #
# Builds and releases are handled by CI workflows (pre-release.yml, # Builds and releases are handled by CI workflows (pre-release.yml,
# auto-release.yml). This Makefile provides local validation helpers # auto-release.yml). This Makefile provides local validation helpers
@@ -12,7 +12,7 @@
# CONFIGURATION # CONFIGURATION
# ============================================================================== # ==============================================================================
EXTENSION_NAME := mokojoombackup EXTENSION_NAME := mokosuitebackup
EXTENSION_TYPE := package EXTENSION_TYPE := package
SRC_DIR := source SRC_DIR := source
@@ -20,7 +20,7 @@ SRC_DIR := source
# Gitea # Gitea
GITEA_URL := https://git.mokoconsulting.tech GITEA_URL := https://git.mokoconsulting.tech
GITEA_ORG := MokoConsulting GITEA_ORG := MokoConsulting
GITEA_REPO := MokoJoomBackup GITEA_REPO := MokoSuiteBackup
# Tools # Tools
PHP := php PHP := php
@@ -44,7 +44,7 @@ COLOR_RED := \033[31m
.PHONY: help .PHONY: help
help: ## Show this help message help: ## Show this help message
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)" @echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
@echo "$(COLOR_BLUE)║ MokoJoomBackup Makefile ║$(COLOR_RESET)" @echo "$(COLOR_BLUE)║ MokoSuiteBackup Makefile ║$(COLOR_RESET)"
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)" @echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
@echo "" @echo ""
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)" @echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
@@ -158,7 +158,7 @@ release-rc: validate validate-xml ## Trigger release-candidate build via CI work
.PHONY: version .PHONY: version
version: ## Display version from package manifest version: ## Display version from package manifest
@VERSION=$$(grep '<version>' $(SRC_DIR)/pkg_mokojoombackup.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/'); \ @VERSION=$$(grep '<version>' $(SRC_DIR)/pkg_mokosuitebackup.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/'); \
echo "$(COLOR_BLUE)$(EXTENSION_NAME)$(COLOR_RESET) v$$VERSION ($(EXTENSION_TYPE))" echo "$(COLOR_BLUE)$(EXTENSION_NAME)$(COLOR_RESET) v$$VERSION ($(EXTENSION_TYPE))"
# Default target # Default target
+5 -5
View File
@@ -1,12 +1,12 @@
# MokoJoomBackup # MokoSuiteBackup
<!-- VERSION: 01.08.00 --> <!-- VERSION: 01.08.05 -->
Full-site backup and restore for Joomla — database, files, and configuration. Full-site backup and restore for Joomla — database, files, and configuration.
## Overview ## Overview
MokoJoomBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It creates complete site backups including the database, files, and configuration, packaged into downloadable ZIP archives. Supports multiple backup profiles, scheduled backups via CLI/cron, and a REST API for remote management. MokoSuiteBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It creates complete site backups including the database, files, and configuration, packaged into downloadable ZIP archives. Supports multiple backup profiles, scheduled backups via CLI/cron, and a REST API for remote management.
## Features ## Features
@@ -25,13 +25,13 @@ MokoJoomBackup is a comprehensive backup solution for Joomla 4/5/6 sites. It cre
## Installation ## Installation
1. Download `pkg_mokobackup-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomBackup/releases) 1. Download `pkg_mokobackup-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteBackup/releases)
2. Joomla Administrator > Extensions > Install 2. Joomla Administrator > Extensions > Install
3. System plugin enabled automatically on install 3. System plugin enabled automatically on install
## Configuration ## Configuration
- **Component**: Administrator > Components > MokoJoomBackup - **Component**: Administrator > Components > MokoSuiteBackup
- **Profiles**: Create backup profiles with different file/database filters - **Profiles**: Create backup profiles with different file/database filters
- **System Plugin**: Configure scheduled backup triggers and notifications - **System Plugin**: Configure scheduled backup triggers and notifications
- **CLI**: `php cli/mokobackup.php --profile=1` for cron-based backups - **CLI**: `php cli/mokobackup.php --profile=1` for cron-based backups
@@ -1,10 +0,0 @@
; MokoJoomBackup — Package language file (en-GB)
; @package MokoJoomBackup
; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later
PKG_MOKOJOOMBACKUP="Package - MokoJoomBackup"
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
@@ -0,0 +1,10 @@
; MokoSuiteBackup — Package language file (en-GB)
; @package MokoSuiteBackup
; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later
PKG_MOKOJOOMBACKUP="Package - MokoSuiteBackup"
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoSuiteBackup requires PHP %s or later."
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
@@ -1,10 +0,0 @@
; MokoJoomBackup — Package language file (en-US)
; @package MokoJoomBackup
; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later
PKG_MOKOJOOMBACKUP="Package - MokoJoomBackup"
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoJoomBackup requires PHP %s or later."
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
@@ -0,0 +1,10 @@
; MokoSuiteBackup — Package language file (en-US)
; @package MokoSuiteBackup
; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later
PKG_MOKOJOOMBACKUP="Package - MokoSuiteBackup"
PKG_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration. Includes admin component, system plugin, and REST API."
PKG_MOKOJOOMBACKUP_PHP_VERSION_ERROR="MokoSuiteBackup requires PHP %s or later."
PKG_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
@@ -1,2 +0,0 @@
DROP TABLE IF EXISTS `#__mokojoombackup_records`;
DROP TABLE IF EXISTS `#__mokojoombackup_profiles`;
@@ -1 +0,0 @@
ALTER TABLE `#__mokojoombackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive';
@@ -1,12 +0,0 @@
-- MokoJoomBackup 01.01.02
-- Consolidated schema updates: NULL defaults, notifications, archive name format
-- Fix: allow NULL defaults for manifest and log columns
ALTER TABLE `#__mokojoombackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL;
ALTER TABLE `#__mokojoombackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
-- Add user group notifications column to profiles
ALTER TABLE `#__mokojoombackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
-- Add archive_name_format column with placeholder support
ALTER TABLE `#__mokojoombackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
@@ -1,45 +0,0 @@
<?php
/**
* @package MokoJoomBackup
* @subpackage com_mokojoombackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Joomla\Component\MokoJoomBackup\Administrator\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Factory;
class MokoJoomBackupComponent extends MVCComponent
{
public function boot(): void
{
parent::boot();
try {
$app = Factory::getApplication();
if (!$app->isClient('administrator')) {
return;
}
$wa = $app->getDocument()->getWebAssetManager();
$wa->addInlineStyle(
'#menu a[href*="com_mokojoombackup"][href*="view=dashboard"] .sidebar-item-title::before,'
. ' #menu a[href*="com_mokojoombackup"][href*="view=backups"] .sidebar-item-title::before,'
. ' #menu a[href*="com_mokojoombackup"][href*="view=profiles"] .sidebar-item-title::before'
. ' { font-family: "Font Awesome 6 Free"; font-weight: 900; margin-right: .5em; }'
. ' #menu a[href*="com_mokojoombackup"][href*="view=dashboard"] .sidebar-item-title::before { content: "\f015"; }'
. ' #menu a[href*="com_mokojoombackup"][href*="view=backups"] .sidebar-item-title::before { content: "\f1c0"; }'
. ' #menu a[href*="com_mokojoombackup"][href*="view=profiles"] .sidebar-item-title::before { content: "\f013"; }'
);
} catch (\Throwable $e) {
error_log('MokoJoomBackup: boot() CSS injection failed: ' . $e->getMessage());
}
}
}
@@ -1,153 +0,0 @@
<?php
/**
* @package MokoJoomBackup
* @subpackage com_mokojoombackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Joomla\Component\MokoJoomBackup\Administrator\Utility;
defined('_JEXEC') or die;
class BackupDirectory
{
public const DEFAULT_RELATIVE = 'administrator/components/com_mokojoombackup/backups';
public const PLACEHOLDER = '[DEFAULT_DIR]';
private const HTACCESS_CONTENT = <<<'HTACCESS'
# Apache 2.4+
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
# Apache 2.2
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
HTACCESS;
private const INDEX_CONTENT = '<!DOCTYPE html><title></title>';
/**
* Get the absolute default backup directory path.
*/
public static function getDefaultAbsolute(): string
{
return JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
}
/**
* Resolve a backup directory path. Replaces [DEFAULT_DIR] placeholder,
* then resolves relative paths from JPATH_ROOT.
*
* @param string $dir Raw directory value from profile
*
* @return string Absolute path (may still contain other placeholders)
*/
public static function resolve(string $dir): string
{
if ($dir === '' || $dir === self::PLACEHOLDER) {
$dir = self::getDefaultAbsolute();
} else {
$dir = str_replace(self::PLACEHOLDER, self::getDefaultAbsolute(), $dir);
}
if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
return rtrim($dir, '/\\');
}
return JPATH_ROOT . '/' . $dir;
}
/**
* Check whether a resolved path still contains unresolved placeholders.
*/
public static function hasPlaceholders(string $path): bool
{
return (bool) preg_match('/\[.+\]/', $path);
}
/**
* Check whether a resolved absolute path is inside the web root.
*/
public static function isWebAccessible(string $absolutePath): bool
{
$jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
$realDir = realpath($absolutePath) ?: $absolutePath;
return strpos($realDir, $jRoot) === 0;
}
/**
* Create .htaccess and index.html protection files in a directory.
* Only creates files if they don't already exist.
*/
public static function protect(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$htaccess = $dir . '/.htaccess';
if (!is_file($htaccess)) {
if (@file_put_contents($htaccess, self::HTACCESS_CONTENT . "\n") === false) {
error_log('MokoJoomBackup: Could not create .htaccess in: ' . $dir);
}
}
$index = $dir . '/index.html';
if (!is_file($index)) {
if (@file_put_contents($index, self::INDEX_CONTENT) === false) {
error_log('MokoJoomBackup: Could not create index.html in: ' . $dir);
}
}
}
/**
* Ensure the backup directory exists, create it if needed,
* and apply web protection if it's inside the web root.
*
* @return bool True if directory exists and is usable
*/
public static function ensureReady(string $dir): bool
{
if (!is_dir($dir)) {
if (!@mkdir($dir, 0755, true)) {
return false;
}
}
self::protect($dir);
return true;
}
/**
* Parse a newline-separated text field into an array of trimmed, non-empty strings.
*/
public static function parseNewlineList(string $text): array
{
if (empty($text)) {
return [];
}
return array_values(array_filter(
array_map('trim', explode("\n", str_replace("\r", '', $text))),
fn($line) => $line !== ''
));
}
/**
* Derive the log file path from an archive path.
*/
public static function logPathFromArchive(string $archivePath): string
{
return preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
}
}
@@ -1,19 +1,19 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Api\Controller; namespace Joomla\Component\MokoSuiteBackup\Api\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\ApiController; use Joomla\CMS\MVC\Controller\ApiController;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
class BackupsController extends ApiController class BackupsController extends ApiController
{ {
@@ -21,7 +21,7 @@ class BackupsController extends ApiController
protected $default_view = 'backups'; protected $default_view = 'backups';
/** /**
* Start a new backup (POST /api/index.php/v1/mokojoombackup/backup) * Start a new backup (POST /api/index.php/v1/mokosuitebackup/backup)
*/ */
public function backup(): static public function backup(): static
{ {
@@ -47,7 +47,7 @@ class BackupsController extends ApiController
} }
/** /**
* Download a backup archive (GET /api/index.php/v1/mokojoombackup/backup/:id/download) * Download a backup archive (GET /api/index.php/v1/mokosuitebackup/backup/:id/download)
*/ */
public function download(): static public function download(): static
{ {
@@ -74,7 +74,7 @@ class BackupsController extends ApiController
} }
/** /**
* List backup profiles (GET /api/index.php/v1/mokojoombackup/profiles) * List backup profiles (GET /api/index.php/v1/mokosuitebackup/profiles)
*/ */
public function profiles(): static public function profiles(): static
{ {
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Api\View\Backups; namespace Joomla\Component\MokoSuiteBackup\Api\View\Backups;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -10,7 +10,7 @@
* CLI backup script for cron/scheduled use. * CLI backup script for cron/scheduled use.
* *
* Usage: * Usage:
* php cli/mokojoombackup.php --profile=1 --description="Scheduled backup" * php cli/mokosuitebackup.php --profile=1 --description="Scheduled backup"
* *
* Must be run from the Joomla root directory. * Must be run from the Joomla root directory.
*/ */
@@ -30,7 +30,7 @@ if (!defined('JPATH_BASE')) {
require_once JPATH_BASE . '/includes/framework.php'; require_once JPATH_BASE . '/includes/framework.php';
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
// Parse CLI arguments // Parse CLI arguments
$profileId = 1; $profileId = 1;
@@ -51,7 +51,7 @@ if (empty($description)) {
// Boot the application // Boot the application
$app = Factory::getApplication('administrator'); $app = Factory::getApplication('administrator');
echo "MokoJoomBackup CLI\n"; echo "MokoSuiteBackup CLI\n";
echo "Profile: {$profileId}\n"; echo "Profile: {$profileId}\n";
echo "Description: {$description}\n"; echo "Description: {$description}\n";
echo "Starting backup...\n\n"; echo "Starting backup...\n\n";
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -13,15 +13,15 @@
type="FolderPicker" type="FolderPicker"
label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR" label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR"
description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC" description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC"
default="administrator/components/com_mokojoombackup/backups" default="administrator/components/com_mokosuitebackup/backups"
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field" addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
/> />
<field <field
name="default_profile" name="default_profile"
type="sql" type="sql"
label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE" label="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE"
description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC" description="COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_PROFILE_DESC"
query="SELECT id AS value, title AS text FROM #__mokojoombackup_profiles WHERE published = 1 ORDER BY ordering ASC" query="SELECT id AS value, title AS text FROM #__mokosuitebackup_profiles WHERE published = 1 ORDER BY ordering ASC"
default="1" default="1"
> >
<option value="1">Default Backup Profile</option> <option value="1">Default Backup Profile</option>
@@ -133,7 +133,7 @@
label="JCONFIG_PERMISSIONS_LABEL" label="JCONFIG_PERMISSIONS_LABEL"
filter="rules" filter="rules"
validate="rules" validate="rules"
component="com_mokojoombackup" component="com_mokosuitebackup"
section="component" section="component"
/> />
</fieldset> </fieldset>
@@ -67,8 +67,8 @@
type="FolderPicker" type="FolderPicker"
label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR" label="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR"
description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC" description="COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC"
default="[DEFAULT_DIR]" default="./backups"
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field" addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
/> />
<field <field
name="archive_name_format" name="archive_name_format"
@@ -129,7 +129,7 @@
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DIRS_DESC" description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_DIRS_DESC"
filter="raw" filter="raw"
hint="tmp" hint="tmp"
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field" addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
/> />
<field <field
name="exclude_files" name="exclude_files"
@@ -138,7 +138,7 @@
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_FILES_DESC" description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_FILES_DESC"
filter="raw" filter="raw"
hint="*.bak" hint="*.bak"
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field" addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
/> />
<field <field
name="exclude_tables" name="exclude_tables"
@@ -146,7 +146,7 @@
label="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES" label="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES"
description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_DESC" description="COM_MOKOJOOMBACKUP_FIELD_EXCLUDE_TABLES_DESC"
filter="raw" filter="raw"
addfieldprefix="Joomla\Component\MokoJoomBackup\Administrator\Field" addfieldprefix="Joomla\Component\MokoSuiteBackup\Administrator\Field"
/> />
</fieldset> </fieldset>
@@ -1,10 +1,10 @@
; MokoJoomBackup — Component language file (en-GB) ; MokoSuiteBackup — Component language file (en-GB)
; @package MokoJoomBackup ; @package MokoSuiteBackup
; @author Moko Consulting <hello@mokoconsulting.tech> ; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. ; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later ; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoJoomBackup" COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla" COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
; Submenu ; Submenu
@@ -13,7 +13,7 @@ COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles" COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
; Dashboard view ; Dashboard view
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard" COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup" COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet" COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled" COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
@@ -94,7 +94,7 @@ COM_MOKOJOOMBACKUP_FIELD_ENCRYPTION_PASSWORD_DESC="Set a password to encrypt the
COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE="Split Size (MB)" COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE="Split Size (MB)"
COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE_DESC="Split archive into parts of this size in MB. 0 = no splitting." COM_MOKOJOOMBACKUP_FIELD_SPLIT_SIZE_DESC="Split archive into parts of this size in MB. 0 = no splitting."
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR="Backup Directory" COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR="Backup Directory"
COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Directory where backup archives are stored. Supports placeholders: [host], [date], [year], [month], [day], [profile_name], [site_name], [type]. Absolute paths (starting with /) are used as-is; relative paths resolve from the Joomla root." COM_MOKOJOOMBACKUP_FIELD_BACKUP_DIR_DESC="Directory where backup archives are stored. Supports placeholders: [HOME] (user home directory), [host], [date], [year], [month], [day], [profile_name], [site_name], [type]. Use [HOME]/backups to store outside the web root. Absolute paths (starting with /) are used as-is; relative paths resolve from the Joomla root."
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT="Archive Name Format" COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT="Archive Name Format"
COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC="Filename template for backup archives (without extension). Placeholders: [host] hostname, [date] Ymd, [time] His, [datetime] Ymd_His, [year] [month] [day] [hour] [minute] [second], [profile_id], [profile_name], [site_name], [type], [random]." COM_MOKOJOOMBACKUP_FIELD_ARCHIVE_NAME_FORMAT_DESC="Filename template for backup archives (without extension). Placeholders: [host] hostname, [date] Ymd, [time] His, [datetime] Ymd_His, [year] [month] [day] [hour] [minute] [second], [profile_id], [profile_name], [site_name], [type], [random]."
COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE="Include Restore Script" COM_MOKOJOOMBACKUP_FIELD_INCLUDE_MOKORESTORE="Include Restore Script"
@@ -216,8 +216,8 @@ COM_MOKOJOOMBACKUP_AKEEBA_NOT_FOUND="Akeeba Backup tables not found. Is Akeeba B
; Update site notice ; Update site notice
COM_MOKOJOOMBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key." COM_MOKOJOOMBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key."
COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server." COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoSuiteBackup update site not found. Reinstall the package to register the update server."
COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates." COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
; Component Options (config.xml) ; Component Options (config.xml)
COM_MOKOJOOMBACKUP_CONFIG_GENERAL="General" COM_MOKOJOOMBACKUP_CONFIG_GENERAL="General"
@@ -245,7 +245,7 @@ COM_MOKOJOOMBACKUP_CONFIG_WEBCRON="Web Cron"
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED="Enable Web Cron" COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED="Enable Web Cron"
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED_DESC="Allow backups to be triggered via a URL with a secret key. Use this when crontab is not available on shared hosting." COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_ENABLED_DESC="Allow backups to be triggered via a URL with a secret key. Use this when crontab is not available on shared hosting."
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET="Secret Word" COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET="Secret Word"
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET_DESC="The secret key required in the URL to trigger a backup. Use a long, random string. URL format: index.php?mokojoombackup_cron=YOUR_SECRET&profile_id=1" COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_SECRET_DESC="The secret key required in the URL to trigger a backup. Use a long, random string. URL format: index.php?mokosuitebackup_cron=YOUR_SECRET&profile_id=1"
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP="IP Whitelist" COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP="IP Whitelist"
COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP_DESC="Comma-separated list of IP addresses allowed to trigger web cron. Leave blank to allow any IP." COM_MOKOJOOMBACKUP_CONFIG_WEBCRON_IP_DESC="Comma-separated list of IP addresses allowed to trigger web cron. Leave blank to allow any IP."
@@ -1,10 +1,10 @@
; MokoJoomBackup — Component system language file (en-GB) ; MokoSuiteBackup — Component system language file (en-GB)
; @package MokoJoomBackup ; @package MokoSuiteBackup
; @author Moko Consulting <hello@mokoconsulting.tech> ; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. ; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later ; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoJoomBackup" COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration." COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard" COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
@@ -1,15 +1,15 @@
; MokoJoomBackup — Component language file (en-US) ; MokoSuiteBackup — Component language file (en-US)
; @package MokoJoomBackup ; @package MokoSuiteBackup
; @author Moko Consulting <hello@mokoconsulting.tech> ; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. ; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later ; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoJoomBackup" COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla" COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla"
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard" COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles" COM_MOKOJOOMBACKUP_SUBMENU_PROFILES="Backup Profiles"
COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoJoomBackup Dashboard" COM_MOKOJOOMBACKUP_DASHBOARD_TITLE="MokoSuiteBackup Dashboard"
COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup" COM_MOKOJOOMBACKUP_DASHBOARD_LAST_BACKUP="Last Backup"
COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet" COM_MOKOJOOMBACKUP_DASHBOARD_NO_BACKUPS="No backups yet"
COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled" COM_MOKOJOOMBACKUP_DASHBOARD_NEXT_SCHEDULED="Next Scheduled"
@@ -27,8 +27,8 @@ COM_MOKOJOOMBACKUP_TOOLBAR_BACKUP_NOW="Backup Now"
COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup." COM_MOKOJOOMBACKUP_NO_BACKUPS="No backups found. Click 'Backup Now' to create your first backup."
COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found." COM_MOKOJOOMBACKUP_NO_PROFILES="No backup profiles found."
COM_MOKOJOOMBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key." COM_MOKOJOOMBACKUP_UPDATE_SITE_NOTICE="To receive automatic updates, configure your <a href=\"%s\">Update Site</a> with your download key."
COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoJoomBackup update site not found. Reinstall the package to register the update server." COM_MOKOJOOMBACKUP_UPDATE_SITE_MISSING="MokoSuiteBackup update site not found. Reinstall the package to register the update server."
COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoJoomBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates." COM_MOKOJOOMBACKUP_POSTINSTALL_UPDATE_SITE="MokoSuiteBackup installed successfully. Configure your <a href=\"%s\">Update Site</a> to receive automatic updates."
COM_MOKOJOOMBACKUP_CONFIG_GENERAL="General" COM_MOKOJOOMBACKUP_CONFIG_GENERAL="General"
COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR="Default Backup Directory" COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR="Default Backup Directory"
COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC="Default directory for backup archives, relative to Joomla root. Can be overridden per profile." COM_MOKOJOOMBACKUP_CONFIG_DEFAULT_BACKUP_DIR_DESC="Default directory for backup archives, relative to Joomla root. Can be overridden per profile."
@@ -1,10 +1,10 @@
; MokoJoomBackup — Component system language file (en-US) ; MokoSuiteBackup — Component system language file (en-US)
; @package MokoJoomBackup ; @package MokoSuiteBackup
; @author Moko Consulting <hello@mokoconsulting.tech> ; @author Moko Consulting <hello@mokoconsulting.tech>
; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. ; @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
; @license GPL-3.0-or-later ; @license GPL-3.0-or-later
COM_MOKOJOOMBACKUP="MokoJoomBackup" COM_MOKOJOOMBACKUP="MokoSuiteBackup"
COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration." COM_MOKOJOOMBACKUP_DESCRIPTION="Full-site backup and restore for Joomla — database, files, and configuration."
COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard" COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD="Dashboard"
COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records" COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS="Backup Records"
@@ -1,14 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- <!--
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
--> -->
<extension type="component" method="upgrade"> <extension type="component" method="upgrade">
<name>com_mokojoombackup</name> <name>MokoSuiteBackup</name>
<version>01.08.00</version> <version>01.08.05-dev</version>
<creationDate>2026-06-02</creationDate> <creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -17,7 +16,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<description>COM_MOKOJOOMBACKUP_DESCRIPTION</description> <description>COM_MOKOJOOMBACKUP_DESCRIPTION</description>
<namespace path="src">Joomla\Component\MokoJoomBackup</namespace> <namespace path="src">Joomla\Component\MokoSuiteBackup</namespace>
<install> <install>
<sql> <sql>
@@ -40,9 +39,6 @@
<administration> <administration>
<menu img="class:archive">COM_MOKOJOOMBACKUP</menu> <menu img="class:archive">COM_MOKOJOOMBACKUP</menu>
<submenu> <submenu>
<menu link="option=com_mokojoombackup&amp;view=dashboard" img="class:home">COM_MOKOJOOMBACKUP_SUBMENU_DASHBOARD</menu>
<menu link="option=com_mokojoombackup&amp;view=backups" img="class:database">COM_MOKOJOOMBACKUP_SUBMENU_BACKUPS</menu>
<menu link="option=com_mokojoombackup&amp;view=profiles" img="class:cog">COM_MOKOJOOMBACKUP_SUBMENU_PROFILES</menu>
</submenu> </submenu>
<files folder="."> <files folder=".">
<folder>cli</folder> <folder>cli</folder>
@@ -53,8 +49,6 @@
<folder>tmpl</folder> <folder>tmpl</folder>
</files> </files>
<languages folder="language"> <languages folder="language">
<language tag="en-GB">en-GB/com_mokojoombackup.ini</language>
<language tag="en-GB">en-GB/com_mokojoombackup.sys.ini</language>
</languages> </languages>
</administration> </administration>
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -15,20 +15,20 @@ use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory; use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
use Joomla\CMS\Extension\Service\Provider\MVCFactory; use Joomla\CMS\Extension\Service\Provider\MVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Component\MokoJoomBackup\Administrator\Extension\MokoJoomBackupComponent; use Joomla\Component\MokoSuiteBackup\Administrator\Extension\MokoSuiteBackupComponent;
use Joomla\DI\Container; use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface; use Joomla\DI\ServiceProviderInterface;
return new class () implements ServiceProviderInterface { return new class () implements ServiceProviderInterface {
public function register(Container $container): void public function register(Container $container): void
{ {
$container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoJoomBackup')); $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\MokoSuiteBackup'));
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoJoomBackup')); $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\MokoSuiteBackup'));
$container->set( $container->set(
ComponentInterface::class, ComponentInterface::class,
function (Container $container) { function (Container $container) {
$component = new MokoJoomBackupComponent( $component = new MokoSuiteBackupComponent(
$container->get(ComponentDispatcherFactoryInterface::class) $container->get(ComponentDispatcherFactoryInterface::class)
); );
$component->setMVCFactory($container->get(MVCFactoryInterface::class)); $component->setMVCFactory($container->get(MVCFactoryInterface::class));
@@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` ( CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_profiles` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL DEFAULT '', `title` VARCHAR(255) NOT NULL DEFAULT '',
`description` TEXT NOT NULL, `description` TEXT NOT NULL,
@@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` (
`archive_format` VARCHAR(10) NOT NULL DEFAULT 'zip', `archive_format` VARCHAR(10) NOT NULL DEFAULT 'zip',
`compression_level` TINYINT(1) UNSIGNED NOT NULL DEFAULT 5 COMMENT '0=none, 9=max', `compression_level` TINYINT(1) UNSIGNED NOT NULL DEFAULT 5 COMMENT '0=none, 9=max',
`split_size` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=no split, otherwise MB per part', `split_size` INT(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '0=no split, otherwise MB per part',
`backup_dir` VARCHAR(512) NOT NULL DEFAULT 'administrator/components/com_mokojoombackup/backups', `backup_dir` VARCHAR(512) NOT NULL DEFAULT './backups',
`archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders', `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders',
`exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude', `exclude_dirs` TEXT NOT NULL COMMENT 'Newline-separated directory paths to exclude',
`exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude', `exclude_files` TEXT NOT NULL COMMENT 'Newline-separated filename patterns to exclude',
@@ -44,7 +44,7 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_profiles` (
KEY `idx_published` (`published`) KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `#__mokojoombackup_records` ( CREATE TABLE IF NOT EXISTS `#__mokosuitebackup_records` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1, `profile_id` INT(11) UNSIGNED NOT NULL DEFAULT 1,
`description` VARCHAR(255) NOT NULL DEFAULT '', `description` VARCHAR(255) NOT NULL DEFAULT '',
@@ -74,15 +74,15 @@ CREATE TABLE IF NOT EXISTS `#__mokojoombackup_records` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default backup profile (IGNORE prevents duplicate key error on update) -- Insert default backup profile (IGNORE prevents duplicate key error on update)
INSERT IGNORE INTO `#__mokojoombackup_profiles` ( INSERT IGNORE INTO `#__mokosuitebackup_profiles` (
`id`, `title`, `description`, `backup_type`, `id`, `title`, `description`, `backup_type`,
`archive_format`, `compression_level`, `split_size`, `backup_dir`, `archive_format`, `compression_level`, `split_size`, `backup_dir`,
`exclude_dirs`, `exclude_files`, `exclude_tables`, `exclude_dirs`, `exclude_files`, `exclude_tables`,
`published`, `ordering`, `created`, `modified` `published`, `ordering`, `created`, `modified`
) VALUES ( ) VALUES (
1, 'Default Backup Profile', 'Full site backup with default settings', 'full', 1, 'Default Backup Profile', 'Full site backup with default settings', 'full',
'zip', 5, 0, 'administrator/components/com_mokojoombackup/backups', 'zip', 5, 0, './backups',
'administrator/components/com_mokojoombackup/backups\ntmp\ncache\nlogs\nadministrator/logs', 'administrator/components/com_mokosuitebackup/backups\ntmp\ncache\nlogs\nadministrator/logs',
'.gitignore\n.htaccess.bak', '.gitignore\n.htaccess.bak',
'#__session', '#__session',
1, 1, NOW(), NOW() 1, 1, NOW(), NOW()
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS `#__mokosuitebackup_records`;
DROP TABLE IF EXISTS `#__mokosuitebackup_profiles`;
@@ -0,0 +1 @@
ALTER TABLE `#__mokosuitebackup_profiles` CHANGE `include_kickstart` `include_mokorestore` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Include MokoRestore standalone restore script in archive';
@@ -0,0 +1,12 @@
-- MokoSuiteBackup 01.01.02
-- Consolidated schema updates: NULL defaults, notifications, archive name format
-- Fix: allow NULL defaults for manifest and log columns
ALTER TABLE `#__mokosuitebackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL;
ALTER TABLE `#__mokosuitebackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
-- Add user group notifications column to profiles
ALTER TABLE `#__mokosuitebackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
-- Add archive_name_format column with placeholder support
ALTER TABLE `#__mokosuitebackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -11,14 +11,14 @@
* Handles init and step requests from the admin UI JavaScript. * Handles init and step requests from the admin UI JavaScript.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session; use Joomla\CMS\Session\Session;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\SteppedBackupEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\SteppedBackupEngine;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
class AjaxController extends BaseController class AjaxController extends BaseController
{ {
@@ -82,11 +82,14 @@ class AjaxController extends BaseController
} }
$requestPath = $this->input->getString('path', JPATH_ROOT); $requestPath = $this->input->getString('path', JPATH_ROOT);
$path = realpath($requestPath) ?: $requestPath;
// Resolve placeholders and relative paths before permission check
$resolved = BackupDirectory::resolve($requestPath);
$path = realpath($resolved) ?: $resolved;
// Security: restrict browsing to site root and current user's home // Security: restrict browsing to site root and current user's home
$jRoot = realpath(JPATH_ROOT); $jRoot = realpath(JPATH_ROOT);
$homeDir = getenv('HOME') ?: (getenv('USERPROFILE') ?: ''); $homeDir = BackupDirectory::getHomeDirectory();
$allowed = false; $allowed = false;
if ($jRoot !== false && strpos($path, $jRoot) === 0) { if ($jRoot !== false && strpos($path, $jRoot) === 0) {
@@ -143,7 +146,7 @@ class AjaxController extends BaseController
if ($parent !== $path) { if ($parent !== $path) {
if ($jRoot !== false && strpos($parent, $jRoot) === 0) { if ($jRoot !== false && strpos($parent, $jRoot) === 0) {
$parentAllowed = true; $parentAllowed = true;
} elseif ($homeDir !== '' && strpos($parent, $homeDir) === 0) { } elseif ($homeDir !== '' && (strpos($parent, $homeDir) === 0 || $parent === \dirname($homeDir))) {
$parentAllowed = true; $parentAllowed = true;
} }
} }
@@ -185,7 +188,7 @@ class AjaxController extends BaseController
$db = \Joomla\CMS\Factory::getDbo(); $db = \Joomla\CMS\Factory::getDbo();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName(['absolute_path', 'log'])) ->select($db->quoteName(['absolute_path', 'log']))
->from($db->quoteName('#__mokojoombackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('id') . ' = ' . (int) $id); ->where($db->quoteName('id') . ' = ' . (int) $id);
$db->setQuery($query); $db->setQuery($query);
$record = $db->loadObject(); $record = $db->loadObject();
@@ -226,7 +229,7 @@ class AjaxController extends BaseController
return; return;
} }
if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokojoombackup')) { if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) {
$this->sendJson(['error' => true, 'message' => 'Access denied']); $this->sendJson(['error' => true, 'message' => 'Access denied']);
return; return;
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,21 +1,21 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\AdminController; use Joomla\CMS\MVC\Controller\AdminController;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\BackupEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\RestoreEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\RestoreEngine;
class BackupsController extends AdminController class BackupsController extends AdminController
{ {
@@ -47,7 +47,7 @@ class BackupsController extends AdminController
$this->setMessage($result['message'], 'error'); $this->setMessage($result['message'], 'error');
} }
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
} }
/** /**
@@ -63,7 +63,7 @@ class BackupsController extends AdminController
if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) { if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) {
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error'); $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error');
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return; return;
} }
@@ -109,7 +109,7 @@ class BackupsController extends AdminController
if (!$id) { if (!$id) {
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error'); $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error');
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return; return;
} }
@@ -123,7 +123,7 @@ class BackupsController extends AdminController
$this->setMessage($result['message'], 'error'); $this->setMessage($result['message'], 'error');
} }
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
} }
/** /**
@@ -138,7 +138,7 @@ class BackupsController extends AdminController
if (!$id) { if (!$id) {
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error'); $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error');
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return; return;
} }
@@ -148,21 +148,21 @@ class BackupsController extends AdminController
if (!$item || !$item->id) { if (!$item || !$item->id) {
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error'); $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_NO_RECORD_SELECTED', 'error');
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return; return;
} }
if (!is_file($item->absolute_path)) { if (!is_file($item->absolute_path)) {
$this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error'); $this->setMessage('COM_MOKOJOOMBACKUP_ERROR_FILE_NOT_FOUND', 'error');
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return; return;
} }
if (empty($item->checksum)) { if (empty($item->checksum)) {
$this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM', 'warning'); $this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_NO_CHECKSUM', 'warning');
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
return; return;
} }
@@ -175,6 +175,6 @@ class BackupsController extends AdminController
$this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_FAILED', 'error'); $this->setMessage('COM_MOKOJOOMBACKUP_VERIFY_FAILED', 'error');
} }
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=backups', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=backups', false));
} }
} }
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,21 +1,21 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Controller; namespace Joomla\Component\MokoSuiteBackup\Administrator\Controller;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Controller\AdminController; use Joomla\CMS\MVC\Controller\AdminController;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\AkeebaImporter; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\AkeebaImporter;
class ProfilesController extends AdminController class ProfilesController extends AdminController
{ {
@@ -40,7 +40,7 @@ class ProfilesController extends AdminController
if (!$detection['profiles']) { if (!$detection['profiles']) {
$this->setMessage('COM_MOKOJOOMBACKUP_AKEEBA_NOT_FOUND', 'error'); $this->setMessage('COM_MOKOJOOMBACKUP_AKEEBA_NOT_FOUND', 'error');
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=profiles', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false));
return; return;
} }
@@ -55,7 +55,7 @@ class ProfilesController extends AdminController
$this->setMessage($result['message'], 'error'); $this->setMessage($result['message'], 'error');
} }
$this->setRedirect(Route::_('index.php?option=com_mokojoombackup&view=profiles', false)); $this->setRedirect(Route::_('index.php?option=com_mokosuitebackup&view=profiles', false));
} }
/** /**
@@ -1,16 +1,16 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
* *
* Imports Akeeba Backup Pro profiles and backup history into MokoJoomBackup. * Imports Akeeba Backup Pro profiles and backup history into MokoSuiteBackup.
* *
* Reads from #__ak_profiles and #__ak_stats, maps Akeeba's configuration * Reads from #__ak_profiles and #__ak_stats, maps Akeeba's configuration
* format to MokoJoomBackup's individual column format. * format to MokoSuiteBackup's individual column format.
* *
* Akeeba config format: * Akeeba config format:
* INI-style with dot-notation keys, e.g.: * INI-style with dot-notation keys, e.g.:
@@ -25,12 +25,12 @@
* "databases": {"include": {...}, "exclude": {...}}} * "databases": {"include": {...}, "exclude": {...}}}
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
class AkeebaImporter class AkeebaImporter
{ {
@@ -90,7 +90,7 @@ class AkeebaImporter
} }
/** /**
* Import all Akeeba profiles into MokoJoomBackup. * Import all Akeeba profiles into MokoSuiteBackup.
* *
* @param bool $importHistory Also import backup history from #__ak_stats * @param bool $importHistory Also import backup history from #__ak_stats
* *
@@ -120,7 +120,7 @@ class AkeebaImporter
$akProfiles = $db->loadObjectList(); $akProfiles = $db->loadObjectList();
$profilesImported = 0; $profilesImported = 0;
$profileIdMap = []; // akeeba_id => mokojoombackup_id $profileIdMap = []; // akeeba_id => mokosuitebackup_id
foreach ($akProfiles as $akProfile) { foreach ($akProfiles as $akProfile) {
$config = $this->parseAkeebaConfig($akProfile->configuration ?? ''); $config = $this->parseAkeebaConfig($akProfile->configuration ?? '');
@@ -128,11 +128,11 @@ class AkeebaImporter
$mokoProfile = $this->mapToMokoProfile($akProfile, $config, $filters); $mokoProfile = $this->mapToMokoProfile($akProfile, $config, $filters);
$db->insertObject('#__mokojoombackup_profiles', $mokoProfile, 'id'); $db->insertObject('#__mokosuitebackup_profiles', $mokoProfile, 'id');
$profileIdMap[$akProfile->id] = $mokoProfile->id; $profileIdMap[$akProfile->id] = $mokoProfile->id;
$profilesImported++; $profilesImported++;
$this->log('Imported profile: "' . $akProfile->description . '" (Akeeba #' . $akProfile->id . ' → MokoJoomBackup #' . $mokoProfile->id . ')'); $this->log('Imported profile: "' . $akProfile->description . '" (Akeeba #' . $akProfile->id . ' → MokoSuiteBackup #' . $mokoProfile->id . ')');
} }
// Import backup history // Import backup history
@@ -201,7 +201,7 @@ class AkeebaImporter
'log' => 'Imported from Akeeba Backup record #' . $stat->id, 'log' => 'Imported from Akeeba Backup record #' . $stat->id,
]; ];
$db->insertObject('#__mokojoombackup_records', $record, 'id'); $db->insertObject('#__mokosuitebackup_records', $record, 'id');
$imported++; $imported++;
} }
@@ -211,7 +211,7 @@ class AkeebaImporter
} }
/** /**
* Map an Akeeba profile to a MokoJoomBackup profile object. * Map an Akeeba profile to a MokoSuiteBackup profile object.
*/ */
private function mapToMokoProfile(object $akProfile, array $config, array $filters): object private function mapToMokoProfile(object $akProfile, array $config, array $filters): object
{ {
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,19 +1,19 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
use Joomla\Event\Event; use Joomla\Event\Event;
class BackupEngine class BackupEngine
@@ -47,7 +47,7 @@ class BackupEngine
// Load profile // Load profile
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokojoombackup_profiles')) ->from($db->quoteName('#__mokosuitebackup_profiles'))
->where($db->quoteName('id') . ' = ' . $profileId); ->where($db->quoteName('id') . ' = ' . $profileId);
$db->setQuery($query); $db->setQuery($query);
$profile = $db->loadObject(); $profile = $db->loadObject();
@@ -105,7 +105,7 @@ class BackupEngine
'log' => '', 'log' => '',
]; ];
$db->insertObject('#__mokojoombackup_records', $record, 'id'); $db->insertObject('#__mokosuitebackup_records', $record, 'id');
$recordId = $record->id; $recordId = $record->id;
try { try {
@@ -249,7 +249,7 @@ class BackupEngine
$logContent = implode("\n", $this->log); $logContent = implode("\n", $this->log);
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath); $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
if (@file_put_contents($logPath, $logContent) === false) { if (@file_put_contents($logPath, $logContent) === false) {
error_log('MokoJoomBackup: Could not write log file: ' . $logPath); error_log('MokoSuiteBackup: Could not write log file: ' . $logPath);
} }
// Final record update // Final record update
@@ -268,7 +268,7 @@ class BackupEngine
'log' => $logContent, 'log' => $logContent,
]; ];
$db->updateObject('#__mokojoombackup_records', $update, 'id'); $db->updateObject('#__mokosuitebackup_records', $update, 'id');
// Send success notification // Send success notification
NotificationSender::send($profile, $update, true, implode("\n", $this->log)); NotificationSender::send($profile, $update, true, implode("\n", $this->log));
@@ -296,7 +296,7 @@ class BackupEngine
'log' => implode("\n", $this->log), 'log' => implode("\n", $this->log),
]; ];
$db->updateObject('#__mokojoombackup_records', $update, 'id'); $db->updateObject('#__mokosuitebackup_records', $update, 'id');
// Send failure notification // Send failure notification
NotificationSender::send($profile, $update, false, implode("\n", $this->log)); NotificationSender::send($profile, $update, false, implode("\n", $this->log));
@@ -416,7 +416,7 @@ class BackupEngine
{ {
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName('manifest')) ->select($db->quoteName('manifest'))
->from($db->quoteName('#__mokojoombackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('profile_id') . ' = ' . $profileId) ->where($db->quoteName('profile_id') . ' = ' . $profileId)
->where($db->quoteName('status') . ' = ' . $db->quote('complete')) ->where($db->quoteName('status') . ' = ' . $db->quote('complete'))
->where($db->quoteName('manifest') . ' != ' . $db->quote('')) ->where($db->quoteName('manifest') . ' != ' . $db->quote(''))
@@ -472,14 +472,14 @@ class BackupEngine
} }
/** /**
* Dispatch the onMokoJoomBackupAfterRun event so plugins (actionlog, etc.) can react. * Dispatch the onMokoSuiteBackupAfterRun event so plugins (actionlog, etc.) can react.
*/ */
private function dispatchAfterRun(bool $success, int $recordId, string $description, int $profileId, string $origin): void private function dispatchAfterRun(bool $success, int $recordId, string $description, int $profileId, string $origin): void
{ {
try { try {
$app = Factory::getApplication(); $app = Factory::getApplication();
$event = new Event('onMokoJoomBackupAfterRun', [ $event = new Event('onMokoSuiteBackupAfterRun', [
'success' => $success, 'success' => $success,
'record_id' => $recordId, 'record_id' => $recordId,
'description' => $description, 'description' => $description,
@@ -487,10 +487,10 @@ class BackupEngine
'origin' => $origin, 'origin' => $origin,
]); ]);
$app->getDispatcher()->dispatch('onMokoJoomBackupAfterRun', $event); $app->getDispatcher()->dispatch('onMokoSuiteBackupAfterRun', $event);
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Never let a listener failure break the backup result, but log it // Never let a listener failure break the backup result, but log it
error_log('MokoJoomBackup: onAfterRun listener error: ' . $e->getMessage()); error_log('MokoSuiteBackup: onAfterRun listener error: ' . $e->getMessage());
} }
} }
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -56,7 +56,7 @@ class DatabaseDumper
$prefix = $db->getPrefix(); $prefix = $db->getPrefix();
$output = []; $output = [];
$output[] = '-- MokoJoomBackup Database Dump'; $output[] = '-- MokoSuiteBackup Database Dump';
$output[] = '-- Generated: ' . date('Y-m-d H:i:s'); $output[] = '-- Generated: ' . date('Y-m-d H:i:s');
$output[] = '-- Server: ' . $db->getServerType(); $output[] = '-- Server: ' . $db->getServerType();
$output[] = '-- Database: ' . $db->getName(); $output[] = '-- Database: ' . $db->getName();
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -12,7 +12,7 @@
* and DROP TABLE before CREATE TABLE for clean restores. * and DROP TABLE before CREATE TABLE for clean restores.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -101,7 +101,7 @@ class DatabaseImporter
// Log but don't abort — some statements may fail on // Log but don't abort — some statements may fail on
// different MySQL versions (e.g. charset differences) // different MySQL versions (e.g. charset differences)
// but the overall restore should continue. // but the overall restore should continue.
error_log('MokoJoomBackup SQL import warning: ' . $e->getMessage()); error_log('MokoSuiteBackup SQL import warning: ' . $e->getMessage());
} }
} }
} }
@@ -115,7 +115,7 @@ class DatabaseImporter
$db->execute(); $db->execute();
$statementsExecuted++; $statementsExecuted++;
} catch (\Exception $e) { } catch (\Exception $e) {
error_log('MokoJoomBackup SQL import warning (final): ' . $e->getMessage()); error_log('MokoSuiteBackup SQL import warning (final): ' . $e->getMessage());
} }
} }
} finally { } finally {
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -15,7 +15,7 @@
* {"path/to/file": {"size": 1234, "mtime": 1717350000}, ...} * {"path/to/file": {"size": 1234, "mtime": 1717350000}, ...}
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -11,7 +11,7 @@
* Skips database.sql and sensitive files that should not be overwritten. * Skips database.sql and sensitive files that should not be overwritten.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -12,7 +12,7 @@
* No SDK dependency pure PHP with cURL. * No SDK dependency pure PHP with cURL.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -19,7 +19,7 @@
* The RestoreEngine can then restore from the extracted files. * The RestoreEngine can then restore from the extracted files.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -21,7 +21,7 @@
* with a Joomla-styled wizard interface. * with a Joomla-styled wizard interface.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -89,7 +89,7 @@ class MokoRestore
* *
* DELETE THIS FILE AFTER INSTALLATION IS COMPLETE. * DELETE THIS FILE AFTER INSTALLATION IS COMPLETE.
* *
* @package MokoJoomBackup * @package MokoSuiteBackup
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later * @license GPL-3.0-or-later
*/ */
@@ -763,7 +763,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<body> <body>
<div class="mr-header"> <div class="mr-header">
<h1>MokoRestore</h1> <h1>MokoRestore</h1>
<p>Standalone Site Installer &mdash; MokoJoomBackup</p> <p>Standalone Site Installer &mdash; MokoSuiteBackup</p>
</div> </div>
<div class="mr-container"> <div class="mr-container">
@@ -927,7 +927,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
<div class="mr-footer"> <div class="mr-footer">
MokoRestore &mdash; <a href="https://mokoconsulting.tech" target="_blank">Moko Consulting</a> MokoRestore &mdash; <a href="https://mokoconsulting.tech" target="_blank">Moko Consulting</a>
&mdash; Part of MokoJoomBackup &mdash; Part of MokoSuiteBackup
</div> </div>
<script> <script>
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -11,7 +11,7 @@
* Uses Joomla's built-in mail system (Factory::getMailer()). * Uses Joomla's built-in mail system (Factory::getMailer()).
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -73,7 +73,7 @@ class NotificationSender
// Build subject // Build subject
$statusLabel = $success ? 'SUCCESS' : 'FAILED'; $statusLabel = $success ? 'SUCCESS' : 'FAILED';
$mailer->setSubject("[MokoJoomBackup] {$statusLabel}: {$record->description}{$siteName}"); $mailer->setSubject("[MokoSuiteBackup] {$statusLabel}: {$record->description}{$siteName}");
// Build body // Build body
$duration = ''; $duration = '';
@@ -92,7 +92,7 @@ class NotificationSender
? number_format($record->total_size / 1048576, 2) . ' MB' ? number_format($record->total_size / 1048576, 2) . ' MB'
: 'N/A'; : 'N/A';
$body = "MokoJoomBackup Notification\n" $body = "MokoSuiteBackup Notification\n"
. "============================\n\n" . "============================\n\n"
. "Site: {$siteName}\n" . "Site: {$siteName}\n"
. "URL: {$siteUrl}\n" . "URL: {$siteUrl}\n"
@@ -125,7 +125,7 @@ class NotificationSender
} }
$body .= "\n--\n" $body .= "\n--\n"
. "MokoJoomBackup — https://mokoconsulting.tech\n"; . "MokoSuiteBackup — https://mokoconsulting.tech\n";
$mailer->setBody($body); $mailer->setBody($body);
$mailer->isHtml(false); $mailer->isHtml(false);
@@ -133,7 +133,7 @@ class NotificationSender
return $mailer->Send(); return $mailer->Send();
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Don't let notification failure break the backup flow // Don't let notification failure break the backup flow
error_log('MokoJoomBackup notification error: ' . $e->getMessage()); error_log('MokoSuiteBackup notification error: ' . $e->getMessage());
return false; return false;
} }
@@ -172,7 +172,7 @@ class NotificationSender
return $db->loadColumn() ?: []; return $db->loadColumn() ?: [];
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log('MokoJoomBackup: Could not resolve user group emails: ' . $e->getMessage()); error_log('MokoSuiteBackup: Could not resolve user group emails: ' . $e->getMessage());
return []; return [];
} }
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -11,12 +11,12 @@
* directory paths and archive filename formats. * directory paths and archive filename formats.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
class PlaceholderResolver class PlaceholderResolver
{ {
@@ -40,6 +40,7 @@ class PlaceholderResolver
'[type]' => 'Backup type (full, database, files, differential)', '[type]' => 'Backup type (full, database, files, differential)',
'[random]' => 'Random 6-character hex string', '[random]' => 'Random 6-character hex string',
'[DEFAULT_DIR]' => 'Default backup directory', '[DEFAULT_DIR]' => 'Default backup directory',
'[HOME]' => 'Home directory of the PHP process owner',
]; ];
private array $replacements; private array $replacements;
@@ -77,6 +78,7 @@ class PlaceholderResolver
'[type]' => $profile->backup_type ?? 'full', '[type]' => $profile->backup_type ?? 'full',
'[random]' => bin2hex(random_bytes(3)), '[random]' => bin2hex(random_bytes(3)),
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(), '[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
'[HOME]' => BackupDirectory::getHomeDirectory(),
]; ];
} }
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -18,7 +18,7 @@
* 6. Clean up staging directory * 6. Clean up staging directory
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -57,7 +57,7 @@ class RestoreEngine
// Load backup record // Load backup record
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokojoombackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('id') . ' = ' . $recordId); ->where($db->quoteName('id') . ' = ' . $recordId);
$db->setQuery($query); $db->setQuery($query);
$record = $db->loadObject(); $record = $db->loadObject();
@@ -77,7 +77,7 @@ class RestoreEngine
} }
// Create staging directory // Create staging directory
$this->stagingDir = JPATH_ROOT . '/tmp/mokojoombackup-restore-' . $record->tag; $this->stagingDir = JPATH_ROOT . '/tmp/mokosuitebackup-restore-' . $record->tag;
if (is_dir($this->stagingDir)) { if (is_dir($this->stagingDir)) {
$this->recursiveDelete($this->stagingDir); $this->recursiveDelete($this->stagingDir);
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -12,7 +12,7 @@
* No SDK dependency pure PHP with cURL. * No SDK dependency pure PHP with cURL.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -16,12 +16,12 @@
* where ini_set() and set_time_limit() are disabled. * where ini_set() and set_time_limit() are disabled.
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
class SteppedBackupEngine class SteppedBackupEngine
{ {
@@ -37,7 +37,7 @@ class SteppedBackupEngine
// Load profile // Load profile
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokojoombackup_profiles')) ->from($db->quoteName('#__mokosuitebackup_profiles'))
->where($db->quoteName('id') . ' = ' . $profileId); ->where($db->quoteName('id') . ' = ' . $profileId);
$db->setQuery($query); $db->setQuery($query);
$profile = $db->loadObject(); $profile = $db->loadObject();
@@ -100,7 +100,7 @@ class SteppedBackupEngine
'log' => '', 'log' => '',
]; ];
$db->insertObject('#__mokojoombackup_records', $record, 'id'); $db->insertObject('#__mokosuitebackup_records', $record, 'id');
$session->recordId = $record->id; $session->recordId = $record->id;
// Determine what work needs to be done and estimate steps // Determine what work needs to be done and estimate steps
@@ -228,7 +228,7 @@ class SteppedBackupEngine
$flags = $session->tableIndex === 0 ? 0 : FILE_APPEND; $flags = $session->tableIndex === 0 ? 0 : FILE_APPEND;
if ($session->tableIndex === 0) { if ($session->tableIndex === 0) {
$header = "-- MokoJoomBackup Database Dump\n" $header = "-- MokoSuiteBackup Database Dump\n"
. "-- Generated: " . date('Y-m-d H:i:s') . "\n" . "-- Generated: " . date('Y-m-d H:i:s') . "\n"
. "-- Prefix: " . $db->getPrefix() . "\n\n" . "-- Prefix: " . $db->getPrefix() . "\n\n"
. "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n" . "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";\n"
@@ -315,7 +315,7 @@ class SteppedBackupEngine
// Clean up temp SQL file // Clean up temp SQL file
if (is_file($sqlFile) && !@unlink($sqlFile)) { if (is_file($sqlFile) && !@unlink($sqlFile)) {
error_log('MokoJoomBackup: Could not delete temp SQL file: ' . $sqlFile); error_log('MokoSuiteBackup: Could not delete temp SQL file: ' . $sqlFile);
} }
$totalSize = file_exists($session->archivePath) ? filesize($session->archivePath) : 0; $totalSize = file_exists($session->archivePath) ? filesize($session->archivePath) : 0;
@@ -344,7 +344,7 @@ class SteppedBackupEngine
'filesexist' => 1, 'filesexist' => 1,
]; ];
$db->updateObject('#__mokojoombackup_records', $update, 'id'); $db->updateObject('#__mokosuitebackup_records', $update, 'id');
$session->currentStep++; $session->currentStep++;
$session->phase = ($session->remoteStorage !== 'none') ? 'upload' : 'complete'; $session->phase = ($session->remoteStorage !== 'none') ? 'upload' : 'complete';
@@ -366,7 +366,7 @@ class SteppedBackupEngine
// Reload profile for remote settings // Reload profile for remote settings
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__mokojoombackup_profiles')) ->from($db->quoteName('#__mokosuitebackup_profiles'))
->where($db->quoteName('id') . ' = ' . $session->profileId); ->where($db->quoteName('id') . ' = ' . $session->profileId);
$db->setQuery($query); $db->setQuery($query);
$profile = $db->loadObject(); $profile = $db->loadObject();
@@ -402,7 +402,7 @@ class SteppedBackupEngine
'filesexist' => is_file($session->archivePath) ? 1 : 0, 'filesexist' => is_file($session->archivePath) ? 1 : 0,
]; ];
$db->updateObject('#__mokojoombackup_records', $update, 'id'); $db->updateObject('#__mokosuitebackup_records', $update, 'id');
$session->currentStep++; $session->currentStep++;
$session->phase = 'complete'; $session->phase = 'complete';
@@ -421,7 +421,7 @@ class SteppedBackupEngine
// Write log file alongside the archive // Write log file alongside the archive
$logPath = BackupDirectory::logPathFromArchive($session->archivePath); $logPath = BackupDirectory::logPathFromArchive($session->archivePath);
if (@file_put_contents($logPath, $logContent) === false) { if (@file_put_contents($logPath, $logContent) === false) {
error_log('MokoJoomBackup: Could not write log file: ' . $logPath); error_log('MokoSuiteBackup: Could not write log file: ' . $logPath);
} }
$update = (object) [ $update = (object) [
@@ -431,7 +431,7 @@ class SteppedBackupEngine
'log' => $logContent, 'log' => $logContent,
]; ];
$db->updateObject('#__mokojoombackup_records', $update, 'id'); $db->updateObject('#__mokosuitebackup_records', $update, 'id');
} }
/** /**
@@ -447,7 +447,7 @@ class SteppedBackupEngine
'log' => implode("\n", $session->log), 'log' => implode("\n", $session->log),
]; ];
$db->updateObject('#__mokojoombackup_records', $update, 'id'); $db->updateObject('#__mokosuitebackup_records', $update, 'id');
} }
/** /**
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -17,7 +17,7 @@
* Phases: init database files finalize upload complete * Phases: init database files finalize upload complete
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -63,7 +63,7 @@ class SteppedSession
private static function getSessionDir(): string private static function getSessionDir(): string
{ {
$dir = JPATH_ROOT . '/tmp/mokojoombackup-sessions'; $dir = JPATH_ROOT . '/tmp/mokosuitebackup-sessions';
if (!is_dir($dir)) { if (!is_dir($dir)) {
if (!mkdir($dir, 0755, true)) { if (!mkdir($dir, 0755, true)) {
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Engine; namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -0,0 +1,45 @@
<?php
/**
* @package MokoSuiteBackup
* @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Joomla\Component\MokoSuiteBackup\Administrator\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Extension\MVCComponent;
use Joomla\CMS\Factory;
class MokoSuiteBackupComponent extends MVCComponent
{
public function boot(): void
{
parent::boot();
try {
$app = Factory::getApplication();
if (!$app->isClient('administrator')) {
return;
}
$wa = $app->getDocument()->getWebAssetManager();
$wa->addInlineStyle(
'#menu a[href*="com_mokosuitebackup"][href*="view=dashboard"] .sidebar-item-title::before,'
. ' #menu a[href*="com_mokosuitebackup"][href*="view=backups"] .sidebar-item-title::before,'
. ' #menu a[href*="com_mokosuitebackup"][href*="view=profiles"] .sidebar-item-title::before'
. ' { font-family: "Font Awesome 6 Free"; font-weight: 900; margin-right: .5em; }'
. ' #menu a[href*="com_mokosuitebackup"][href*="view=dashboard"] .sidebar-item-title::before { content: "\f015"; }'
. ' #menu a[href*="com_mokosuitebackup"][href*="view=backups"] .sidebar-item-title::before { content: "\f1c0"; }'
. ' #menu a[href*="com_mokosuitebackup"][href*="view=profiles"] .sidebar-item-title::before { content: "\f013"; }'
);
} catch (\Throwable $e) {
error_log('MokoSuiteBackup: boot() CSS injection failed: ' . $e->getMessage());
}
}
}
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Field; namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,8 +1,8 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
@@ -11,7 +11,7 @@
* Loads the directory tree from the server via AJAX (browseDir endpoint). * Loads the directory tree from the server via AJAX (browseDir endpoint).
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Field; namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -163,7 +163,7 @@ class DirectoryFilterField extends FormField
const tokenName = Joomla.getOptions('csrf.token') || ''; const tokenName = Joomla.getOptions('csrf.token') || '';
if (tokenName) form.append(tokenName, '1'); if (tokenName) form.append(tokenName, '1');
fetch('index.php?option=com_mokojoombackup&format=json', { fetch('index.php?option=com_mokosuitebackup&format=json', {
method: 'POST', body: form, method: 'POST', body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
}) })
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Field; namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -1,21 +1,21 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Field; namespace Joomla\Component\MokoSuiteBackup\Administrator\Field;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Form\FormField; use Joomla\CMS\Form\FormField;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
class FolderPickerField extends FormField class FolderPickerField extends FormField
{ {
@@ -51,6 +51,7 @@ class FolderPickerField extends FormField
$placeholders = [ $placeholders = [
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(), '[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
'[HOME]' => BackupDirectory::getHomeDirectory(),
'[host]' => $hostname, '[host]' => $hostname,
'[site_name]' => $sanitizedSiteName ?: 'joomla', '[site_name]' => $sanitizedSiteName ?: 'joomla',
'[profile_id]' => '1', '[profile_id]' => '1',
@@ -90,11 +91,14 @@ class FolderPickerField extends FormField
<div class="input-group"> <div class="input-group">
<input type="text" name="{$name}" id="{$id}" value="{$value}" <input type="text" name="{$name}" id="{$id}" value="{$value}"
class="form-control" maxlength="512" class="form-control" maxlength="512"
placeholder="[DEFAULT_DIR] or /home/user/backups/[host]" /> placeholder="[HOME]/backups or [DEFAULT_DIR]" />
<button type="button" class="btn btn-outline-secondary" id="{$id}_btn"> <button type="button" class="btn btn-outline-secondary" id="{$id}_btn">
<span class="icon-folder-open" aria-hidden="true"></span> <span class="icon-folder-open" aria-hidden="true"></span>
Browse Browse
</button> </button>
<button type="button" class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#{$id}_helpModal" title="Available placeholders">
<span class="icon-question-circle" aria-hidden="true"></span>
</button>
</div> </div>
<div class="mt-1" id="{$id}_status"> <div class="mt-1" id="{$id}_status">
<small class="{$statusClass}"> <small class="{$statusClass}">
@@ -106,6 +110,44 @@ class FolderPickerField extends FormField
<span class="icon-warning-circle" aria-hidden="true"></span> <span class="icon-warning-circle" aria-hidden="true"></span>
The default backup directory is inside the web root. Backup archives may be directly downloadable if <code>.htaccess</code> is not supported. For better security, use a path outside the web root. The default backup directory is inside the web root. Backup archives may be directly downloadable if <code>.htaccess</code> is not supported. For better security, use a path outside the web root.
</div> </div>
<div class="modal fade" id="{$id}_helpModal" tabindex="-1" aria-labelledby="{$id}_helpLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="{$id}_helpLabel">Backup Directory Placeholders</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Use these placeholders in the backup directory path. They are resolved at backup time.</p>
<table class="table table-sm table-striped">
<thead><tr><th>Placeholder</th><th>Description</th><th>Example</th></tr></thead>
<tbody>
<tr><td><code>[HOME]</code></td><td>Home directory of the server user</td><td><code>{$placeholders['[HOME]']}</code></td></tr>
<tr><td><code>[DEFAULT_DIR]</code></td><td>Default backup directory (inside web root)</td><td><code>{$placeholders['[DEFAULT_DIR]']}</code></td></tr>
<tr><td><code>[host]</code></td><td>Server hostname</td><td><code>{$placeholders['[host]']}</code></td></tr>
<tr><td><code>[site_name]</code></td><td>Joomla site name</td><td><code>{$placeholders['[site_name]']}</code></td></tr>
<tr><td><code>[date]</code></td><td>Date (Ymd)</td><td><code>{$placeholders['[date]']}</code></td></tr>
<tr><td><code>[year]</code></td><td>Four-digit year</td><td><code>{$placeholders['[year]']}</code></td></tr>
<tr><td><code>[month]</code></td><td>Two-digit month</td><td><code>{$placeholders['[month]']}</code></td></tr>
<tr><td><code>[day]</code></td><td>Two-digit day</td><td><code>{$placeholders['[day]']}</code></td></tr>
<tr><td><code>[profile_id]</code></td><td>Backup profile ID</td><td><code>1</code></td></tr>
<tr><td><code>[profile_name]</code></td><td>Profile title</td><td><code>default</code></td></tr>
<tr><td><code>[type]</code></td><td>Backup type</td><td><code>full</code></td></tr>
</tbody>
</table>
<h6>Recommended Paths</h6>
<ul class="list-unstyled">
<li><code>[HOME]/backups</code> Outside web root (recommended)</li>
<li><code>[HOME]/backups/[host]</code> Per-site subdirectory</li>
<li><code>[DEFAULT_DIR]</code> Inside web root (protected by .htaccess)</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;"> <div id="{$id}_browser" class="card mt-2" style="display:none; max-height:300px; overflow-y:auto;">
<div class="card-body p-2"> <div class="card-body p-2">
<div id="{$id}_tree"></div> <div id="{$id}_tree"></div>
@@ -162,8 +204,12 @@ class FolderPickerField extends FormField
function setDefaultDirWarning() { function setDefaultDirWarning() {
var warn = document.getElementById(fieldId + '_defaultwarn'); var warn = document.getElementById(fieldId + '_defaultwarn');
var val = input.value.trim(); var val = input.value.trim();
var isDefault = (!val || val === '[DEFAULT_DIR]' || val === 'administrator/components/com_mokojoombackup/backups'); var resolved = resolve(val);
if (warn) warn.style.display = isDefault ? 'block' : 'none'; var jRoot = placeholders['[DEFAULT_DIR]'].replace(/\/administrator\/components\/com_mokosuitebackup\/backups$/, '');
var isInsideWebRoot = resolved && resolved.indexOf(jRoot) === 0;
var isOldDefault = val === 'administrator/components/com_mokosuitebackup/backups' || val === 'administrator/components/com_mokojoombackup/backups' || val === '[DEFAULT_DIR]';
var showWarning = isOldDefault || isInsideWebRoot;
if (warn) warn.style.display = showWarning ? 'block' : 'none';
} }
function checkDirPermissions() { function checkDirPermissions() {
@@ -179,7 +225,7 @@ class FolderPickerField extends FormField
var tokenName = Joomla.getOptions('csrf.token') || ''; var tokenName = Joomla.getOptions('csrf.token') || '';
if (tokenName) form.append(tokenName, '1'); if (tokenName) form.append(tokenName, '1');
fetch('index.php?option=com_mokojoombackup&format=json', { fetch('index.php?option=com_mokosuitebackup&format=json', {
method: 'POST', method: 'POST',
body: form, body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
@@ -237,7 +283,7 @@ class FolderPickerField extends FormField
var tokenName = Joomla.getOptions('csrf.token') || ''; var tokenName = Joomla.getOptions('csrf.token') || '';
if (tokenName) form.append(tokenName, '1'); if (tokenName) form.append(tokenName, '1');
fetch('index.php?option=com_mokojoombackup&format=json', { fetch('index.php?option=com_mokosuitebackup&format=json', {
method: 'POST', method: 'POST',
body: form, body: form,
headers: { 'X-Requested-With': 'XMLHttpRequest' } headers: { 'X-Requested-With': 'XMLHttpRequest' }
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Model; namespace Joomla\Component\MokoSuiteBackup\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -20,7 +20,7 @@ class BackupModel extends AdminModel
public function getForm($data = [], $loadData = true) public function getForm($data = [], $loadData = true)
{ {
$form = $this->loadForm( $form = $this->loadForm(
'com_mokojoombackup.backup', 'com_mokosuitebackup.backup',
'backup', 'backup',
['control' => 'jform', 'load_data' => $loadData] ['control' => 'jform', 'load_data' => $loadData]
); );
@@ -30,7 +30,7 @@ class BackupModel extends AdminModel
protected function loadFormData(): object protected function loadFormData(): object
{ {
$data = Factory::getApplication()->getUserState('com_mokojoombackup.edit.backup.data', []); $data = Factory::getApplication()->getUserState('com_mokosuitebackup.edit.backup.data', []);
if (empty($data)) { if (empty($data)) {
$data = $this->getItem(); $data = $this->getItem();
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Model; namespace Joomla\Component\MokoSuiteBackup\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -41,11 +41,11 @@ class BackupsModel extends ListModel
$query = $db->getQuery(true); $query = $db->getQuery(true);
$query->select('a.*') $query->select('a.*')
->from($db->quoteName('#__mokojoombackup_records', 'a')); ->from($db->quoteName('#__mokosuitebackup_records', 'a'));
// Join profile title // Join profile title
$query->select($db->quoteName('p.title', 'profile_title')) $query->select($db->quoteName('p.title', 'profile_title'))
->join('LEFT', $db->quoteName('#__mokojoombackup_profiles', 'p') . ' ON p.id = a.profile_id'); ->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = a.profile_id');
// Filter by status // Filter by status
$status = $this->getState('filter.status'); $status = $this->getState('filter.status');
@@ -1,20 +1,20 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Model; namespace Joomla\Component\MokoSuiteBackup\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
class DashboardModel extends BaseDatabaseModel class DashboardModel extends BaseDatabaseModel
{ {
@@ -28,8 +28,8 @@ class DashboardModel extends BaseDatabaseModel
$db = $this->getDatabase(); $db = $this->getDatabase();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('r.*, p.title AS profile_title') ->select('r.*, p.title AS profile_title')
->from($db->quoteName('#__mokojoombackup_records', 'r')) ->from($db->quoteName('#__mokosuitebackup_records', 'r'))
->join('LEFT', $db->quoteName('#__mokojoombackup_profiles', 'p') . ' ON p.id = r.profile_id') ->join('LEFT', $db->quoteName('#__mokosuitebackup_profiles', 'p') . ' ON p.id = r.profile_id')
->where($db->quoteName('r.status') . ' = ' . $db->quote('complete')) ->where($db->quoteName('r.status') . ' = ' . $db->quote('complete'))
->order($db->quoteName('r.backupend') . ' DESC'); ->order($db->quoteName('r.backupend') . ' DESC');
$db->setQuery($query, 0, 1); $db->setQuery($query, 0, 1);
@@ -38,7 +38,7 @@ class DashboardModel extends BaseDatabaseModel
} }
/** /**
* Query com_scheduler for the next scheduled MokoJoomBackup task. * Query com_scheduler for the next scheduled MokoSuiteBackup task.
* *
* @return object|null Object with next_execution and title, or null * @return object|null Object with next_execution and title, or null
*/ */
@@ -50,7 +50,7 @@ class DashboardModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName(['t.next_execution', 't.title'])) ->select($db->quoteName(['t.next_execution', 't.title']))
->from($db->quoteName('#__scheduler_tasks', 't')) ->from($db->quoteName('#__scheduler_tasks', 't'))
->where($db->quoteName('t.type') . ' = ' . $db->quote('mokojoombackup.run_profile')) ->where($db->quoteName('t.type') . ' = ' . $db->quote('mokosuitebackup.run_profile'))
->where($db->quoteName('t.state') . ' = 1') ->where($db->quoteName('t.state') . ' = 1')
->order($db->quoteName('t.next_execution') . ' ASC'); ->order($db->quoteName('t.next_execution') . ' ASC');
$db->setQuery($query, 0, 1); $db->setQuery($query, 0, 1);
@@ -74,7 +74,7 @@ class DashboardModel extends BaseDatabaseModel
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('COUNT(*) AS total_count') ->select('COUNT(*) AS total_count')
->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size') ->select('COALESCE(SUM(' . $db->quoteName('total_size') . '), 0) AS total_size')
->from($db->quoteName('#__mokojoombackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('status') . ' = ' . $db->quote('complete')); ->where($db->quoteName('status') . ' = ' . $db->quote('complete'));
$db->setQuery($query); $db->setQuery($query);
$stats = $db->loadObject(); $stats = $db->loadObject();
@@ -83,7 +83,7 @@ class DashboardModel extends BaseDatabaseModel
$cutoff = date('Y-m-d H:i:s', strtotime('-7 days')); $cutoff = date('Y-m-d H:i:s', strtotime('-7 days'));
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('COUNT(*) AS fail_count') ->select('COUNT(*) AS fail_count')
->from($db->quoteName('#__mokojoombackup_records')) ->from($db->quoteName('#__mokosuitebackup_records'))
->where($db->quoteName('status') . ' = ' . $db->quote('fail')) ->where($db->quoteName('status') . ' = ' . $db->quote('fail'))
->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff)); ->where($db->quoteName('backupstart') . ' >= ' . $db->quote($cutoff));
$db->setQuery($query); $db->setQuery($query);
@@ -129,7 +129,7 @@ class DashboardModel extends BaseDatabaseModel
$db2 = $this->getDatabase(); $db2 = $this->getDatabase();
$qDir = $db2->getQuery(true) $qDir = $db2->getQuery(true)
->select($db2->quoteName('backup_dir')) ->select($db2->quoteName('backup_dir'))
->from($db2->quoteName('#__mokojoombackup_profiles')) ->from($db2->quoteName('#__mokosuitebackup_profiles'))
->where($db2->quoteName('published') . ' = 1') ->where($db2->quoteName('published') . ' = 1')
->where($db2->quoteName('backup_dir') . ' != ' . $db2->quote('')) ->where($db2->quoteName('backup_dir') . ' != ' . $db2->quote(''))
->where($db2->quoteName('backup_dir') . ' IS NOT NULL'); ->where($db2->quoteName('backup_dir') . ' IS NOT NULL');
@@ -176,16 +176,26 @@ class DashboardModel extends BaseDatabaseModel
{ {
$db = $this->getDatabase(); $db = $this->getDatabase();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select('COUNT(*)') ->select($db->quoteName('backup_dir'))
->from($db->quoteName('#__mokojoombackup_profiles')) ->from($db->quoteName('#__mokosuitebackup_profiles'))
->where($db->quoteName('published') . ' = 1') ->where($db->quoteName('published') . ' = 1');
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::DEFAULT_RELATIVE)
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::PLACEHOLDER)
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
$db->setQuery($query); $db->setQuery($query);
$dirs = $db->loadColumn();
return (int) $db->loadResult() > 0; // Warn only if any profile's resolved path is inside the web root
foreach ($dirs as $dir) {
$resolved = BackupDirectory::resolve($dir ?: BackupDirectory::DEFAULT_RELATIVE);
if (BackupDirectory::hasPlaceholders($resolved)) {
continue;
}
if (BackupDirectory::isWebAccessible($resolved)) {
return true;
}
}
return false;
} }
/** /**
@@ -198,7 +208,7 @@ class DashboardModel extends BaseDatabaseModel
$db = $this->getDatabase(); $db = $this->getDatabase();
$query = $db->getQuery(true) $query = $db->getQuery(true)
->select($db->quoteName(['id', 'title', 'backup_type'])) ->select($db->quoteName(['id', 'title', 'backup_type']))
->from($db->quoteName('#__mokojoombackup_profiles')) ->from($db->quoteName('#__mokosuitebackup_profiles'))
->where($db->quoteName('published') . ' = 1') ->where($db->quoteName('published') . ' = 1')
->order($db->quoteName('ordering') . ' ASC'); ->order($db->quoteName('ordering') . ' ASC');
$db->setQuery($query); $db->setQuery($query);
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Model; namespace Joomla\Component\MokoSuiteBackup\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -20,7 +20,7 @@ class ProfileModel extends AdminModel
public function getForm($data = [], $loadData = true) public function getForm($data = [], $loadData = true)
{ {
$form = $this->loadForm( $form = $this->loadForm(
'com_mokojoombackup.profile', 'com_mokosuitebackup.profile',
'profile', 'profile',
['control' => 'jform', 'load_data' => $loadData] ['control' => 'jform', 'load_data' => $loadData]
); );
@@ -30,7 +30,7 @@ class ProfileModel extends AdminModel
protected function loadFormData(): object protected function loadFormData(): object
{ {
$data = Factory::getApplication()->getUserState('com_mokojoombackup.edit.profile.data', []); $data = Factory::getApplication()->getUserState('com_mokosuitebackup.edit.profile.data', []);
if (empty($data)) { if (empty($data)) {
$data = $this->getItem(); $data = $this->getItem();
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Model; namespace Joomla\Component\MokoSuiteBackup\Administrator\Model;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -38,7 +38,7 @@ class ProfilesModel extends ListModel
$query = $db->getQuery(true); $query = $db->getQuery(true);
$query->select('a.*') $query->select('a.*')
->from($db->quoteName('#__mokojoombackup_profiles', 'a')); ->from($db->quoteName('#__mokosuitebackup_profiles', 'a'));
$published = $this->getState('filter.published'); $published = $this->getState('filter.published');
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Table; namespace Joomla\Component\MokoSuiteBackup\Administrator\Table;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -19,7 +19,7 @@ class BackupTable extends Table
{ {
public function __construct(DatabaseDriver $db) public function __construct(DatabaseDriver $db)
{ {
parent::__construct('#__mokojoombackup_records', 'id', $db); parent::__construct('#__mokosuitebackup_records', 'id', $db);
} }
public function check(): bool public function check(): bool
@@ -1,26 +1,26 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\Table; namespace Joomla\Component\MokoSuiteBackup\Administrator\Table;
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Table\Table; use Joomla\CMS\Table\Table;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory; use Joomla\Component\MokoSuiteBackup\Administrator\Utility\BackupDirectory;
use Joomla\Database\DatabaseDriver; use Joomla\Database\DatabaseDriver;
class ProfileTable extends Table class ProfileTable extends Table
{ {
public function __construct(DatabaseDriver $db) public function __construct(DatabaseDriver $db)
{ {
parent::__construct('#__mokojoombackup_profiles', 'id', $db); parent::__construct('#__mokosuitebackup_profiles', 'id', $db);
} }
public function store($updateNulls = true): bool public function store($updateNulls = true): bool
@@ -61,6 +61,11 @@ class ProfileTable extends Table
$this->backup_type = 'full'; $this->backup_type = 'full';
} }
// Normalize backup_dir to portable placeholder form
if (!empty($this->backup_dir)) {
$this->backup_dir = BackupDirectory::portablize($this->backup_dir);
}
$now = date('Y-m-d H:i:s'); $now = date('Y-m-d H:i:s');
if (empty($this->created) || $this->created === '0000-00-00 00:00:00') { if (empty($this->created) || $this->created === '0000-00-00 00:00:00') {
@@ -0,0 +1,277 @@
<?php
/**
* @package MokoSuiteBackup
* @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Joomla\Component\MokoSuiteBackup\Administrator\Utility;
defined('_JEXEC') or die;
class BackupDirectory
{
public const DEFAULT_RELATIVE = './backups';
public const PLACEHOLDER = '[DEFAULT_DIR]';
private const HTACCESS_CONTENT = <<<'HTACCESS'
# Apache 2.4+
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
# Apache 2.2
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>
HTACCESS;
private const INDEX_CONTENT = '<!DOCTYPE html><title></title>';
public const HOME_PLACEHOLDER = '[HOME]';
/**
* Get the absolute default backup directory path.
*/
public static function getDefaultAbsolute(): string
{
return JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/backups';
}
/**
* Detect the home directory of the PHP process owner.
*
* Tries multiple sources because PHP-FPM on shared hosting
* often strips shell environment variables.
*
* @return string Absolute home path, or empty string if undetectable
*/
public static function getHomeDirectory(): string
{
// 1. Environment variables (works in CLI and some FPM configs)
$home = getenv('HOME') ?: ($_SERVER['HOME'] ?? '');
if ($home === '') {
$home = getenv('USERPROFILE') ?: ($_SERVER['USERPROFILE'] ?? '');
}
// 2. POSIX: read from /etc/passwd (most reliable on Linux)
if ($home === '' && \function_exists('posix_getpwuid') && \function_exists('posix_geteuid')) {
$info = posix_getpwuid(posix_geteuid());
if ($info && !empty($info['dir'])) {
$home = $info['dir'];
}
}
// 3. Derive from JPATH_ROOT — walk up until we find a home-like path
if ($home === '' && \defined('JPATH_ROOT')) {
$parts = explode('/', rtrim(JPATH_ROOT, '/'));
// Typical pattern: /home/{user}/domain/public_html
if (count($parts) >= 3 && $parts[1] === 'home') {
$home = '/' . $parts[1] . '/' . $parts[2];
}
}
return rtrim($home, '/\\');
}
/**
* Resolve a backup directory path. Replaces [DEFAULT_DIR] and [HOME]
* placeholders, then resolves relative paths from JPATH_ROOT.
*
* @param string $dir Raw directory value from profile
*
* @return string Absolute path (may still contain other placeholders)
*/
public static function resolve(string $dir): string
{
if ($dir === '' || $dir === self::PLACEHOLDER) {
$dir = self::getDefaultAbsolute();
} else {
$dir = str_replace(self::PLACEHOLDER, self::getDefaultAbsolute(), $dir);
}
// Resolve [HOME] placeholder
if (strpos($dir, self::HOME_PLACEHOLDER) !== false) {
$home = self::getHomeDirectory();
if ($home !== '') {
$dir = str_replace(self::HOME_PLACEHOLDER, $home, $dir);
}
}
if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
return self::normalizePath($dir);
}
return self::normalizePath(JPATH_ROOT . '/' . $dir);
}
/**
* Convert an absolute or literal path back to portable placeholder form.
*
* Replaces known absolute values with their placeholder tokens so
* the stored path remains portable across environments.
*
* @param string $dir Raw directory value (e.g. from database)
*
* @return string Path with placeholders restored where possible
*/
public static function portablize(string $dir): string
{
// Replace the old literal default with the relative default
$oldDefault = 'administrator/components/com_mokosuitebackup/backups';
$oldDefaultAlt = 'administrator/components/com_mokojoombackup/backups';
if ($dir === $oldDefault || $dir === $oldDefaultAlt || $dir === self::PLACEHOLDER) {
return self::DEFAULT_RELATIVE;
}
// Replace absolute default path with relative default
$absDefault = self::getDefaultAbsolute();
if ($dir === $absDefault) {
return self::DEFAULT_RELATIVE;
}
// Replace absolute HOME prefix with [HOME]
$home = self::getHomeDirectory();
if ($home !== '' && strpos($dir, $home . '/') === 0) {
$dir = self::HOME_PLACEHOLDER . substr($dir, \strlen($home));
}
return $dir;
}
/**
* Normalize a path by resolving `.` and `..` segments without requiring
* the path to exist on disk (unlike realpath).
*/
public static function normalizePath(string $path): string
{
$path = str_replace('\\', '/', $path);
$prefix = '';
// Preserve leading slash (Unix) or drive letter (Windows)
if (isset($path[0]) && $path[0] === '/') {
$prefix = '/';
} elseif (preg_match('#^([A-Za-z]:/)#', $path, $m)) {
$prefix = $m[1];
$path = substr($path, \strlen($prefix));
}
$parts = [];
foreach (explode('/', $path) as $seg) {
if ($seg === '' || $seg === '.') {
continue;
}
if ($seg === '..' && $parts && end($parts) !== '..') {
array_pop($parts);
} else {
$parts[] = $seg;
}
}
return rtrim($prefix . implode('/', $parts), '/');
}
/**
* Check whether a resolved path still contains unresolved placeholders.
*/
public static function hasPlaceholders(string $path): bool
{
return (bool) preg_match('/\[.+\]/', $path);
}
/**
* Check whether a resolved absolute path is inside the web root.
*/
public static function isWebAccessible(string $absolutePath): bool
{
$jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
$realDir = realpath($absolutePath) ?: $absolutePath;
return strpos($realDir, $jRoot) === 0;
}
/**
* Create .htaccess and index.html protection files in a directory.
* Only creates files if they don't already exist.
*/
public static function protect(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$htaccess = $dir . '/.htaccess';
if (!is_file($htaccess)) {
if (@file_put_contents($htaccess, self::HTACCESS_CONTENT . "\n") === false) {
error_log('MokoSuiteBackup: Could not create .htaccess in: ' . $dir);
}
}
$index = $dir . '/index.html';
if (!is_file($index)) {
if (@file_put_contents($index, self::INDEX_CONTENT) === false) {
error_log('MokoSuiteBackup: Could not create index.html in: ' . $dir);
}
}
}
/**
* Ensure the backup directory exists, create it if needed,
* and apply web protection if it's inside the web root.
*
* @return bool True if directory exists and is usable
*/
public static function ensureReady(string $dir): bool
{
if (!is_dir($dir)) {
if (!@mkdir($dir, 0755, true)) {
return false;
}
}
// Only add .htaccess/index.html when inside the web root
if (self::isWebAccessible($dir)) {
self::protect($dir);
}
return true;
}
/**
* Parse a newline-separated text field into an array of trimmed, non-empty strings.
*/
public static function parseNewlineList(string $text): array
{
if (empty($text)) {
return [];
}
return array_values(array_filter(
array_map('trim', explode("\n", str_replace("\r", '', $text))),
fn($line) => $line !== ''
));
}
/**
* Derive the log file path from an archive path.
*/
public static function logPathFromArchive(string $archivePath): string
{
return preg_replace('/\.(zip|tar\.gz)$/i', '.log', $archivePath);
}
}
@@ -1,14 +1,14 @@
<?php <?php
/** /**
* @package MokoJoomBackup * @package MokoSuiteBackup
* @subpackage com_mokojoombackup * @subpackage com_mokosuitebackup
* @author Moko Consulting <hello@mokoconsulting.tech> * @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE * @license GNU General Public License version 3 or later; see LICENSE
*/ */
namespace Joomla\Component\MokoJoomBackup\Administrator\View\Backup; namespace Joomla\Component\MokoSuiteBackup\Administrator\View\Backup;
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -34,6 +34,6 @@ class HtmlView extends BaseHtmlView
protected function addToolbar(): void protected function addToolbar(): void
{ {
ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database'); ToolbarHelper::title(Text::_('COM_MOKOJOOMBACKUP_BACKUP_DETAIL'), 'database');
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokojoombackup&view=backups'); ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokosuitebackup&view=backups');
} }
} }

Some files were not shown because too many files have changed in this diff Show More