Refactor: extract BackupDirectory utility class #43

Merged
jmiller merged 2 commits from dev into main 2026-06-07 14:44:09 +00:00
22 changed files with 218 additions and 195 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<display-name>Package - MokoJoomBackup</display-name>
<org>MokoConsulting</org>
<description>Full-site backup and restore for Joomla — database, files, and configuration</description>
<version>01.06.00-dev</version>
<version>01.06.01-dev</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation
# VERSION: 01.06.00
# VERSION: 01.06.01
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+1 -1
View File
@@ -1,6 +1,6 @@
# MokoJoomBackup
<!-- VERSION: 01.06.00 -->
<!-- VERSION: 01.06.01 -->
Full-site backup and restore for Joomla — database, files, and configuration.
@@ -8,7 +8,7 @@
-->
<extension type="component" method="upgrade">
<name>com_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -18,6 +18,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Session\Session;
use Joomla\Component\MokoJoomBackup\Administrator\Engine\SteppedBackupEngine;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class AjaxController extends BaseController
{
@@ -196,7 +197,7 @@ class AjaxController extends BaseController
}
// 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 = '';
if (is_file($logPath)) {
@@ -239,17 +240,9 @@ class AjaxController extends BaseController
return;
}
// Resolve [DEFAULT_DIR] placeholder
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
$resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $rawPath);
$resolved = BackupDirectory::resolve($rawPath);
// Resolve relative paths from JPATH_ROOT
if ($resolved !== '' && $resolved[0] !== '/' && !preg_match('#^[A-Za-z]:[/\\\\]#', $resolved)) {
$resolved = JPATH_ROOT . '/' . $resolved;
}
// Skip check if unresolved placeholders remain
if (preg_match('/\[.+\]/', $resolved)) {
if (BackupDirectory::hasPlaceholders($resolved)) {
$this->sendJson([
'error' => false,
'exists' => null,
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
use Joomla\Event\Event;
class BackupEngine
@@ -56,24 +57,20 @@ class BackupEngine
}
// Read settings directly from profile columns
$excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? '');
$excludeFiles = $this->parseNewlineList($profile->exclude_files ?? '');
$excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
$excludeDirs = BackupDirectory::parseNewlineList($profile->exclude_dirs ?? '');
$excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? '');
$excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
// Resolve placeholders in directory and filename
$resolver = new PlaceholderResolver($profile);
$configuredDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
$this->backupDir = $this->resolveBackupDir($resolver->resolve($configuredDir));
$configuredDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
$this->backupDir = BackupDirectory::resolve($resolver->resolve($configuredDir));
if (!is_dir($this->backupDir)) {
if (!mkdir($this->backupDir, 0755, true)) {
return ['success' => false, 'message' => 'Cannot create backup directory: ' . $this->backupDir, 'record_id' => 0];
}
if (!BackupDirectory::ensureReady($this->backupDir)) {
return ['success' => false, 'message' => 'Cannot create backup directory: ' . $this->backupDir, 'record_id' => 0];
}
$this->protectBackupDir($this->backupDir);
// Create backup record
$now = date('Y-m-d H:i:s');
$tag = $resolver->getTag();
@@ -474,21 +471,6 @@ class BackupEngine
$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.
*/
@@ -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
{
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
@@ -16,6 +16,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Engine;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class PlaceholderResolver
{
@@ -38,7 +39,7 @@ class PlaceholderResolver
'[site_name]' => 'Joomla site name (sanitized)',
'[type]' => 'Backup type (full, database, files, differential)',
'[random]' => 'Random 6-character hex string',
'[DEFAULT_DIR]' => 'Default backup directory (administrator/components/com_mokojoombackup/backups)',
'[DEFAULT_DIR]' => 'Default backup directory',
];
private array $replacements;
@@ -75,7 +76,7 @@ class PlaceholderResolver
'[site_name]' => $this->sanitize($siteName ?: 'joomla'),
'[type]' => $profile->backup_type ?? 'full',
'[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;
use Joomla\CMS\Factory;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class SteppedBackupEngine
{
@@ -52,26 +53,22 @@ class SteppedBackupEngine
$session->backupType = $profile->backup_type;
// Parse profile settings
$session->excludeDirs = $this->parseNewlineList($profile->exclude_dirs ?? '');
$session->excludeFiles = $this->parseNewlineList($profile->exclude_files ?? '');
$session->excludeTables = $this->parseNewlineList($profile->exclude_tables ?? '');
$session->backupDir = $profile->backup_dir ?: '[DEFAULT_DIR]';
$session->excludeDirs = BackupDirectory::parseNewlineList($profile->exclude_dirs ?? '');
$session->excludeFiles = BackupDirectory::parseNewlineList($profile->exclude_files ?? '');
$session->excludeTables = BackupDirectory::parseNewlineList($profile->exclude_tables ?? '');
$session->backupDir = $profile->backup_dir ?: BackupDirectory::PLACEHOLDER;
$session->remoteStorage = $profile->remote_storage ?? 'none';
$session->includeMokoRestore = (bool) ($profile->include_mokorestore ?? false);
$session->remoteKeepLocal = (bool) ($profile->remote_keep_local ?? true);
// Resolve placeholders in directory and filename
$resolver = new PlaceholderResolver($profile);
$backupDir = $this->resolveBackupDir($resolver->resolve($session->backupDir));
$backupDir = BackupDirectory::resolve($resolver->resolve($session->backupDir));
if (!is_dir($backupDir)) {
if (!mkdir($backupDir, 0755, true)) {
return ['error' => true, 'message' => 'Cannot create backup directory: ' . $backupDir];
}
if (!BackupDirectory::ensureReady($backupDir)) {
return ['error' => true, 'message' => 'Cannot create backup directory: ' . $backupDir];
}
$this->protectBackupDir($backupDir);
$now = date('Y-m-d H:i:s');
$tag = $resolver->getTag();
$nameFormat = $profile->archive_name_format ?? '[host]_[datetime]_profile[profile_id]';
@@ -422,7 +419,7 @@ class SteppedBackupEngine
$logContent = implode("\n", $session->log);
// 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) {
error_log('MokoJoomBackup: Could not write log file: ' . $logPath);
}
@@ -554,47 +551,4 @@ class SteppedBackupEngine
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\Form\FormField;
use Joomla\CMS\Language\Text;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class FolderPickerField extends FormField
{
@@ -49,7 +50,7 @@ class FolderPickerField extends FormField
$sanitizedSiteName = preg_replace('/[^a-zA-Z0-9._-]/', '', str_replace(' ', '-', trim($siteName)));
$placeholders = [
'[DEFAULT_DIR]' => JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups',
'[DEFAULT_DIR]' => BackupDirectory::getDefaultAbsolute(),
'[host]' => $hostname,
'[site_name]' => $sanitizedSiteName ?: 'joomla',
'[profile_id]' => '1',
@@ -14,6 +14,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
class DashboardModel extends BaseDatabaseModel
{
@@ -122,11 +123,9 @@ class DashboardModel extends BaseDatabaseModel
'detail' => $aesSupport ? 'Available' : 'Requires libzip 1.2.0+',
];
// Backup directory writable — check the default path
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
$backupDir = $defaultDir;
// Backup directory writable — check the first published profile's dir
$backupDir = BackupDirectory::getDefaultAbsolute();
// If profiles use a custom directory, check that instead
$db2 = $this->getDatabase();
$qDir = $db2->getQuery(true)
->select($db2->quoteName('backup_dir'))
@@ -138,16 +137,10 @@ class DashboardModel extends BaseDatabaseModel
$profileDir = $db2->loadResult();
if ($profileDir) {
// Absolute paths used as-is, relative resolved from JPATH_ROOT
if ($profileDir[0] === '/' || preg_match('#^[A-Za-z]:[/\\\\]#', $profileDir)) {
$backupDir = rtrim($profileDir, '/\\');
} else {
$backupDir = JPATH_ROOT . '/' . $profileDir;
}
$backupDir = BackupDirectory::resolve($profileDir);
}
// Skip filesystem check if path contains placeholders (resolved at backup time)
if (preg_match('/\[.+\]/', $backupDir)) {
if (BackupDirectory::hasPlaceholders($backupDir)) {
$checks[] = (object) [
'label' => 'Backup Directory',
'status' => true,
@@ -182,14 +175,12 @@ class DashboardModel extends BaseDatabaseModel
public function isUsingDefaultBackupDir(): bool
{
$db = $this->getDatabase();
$default = 'administrator/components/com_mokojoombackup/backups';
$query = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokojoombackup_profiles'))
->where($db->quoteName('published') . ' = 1')
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote($default)
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]')
->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);
@@ -13,6 +13,7 @@ namespace Joomla\Component\MokoJoomBackup\Administrator\Table;
defined('_JEXEC') or die;
use Joomla\CMS\Table\Table;
use Joomla\Component\MokoJoomBackup\Administrator\Utility\BackupDirectory;
use Joomla\Database\DatabaseDriver;
class ProfileTable extends Table
@@ -35,49 +36,17 @@ class ProfileTable extends Table
private function protectWebAccessibleDir(string $dir): void
{
// Resolve [DEFAULT_DIR] placeholder
$defaultDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
$resolved = str_replace('[DEFAULT_DIR]', $defaultDir, $dir);
$resolved = BackupDirectory::resolve($dir);
// Resolve relative paths from JPATH_ROOT
if ($resolved !== '' && $resolved[0] !== '/' && !preg_match('#^[A-Za-z]:[/\\\\]#', $resolved)) {
$resolved = JPATH_ROOT . '/' . $resolved;
}
// Skip if unresolved placeholders remain
if (preg_match('/\[.+\]/', $resolved)) {
if (BackupDirectory::hasPlaceholders($resolved)) {
return;
}
// Only protect directories under the web root
$jRoot = realpath(JPATH_ROOT) ?: JPATH_ROOT;
$realDir = realpath($resolved) ?: $resolved;
if (strpos($realDir, $jRoot) !== 0) {
if (!BackupDirectory::isWebAccessible($resolved)) {
return;
}
if (!is_dir($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);
}
}
}
BackupDirectory::ensureReady($resolved);
}
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">
<name>plg_actionlog_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="console" method="upgrade">
<name>plg_console_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="content" method="upgrade">
<name>plg_content_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-04</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<extension type="plugin" group="quickicon" method="upgrade">
<name>plg_quickicon_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="system" method="upgrade">
<name>plg_system_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="task" method="upgrade">
<name>plg_task_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
@@ -8,7 +8,7 @@
-->
<extension type="plugin" group="webservices" method="upgrade">
<name>plg_webservices_mokojoombackup</name>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+1 -1
View File
@@ -8,7 +8,7 @@
<extension type="package" method="upgrade">
<name>Package - MokoJoomBackup</name>
<packagename>mokojoombackup</packagename>
<version>01.06.00</version>
<version>01.06.01-dev</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
+14 -4
View File
@@ -191,15 +191,25 @@ class Pkg_MokoJoomBackupInstallerScript
$db->setQuery($query);
$db->execute();
// Create default backup directory
// Create and protect default backup directory
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokojoombackup/backups';
if (!is_dir($backupDir)) {
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>');
}
}
}