Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9af3b7ba9d | |||
| abe68c1e64 | |||
| 4d74627720 | |||
| e5fb88e1a4 | |||
| df44518611 | |||
| 5e0683c0c7 | |||
| bbf3dcd449 | |||
| 139423cbe9 | |||
| 814d1b147c | |||
| 6dc7fdfa91 | |||
| 3de786cf71 |
@@ -5,7 +5,7 @@
|
|||||||
<display-name>Package - MokoJoomBackup</display-name>
|
<display-name>Package - MokoJoomBackup</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.05.00-dev</version>
|
<version>01.08.00-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>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Automation
|
# INGROUP: mokoplatform.Automation
|
||||||
# VERSION: 01.05.00
|
# VERSION: 01.08.00
|
||||||
# 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"
|
||||||
|
|||||||
+6
-63
@@ -1,6 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [01.08.00] --- 2026-06-07
|
||||||
|
|
||||||
|
## [01.07.00] --- 2026-06-07
|
||||||
|
|
||||||
|
## [01.06.00] --- 2026-06-07
|
||||||
|
|
||||||
|
|
||||||
## [01.05.00] --- 2026-06-07
|
## [01.05.00] --- 2026-06-07
|
||||||
|
|
||||||
@@ -23,66 +29,3 @@
|
|||||||
- Dashboard submenu language key added to `.sys.ini` files (en-GB, en-US)
|
- Dashboard submenu language key added to `.sys.ini` files (en-GB, en-US)
|
||||||
|
|
||||||
## [01.04.00] --- 2026-06-07
|
## [01.04.00] --- 2026-06-07
|
||||||
|
|
||||||
|
|
||||||
## [01.03.00] --- 2026-06-07
|
|
||||||
|
|
||||||
|
|
||||||
## [01.02.00] --- 2026-06-07
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Joomla-styled standalone installer (MokoRestore) with 7-step wizard, admin password reset, and client provisioning
|
|
||||||
- Web cron trigger for shared hosting without crontab — URL-based backup with secret word, IP whitelist
|
|
||||||
- Placeholder support for backup directories and archive filenames ([host], [date], [site_name], [profile_name], etc.)
|
|
||||||
- FolderPicker JS placeholder resolution — resolves [site_name]/[host] when browsing, reverse-replaces on selection for portable profiles
|
|
||||||
- Archive Name Format field on backup profiles with customizable filename templates
|
|
||||||
- Interactive directory tree browser for exclude filters (replaces plain text input)
|
|
||||||
- Backup log viewer modal in backup records list and inline in detail view
|
|
||||||
- Clickable dashboard status tiles linking to backup records, detail views, and scheduled tasks
|
|
||||||
- Table exclusion now supports separate Data and Structure checkboxes (backward compatible)
|
|
||||||
- Tar.gz archive format support
|
|
||||||
- User group notifications for backup events
|
|
||||||
- Folder picker field with live server directory browsing
|
|
||||||
- Default directory dashboard warning when backups are stored inside web root
|
|
||||||
- Backup log files written alongside archives (.log)
|
|
||||||
- Backup detail view with checksum, file path, DB size, and embedded log
|
|
||||||
- Browser beforeunload warning during backup progress
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Renamed all extension elements from mokobackup to mokojoombackup (pkg, com, all plugins, DB tables, namespaces, language keys)
|
|
||||||
- Renamed source directory from src/ to source/ per MokoStandards convention
|
|
||||||
- Dashboard health check shows actual resolved backup directory path from profiles
|
|
||||||
- Update site post-install notice links to filtered list view (avoids Joomla core bug)
|
|
||||||
- License warning suppressed when download key is already configured
|
|
||||||
- Download key preserved across package updates via preflight/postflight backup
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Download ERR_INVALID_RESPONSE — flush output buffers before sending file headers
|
|
||||||
- Backup directory path resolution for absolute paths outside web root
|
|
||||||
- Schema migrations consolidated to version within extension range
|
|
||||||
- PSR-4 class file naming (MokoBackup*.php → MokoJoomBackup*.php)
|
|
||||||
- Nested package directories from rename flattened
|
|
||||||
- INSERT IGNORE for default profile prevents duplicate key on update
|
|
||||||
- ActionlogsHelper::getIp() replaced — method does not exist in Joomla 5
|
|
||||||
- Console plugin namespace and quickicon translation keys
|
|
||||||
- CLI exit codes and SQL schema defaults
|
|
||||||
- Component Options page (added config.xml)
|
|
||||||
- Placeholder-aware directory checks in FolderPicker and dashboard health
|
|
||||||
|
|
||||||
## 01.01 — 2026-06-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Admin dashboard view as default landing page with status cards, quick actions, and system health checklist (#28)
|
|
||||||
- Console plugin (plg_console_mokojoombackup) — CLI commands: run, list, profiles, restore, cleanup (#29)
|
|
||||||
- Content plugin (plg_content_mokojoombackup) — auto-backup before extension install/update (#30)
|
|
||||||
- Actionlog plugin (plg_actionlog_mokojoombackup) — logs backup and profile actions to User Action Logs (#31)
|
|
||||||
- BackupEngine dispatches onMokoJoomBackupAfterRun event for plugin listeners
|
|
||||||
- Update site notice on dashboard and post-install
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Renamed Kickstart to MokoRestore throughout
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- SQL update migration and error handling
|
|
||||||
- Removed orphaned scriptfile from component manifest
|
|
||||||
- Consolidated admin files into single files block
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoJoomBackup
|
# MokoJoomBackup
|
||||||
|
|
||||||
<!-- VERSION: 01.05.00 -->
|
<!-- VERSION: 01.08.00 -->
|
||||||
|
|
||||||
Full-site backup and restore for Joomla — database, files, and configuration.
|
Full-site backup and restore for Joomla — database, files, and configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokojoombackup</name>
|
<name>com_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</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>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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\MokoJoomBackup\Administrator\Engine\SteppedBackupEngine;
|
||||||
|
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
|
||||||
|
|
||||||
class AjaxController extends BaseController
|
class AjaxController extends BaseController
|
||||||
{
|
{
|
||||||
@@ -196,7 +197,7 @@ class AjaxController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to load log from file alongside the archive
|
// Try to load log from file alongside the archive
|
||||||
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $record->absolute_path);
|
$logPath = BackupDirectory::logPathFromArchive($record->absolute_path);
|
||||||
$logContent = '';
|
$logContent = '';
|
||||||
|
|
||||||
if (is_file($logPath)) {
|
if (is_file($logPath)) {
|
||||||
@@ -239,17 +240,9 @@ class AjaxController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve [DEFAULT_DIR] placeholder
|
$resolved = BackupDirectory::resolve($rawPath);
|
||||||
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
|
||||||
$resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $rawPath);
|
|
||||||
|
|
||||||
// Resolve relative paths from JPATH_ROOT
|
if (BackupDirectory::hasPlaceholders($resolved)) {
|
||||||
if ($resolved !== '' && $resolved[0] !== '/' && !preg_match('#^[A-Za-z]:[/\\\\]#', $resolved)) {
|
|
||||||
$resolved = JPATH_ROOT . '/' . $resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip check if unresolved placeholders remain
|
|
||||||
if (preg_match('/\[.+\]/', $resolved)) {
|
|
||||||
$this->sendJson([
|
$this->sendJson([
|
||||||
'error' => false,
|
'error' => false,
|
||||||
'exists' => null,
|
'exists' => null,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ namespace Joomla\Component\MokoJoomBackup\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;
|
||||||
|
|
||||||
class AkeebaImporter
|
class AkeebaImporter
|
||||||
{
|
{
|
||||||
@@ -484,7 +485,7 @@ class AkeebaImporter
|
|||||||
$dir = $config['akeeba.basic.output_directory'] ?? '';
|
$dir = $config['akeeba.basic.output_directory'] ?? '';
|
||||||
|
|
||||||
if (empty($dir) || $dir === '[DEFAULT_OUTPUT]') {
|
if (empty($dir) || $dir === '[DEFAULT_OUTPUT]') {
|
||||||
return 'administrator/components/com_mokojoombackup/backups';
|
return BackupDirectory::DEFAULT_RELATIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert absolute path to relative
|
// Convert absolute path to relative
|
||||||
@@ -492,7 +493,7 @@ class AkeebaImporter
|
|||||||
$dir = ltrim(substr($dir, strlen(JPATH_ROOT)), '/\\');
|
$dir = ltrim(substr($dir, strlen(JPATH_ROOT)), '/\\');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $dir ?: 'administrator/components/com_mokojoombackup/backups';
|
return $dir ?: BackupDirectory::DEFAULT_RELATIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function mapRemoteStorage(array $config): string
|
private function mapRemoteStorage(array $config): string
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoJoomBackup\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\Event\Event;
|
use Joomla\Event\Event;
|
||||||
|
|
||||||
class BackupEngine
|
class BackupEngine
|
||||||
@@ -56,24 +57,20 @@ class BackupEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read settings directly from profile columns
|
// Read settings directly from profile columns
|
||||||
$excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? '');
|
$excludeDirs = BackupDirectory::parseNewlineList($profile->exclude_dirs ?? '');
|
||||||
$excludeFiles = $this->parseNewlineList($profile->exclude_files ?? '');
|
$excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? '');
|
||||||
$excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
|
$excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
|
||||||
|
|
||||||
// Resolve placeholders in directory and filename
|
// Resolve placeholders in directory and filename
|
||||||
$resolver = new PlaceholderResolver($profile);
|
$resolver = new PlaceholderResolver($profile);
|
||||||
|
|
||||||
$configuredDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
|
$configuredDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
|
||||||
$this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir));
|
$this->backupDir = BackupDirectory::resolve($resolver->resolve($configuredDir));
|
||||||
|
|
||||||
if (!is_dir($this->backupDir)) {
|
if (!BackupDirectory::ensureReady($this->backupDir)) {
|
||||||
if (!mkdir($this->backupDir, 0755, true)) {
|
return ['success' => false, 'message' => 'Cannot create backup directory: ' . $this->backupDir, 'record_id' => 0];
|
||||||
return ['success' => false, 'message' => 'Cannot create backup directory: ' . $this->backupDir, 'record_id' => 0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->protectBackupDir($this->backupDir);
|
|
||||||
|
|
||||||
// Create backup record
|
// Create backup record
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
$tag = $resolver->getTag();
|
$tag = $resolver->getTag();
|
||||||
@@ -474,21 +471,6 @@ class BackupEngine
|
|||||||
$zip->close();
|
$zip->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a newline-separated text field into an array of trimmed, non-empty strings.
|
|
||||||
*/
|
|
||||||
private 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 !== ''
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch the onMokoJoomBackupAfterRun event so plugins (actionlog, etc.) can react.
|
* Dispatch the onMokoJoomBackupAfterRun event so plugins (actionlog, etc.) can react.
|
||||||
*/
|
*/
|
||||||
@@ -512,38 +494,6 @@ class BackupEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a backup directory path. Absolute paths are used as-is,
|
|
||||||
* relative paths are resolved from JPATH_ROOT.
|
|
||||||
*/
|
|
||||||
private function resolveBackupDir(string $dir): string
|
|
||||||
{
|
|
||||||
if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
|
|
||||||
return rtrim($dir, '/\\');
|
|
||||||
}
|
|
||||||
|
|
||||||
return JPATH_ROOT . '/' . $dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function protectBackupDir(string $dir): void
|
|
||||||
{
|
|
||||||
$htaccess = $dir . '/.htaccess';
|
|
||||||
|
|
||||||
if (!is_file($htaccess)) {
|
|
||||||
if (@file_put_contents($htaccess, "# Apache 2.4+\n<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n# Apache 2.2\n<IfModule !mod_authz_core.c>\n Order deny,allow\n Deny from all\n</IfModule>\n") === false) {
|
|
||||||
error_log('MokoJoomBackup: Could not create .htaccess in backup directory: ' . $dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$index = $dir . '/index.html';
|
|
||||||
|
|
||||||
if (!is_file($index)) {
|
|
||||||
if (@file_put_contents($index, '<!DOCTYPE html><title></title>') === false) {
|
|
||||||
error_log('MokoJoomBackup: Could not create index.html in backup directory: ' . $dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function log(string $message): void
|
private function log(string $message): void
|
||||||
{
|
{
|
||||||
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
|
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace Joomla\Component\MokoJoomBackup\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;
|
||||||
|
|
||||||
class PlaceholderResolver
|
class PlaceholderResolver
|
||||||
{
|
{
|
||||||
@@ -38,7 +39,7 @@ class PlaceholderResolver
|
|||||||
'[site_name]' => 'Joomla site name (sanitized)',
|
'[site_name]' => 'Joomla site name (sanitized)',
|
||||||
'[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 (administrator/components/com_mokojoombackup/backups)',
|
'[DEFAULT_DIR]' => 'Default backup directory',
|
||||||
];
|
];
|
||||||
|
|
||||||
private array $replacements;
|
private array $replacements;
|
||||||
@@ -75,7 +76,7 @@ class PlaceholderResolver
|
|||||||
'[site_name]' => $this->sanitize($siteName ?: 'joomla'),
|
'[site_name]' => $this->sanitize($siteName ?: 'joomla'),
|
||||||
'[type]' => $profile->backup_type ?? 'full',
|
'[type]' => $profile->backup_type ?? 'full',
|
||||||
'[random]' => bin2hex(random_bytes(3)),
|
'[random]' => bin2hex(random_bytes(3)),
|
||||||
'[DEFAULT_DIR]' => JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups',
|
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace Joomla\Component\MokoJoomBackup\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;
|
||||||
|
|
||||||
class SteppedBackupEngine
|
class SteppedBackupEngine
|
||||||
{
|
{
|
||||||
@@ -52,26 +53,22 @@ class SteppedBackupEngine
|
|||||||
$session->backupType = $profile->backup_type;
|
$session->backupType = $profile->backup_type;
|
||||||
|
|
||||||
// Parse profile settings
|
// Parse profile settings
|
||||||
$session->excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? '');
|
$session->excludeDirs = BackupDirectory::parseNewlineList($profile->exclude_dirs ?? '');
|
||||||
$session->excludeFiles = $this->parseNewlineList($profile->exclude_files ?? '');
|
$session->excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? '');
|
||||||
$session->excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
|
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
|
||||||
$session->backupDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
|
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
|
||||||
$session->remoteStorage = $profile->remote_storage ?? 'none';
|
$session->remoteStorage = $profile->remote_storage ?? 'none';
|
||||||
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
|
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
|
||||||
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
|
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
|
||||||
|
|
||||||
// Resolve placeholders in directory and filename
|
// Resolve placeholders in directory and filename
|
||||||
$resolver = new PlaceholderResolver($profile);
|
$resolver = new PlaceholderResolver($profile);
|
||||||
$backupDir = $this->resolveBackupDir($resolver->resolve($session->backupDir));
|
$backupDir = BackupDirectory::resolve($resolver->resolve($session->backupDir));
|
||||||
|
|
||||||
if (!is_dir($backupDir)) {
|
if (!BackupDirectory::ensureReady($backupDir)) {
|
||||||
if (!mkdir($backupDir, 0755, true)) {
|
return ['error' => true, 'message' => 'Cannot create backup directory: ' . $backupDir];
|
||||||
return ['error' => true, 'message' => 'Cannot create backup directory: ' . $backupDir];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->protectBackupDir($backupDir);
|
|
||||||
|
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
$tag = $resolver->getTag();
|
$tag = $resolver->getTag();
|
||||||
$nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]';
|
$nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]';
|
||||||
@@ -422,7 +419,7 @@ class SteppedBackupEngine
|
|||||||
$logContent = implode("\n", $session->log);
|
$logContent = implode("\n", $session->log);
|
||||||
|
|
||||||
// Write log file alongside the archive
|
// Write log file alongside the archive
|
||||||
$logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $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('MokoJoomBackup: Could not write log file: ' . $logPath);
|
||||||
}
|
}
|
||||||
@@ -554,47 +551,4 @@ class SteppedBackupEngine
|
|||||||
return $tables;
|
return $tables;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a backup directory path. Absolute paths are used as-is,
|
|
||||||
* relative paths are resolved from JPATH_ROOT.
|
|
||||||
*/
|
|
||||||
private function resolveBackupDir(string $dir): string
|
|
||||||
{
|
|
||||||
if ($dir !== '' && ($dir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $dir))) {
|
|
||||||
return rtrim($dir, '/\\');
|
|
||||||
}
|
|
||||||
|
|
||||||
return JPATH_ROOT . '/' . $dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function protectBackupDir(string $dir): void
|
|
||||||
{
|
|
||||||
$htaccess = $dir . '/.htaccess';
|
|
||||||
|
|
||||||
if (!is_file($htaccess)) {
|
|
||||||
if (@file_put_contents($htaccess, "# Apache 2.4+\n<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n# Apache 2.2\n<IfModule !mod_authz_core.c>\n Order deny,allow\n Deny from all\n</IfModule>\n") === false) {
|
|
||||||
error_log('MokoJoomBackup: Could not create .htaccess in backup directory: ' . $dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$index = $dir . '/index.html';
|
|
||||||
|
|
||||||
if (!is_file($index)) {
|
|
||||||
if (@file_put_contents($index, '<!DOCTYPE html><title></title>') === false) {
|
|
||||||
error_log('MokoJoomBackup: Could not create index.html in backup directory: ' . $dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 !== ''
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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;
|
||||||
|
|
||||||
class FolderPickerField extends FormField
|
class FolderPickerField extends FormField
|
||||||
{
|
{
|
||||||
@@ -49,7 +50,7 @@ class FolderPickerField extends FormField
|
|||||||
$sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName)));
|
$sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName)));
|
||||||
|
|
||||||
$placeholders = [
|
$placeholders = [
|
||||||
'[DEFAULT_DIR]' => JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups',
|
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
|
||||||
'[host]' => $hostname,
|
'[host]' => $hostname,
|
||||||
'[site_name]' => $sanitizedSiteName ?: 'joomla',
|
'[site_name]' => $sanitizedSiteName ?: 'joomla',
|
||||||
'[profile_id]' => '1',
|
'[profile_id]' => '1',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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;
|
||||||
|
|
||||||
class DashboardModel extends BaseDatabaseModel
|
class DashboardModel extends BaseDatabaseModel
|
||||||
{
|
{
|
||||||
@@ -122,11 +123,9 @@ class DashboardModel extends BaseDatabaseModel
|
|||||||
'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+',
|
'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Backup directory writable — check the default path
|
// Backup directory writable — check the first published profile's dir
|
||||||
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
$backupDir = BackupDirectory::getDefaultAbsolute();
|
||||||
$backupDir = $defaultDir;
|
|
||||||
|
|
||||||
// If profiles use a custom directory, check that instead
|
|
||||||
$db2 = $this->getDatabase();
|
$db2 = $this->getDatabase();
|
||||||
$qDir = $db2->getQuery(true)
|
$qDir = $db2->getQuery(true)
|
||||||
->select($db2->quoteName('backup_dir'))
|
->select($db2->quoteName('backup_dir'))
|
||||||
@@ -138,16 +137,10 @@ class DashboardModel extends BaseDatabaseModel
|
|||||||
$profileDir = $db2->loadResult();
|
$profileDir = $db2->loadResult();
|
||||||
|
|
||||||
if ($profileDir) {
|
if ($profileDir) {
|
||||||
// Absolute paths used as-is, relative resolved from JPATH_ROOT
|
$backupDir = BackupDirectory::resolve($profileDir);
|
||||||
if ($profileDir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $profileDir)) {
|
|
||||||
$backupDir = rtrim($profileDir, '/\\');
|
|
||||||
} else {
|
|
||||||
$backupDir = JPATH_ROOT . '/' . $profileDir;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip filesystem check if path contains placeholders (resolved at backup time)
|
if (BackupDirectory::hasPlaceholders($backupDir)) {
|
||||||
if (preg_match('/\[.+\]/', $backupDir)) {
|
|
||||||
$checks[] = (object) [
|
$checks[] = (object) [
|
||||||
'label' => 'Backup Directory',
|
'label' => 'Backup Directory',
|
||||||
'status' => true,
|
'status' => true,
|
||||||
@@ -182,14 +175,12 @@ class DashboardModel extends BaseDatabaseModel
|
|||||||
public function isUsingDefaultBackupDir(): bool
|
public function isUsingDefaultBackupDir(): bool
|
||||||
{
|
{
|
||||||
$db = $this->getDatabase();
|
$db = $this->getDatabase();
|
||||||
$default = 'administrator/components/com_mokojoombackup/backups';
|
|
||||||
|
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select('COUNT(*)')
|
->select('COUNT(*)')
|
||||||
->from($db->quoteName('#__mokojoombackup_profiles'))
|
->from($db->quoteName('#__mokojoombackup_profiles'))
|
||||||
->where($db->quoteName('published') . ' = 1')
|
->where($db->quoteName('published') . ' = 1')
|
||||||
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default)
|
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::DEFAULT_RELATIVE)
|
||||||
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]')
|
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote(BackupDirectory::PLACEHOLDER)
|
||||||
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
||||||
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoJoomBackup\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\Database\DatabaseDriver;
|
use Joomla\Database\DatabaseDriver;
|
||||||
|
|
||||||
class ProfileTable extends Table
|
class ProfileTable extends Table
|
||||||
@@ -35,49 +36,17 @@ class ProfileTable extends Table
|
|||||||
|
|
||||||
private function protectWebAccessibleDir(string $dir): void
|
private function protectWebAccessibleDir(string $dir): void
|
||||||
{
|
{
|
||||||
// Resolve [DEFAULT_DIR] placeholder
|
$resolved = BackupDirectory::resolve($dir);
|
||||||
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
|
||||||
$resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $dir);
|
|
||||||
|
|
||||||
// Resolve relative paths from JPATH_ROOT
|
if (BackupDirectory::hasPlaceholders($resolved)) {
|
||||||
if ($resolved !== '' && $resolved[0] !== '/' && !preg_match('#^[A-Za-z]:[/\\\\]#', $resolved)) {
|
|
||||||
$resolved = JPATH_ROOT . '/' . $resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if unresolved placeholders remain
|
|
||||||
if (preg_match('/\[.+\]/', $resolved)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only protect directories under the web root
|
if (!BackupDirectory::isWebAccessible($resolved)) {
|
||||||
$jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
|
|
||||||
$realDir = realpath($resolved) ?: $resolved;
|
|
||||||
|
|
||||||
if (strpos($realDir, $jRoot) !== 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_dir($resolved)) {
|
BackupDirectory::ensureReady($resolved);
|
||||||
@mkdir($resolved, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_dir($resolved)) {
|
|
||||||
$htaccess = $resolved . '/.htaccess';
|
|
||||||
|
|
||||||
if (!is_file($htaccess)) {
|
|
||||||
if (@file_put_contents($htaccess, "# Apache 2.4+\n<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n# Apache 2.2\n<IfModule !mod_authz_core.c>\n Order deny,allow\n Deny from all\n</IfModule>\n") === false) {
|
|
||||||
error_log('MokoJoomBackup: Could not create .htaccess in: ' . $resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$index = $resolved . '/index.html';
|
|
||||||
|
|
||||||
if (!is_file($index)) {
|
|
||||||
if (@file_put_contents($index, '<!DOCTYPE html><title></title>') === false) {
|
|
||||||
error_log('MokoJoomBackup: Could not create index.html in: ' . $resolved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function check(): bool
|
public function check(): bool
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><title></title>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="actionlog" method="upgrade">
|
<extension type="plugin" group="actionlog" method="upgrade">
|
||||||
<name>plg_actionlog_mokojoombackup</name>
|
<name>plg_actionlog_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="console" method="upgrade">
|
<extension type="plugin" group="console" method="upgrade">
|
||||||
<name>plg_console_mokojoombackup</name>
|
<name>plg_console_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>plg_content_mokojoombackup</name>
|
<name>plg_content_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</version>
|
||||||
<creationDate>2026-06-04</creationDate>
|
<creationDate>2026-06-04</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<extension type="plugin" group="quickicon" method="upgrade">
|
<extension type="plugin" group="quickicon" method="upgrade">
|
||||||
<name>plg_quickicon_mokojoombackup</name>
|
<name>plg_quickicon_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>plg_system_mokojoombackup</name>
|
<name>plg_system_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="task" method="upgrade">
|
<extension type="plugin" group="task" method="upgrade">
|
||||||
<name>plg_task_mokojoombackup</name>
|
<name>plg_task_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>plg_webservices_mokojoombackup</name>
|
<name>plg_webservices_mokojoombackup</name>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</version>
|
||||||
<creationDate>2026-06-02</creationDate>
|
<creationDate>2026-06-02</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoJoomBackup</name>
|
<name>Package - MokoJoomBackup</name>
|
||||||
<packagename>mokojoombackup</packagename>
|
<packagename>mokojoombackup</packagename>
|
||||||
<version>01.05.00-rc</version>
|
<version>01.08.00</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>
|
||||||
|
|||||||
+129
-8
@@ -191,24 +191,145 @@ class Pkg_MokoJoomBackupInstallerScript
|
|||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
$db->execute();
|
$db->execute();
|
||||||
|
|
||||||
// Create default backup directory
|
// Create and protect default backup directory
|
||||||
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
|
||||||
|
|
||||||
if (!is_dir($backupDir)) {
|
if (!is_dir($backupDir)) {
|
||||||
mkdir($backupDir, 0755, true);
|
mkdir($backupDir, 0755, true);
|
||||||
|
|
||||||
// Protect backup directory with .htaccess
|
|
||||||
file_put_contents($backupDir . '/.htaccess', "# Apache 2.4+\n<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n# Apache 2.2\n<IfModule !mod_authz_core.c>\n Order deny,allow\n Deny from all\n</IfModule>\n");
|
|
||||||
file_put_contents($backupDir . '/index.html', '<!DOCTYPE html><title></title>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_dir($backupDir)) {
|
||||||
|
$htaccess = $backupDir . '/.htaccess';
|
||||||
|
|
||||||
|
if (!is_file($htaccess)) {
|
||||||
|
file_put_contents($htaccess, "# Apache 2.4+\n<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n# Apache 2.2\n<IfModule !mod_authz_core.c>\n Order deny,allow\n Deny from all\n</IfModule>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$index = $backupDir . '/index.html';
|
||||||
|
|
||||||
|
if (!is_file($index)) {
|
||||||
|
file_put_contents($index, '<!DOCTYPE html><title></title>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create default scheduled task — every 30 days, profile 1
|
||||||
|
$this->createDefaultScheduledTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'uninstall') {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync submenu icons in #__menu (Joomla doesn't update icons on upgrades)
|
// Sync submenu icons in #__menu (Joomla doesn't update icons on upgrades)
|
||||||
$this->syncMenuIcons();
|
$this->syncMenuIcons();
|
||||||
|
|
||||||
// Warn if no license key configured (skip on uninstall)
|
// Warn if no license key configured
|
||||||
if ($type !== 'uninstall') {
|
$this->warnMissingLicenseKey();
|
||||||
$this->warnMissingLicenseKey();
|
|
||||||
|
// Warn if any profile still uses the default backup directory
|
||||||
|
$this->warnDefaultBackupDir();
|
||||||
|
|
||||||
|
// Remind user to review backup profile settings
|
||||||
|
if ($type === 'install') {
|
||||||
|
$profileUrl = Route::_('index.php?option=com_mokojoombackup&view=profiles');
|
||||||
|
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
'<strong>Review Your Backup Settings</strong> — '
|
||||||
|
. 'A default backup profile has been created. Review the profile settings to configure '
|
||||||
|
. 'backup type, schedule, storage location, and notifications. '
|
||||||
|
. '<a href="' . $profileUrl . '" class="btn btn-sm btn-primary ms-2">Review Profiles</a>',
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function warnDefaultBackupDir(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__mokojoombackup_profiles'))
|
||||||
|
->where($db->quoteName('published') . ' = 1')
|
||||||
|
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote('administrator/components/com_mokojoombackup/backups')
|
||||||
|
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]')
|
||||||
|
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
||||||
|
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
||||||
|
$db->setQuery($query);
|
||||||
|
|
||||||
|
if ((int) $db->loadResult() > 0) {
|
||||||
|
$profileUrl = Route::_('index.php?option=com_mokojoombackup&view=profiles');
|
||||||
|
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
'<strong>Backup Directory Warning</strong> — '
|
||||||
|
. 'One or more profiles store backups in the default directory inside the web root. '
|
||||||
|
. 'For better security, configure a backup directory outside the web root. '
|
||||||
|
. '<a href="' . $profileUrl . '" class="btn btn-sm btn-warning ms-2">Edit Profiles</a>',
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('MokoJoomBackup: warnDefaultBackupDir() failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createDefaultScheduledTask(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Check if a MokoJoomBackup task already exists
|
||||||
|
$query = $db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from($db->quoteName('#__scheduler_tasks'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('mokojoombackup.run_profile'));
|
||||||
|
$db->setQuery($query);
|
||||||
|
|
||||||
|
if ((int) $db->loadResult() > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$task = (object) [
|
||||||
|
'title' => 'MokoJoomBackup — Monthly Full Backup',
|
||||||
|
'type' => 'mokojoombackup.run_profile',
|
||||||
|
'execution_rules' => json_encode([
|
||||||
|
'rule-type' => 'interval-days',
|
||||||
|
'interval-days' => '30',
|
||||||
|
'exec-day' => '1',
|
||||||
|
'exec-time' => '03:00:00',
|
||||||
|
]),
|
||||||
|
'cron_rules' => json_encode([
|
||||||
|
'type' => 'interval',
|
||||||
|
'exp' => 'P30D',
|
||||||
|
]),
|
||||||
|
'state' => 1,
|
||||||
|
'params' => json_encode([
|
||||||
|
'profile_id' => 1,
|
||||||
|
'individual_log' => true,
|
||||||
|
'log_file' => '',
|
||||||
|
'notifications' => [
|
||||||
|
'success_mail' => '0',
|
||||||
|
'failure_mail' => '1',
|
||||||
|
'notification_failure_groups' => ['8'],
|
||||||
|
'fatal_failure_mail' => '1',
|
||||||
|
'notification_fatal_groups' => ['8'],
|
||||||
|
'orphan_mail' => '0',
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'priority' => 0,
|
||||||
|
'ordering' => 0,
|
||||||
|
'cli_exclusive' => 0,
|
||||||
|
'note' => '',
|
||||||
|
'created' => $now,
|
||||||
|
'created_by' => Factory::getApplication()->getIdentity()->id ?? 0,
|
||||||
|
'next_execution' => date('Y-m-d 03:00:00', strtotime('+1 day')),
|
||||||
|
];
|
||||||
|
|
||||||
|
$db->insertObject('#__scheduler_tasks', $task);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('MokoJoomBackup: createDefaultScheduledTask() failed: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user