feat: MokoRestore post-restore resets + per-table conflict resolution #133
@@ -1,66 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/mokocli/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||
fi
|
||||
if [ -d "/opt/mokocli/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
+534
-534
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -1,9 +1,12 @@
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
|
||||
## [01.39.00] --- 2026-06-23
|
||||
|
||||
## [01.39.00] --- 2026-06-23
|
||||
### Added
|
||||
- MokoRestore: post-restore reset options — passwords, hits, versions, sessions, cache (#131)
|
||||
- MokoRestore: per-table conflict resolution — replace, skip, merge, data-only per table (#132)
|
||||
- MokoRestore: preset buttons — "All Replace", "All Skip", "Everything except users"
|
||||
- MokoRestore: auto-detect sanitized passwords and prompt for reset
|
||||
- Data sanitization: passwords, emails, sessions in backup profile settings (#129)
|
||||
|
||||
## [01.38.05] --- 2026-06-23
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -376,16 +376,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
function handleAction(string $action, array $data): array
|
||||
{
|
||||
return match ($action) {
|
||||
'preflight' => actionPreflight(),
|
||||
'extract' => actionExtract($data),
|
||||
'testdb' => actionTestDb($data),
|
||||
'database' => actionDatabase($data),
|
||||
'config' => actionConfig($data),
|
||||
'listAdmins' => actionListAdmins($data),
|
||||
'resetAdmin' => actionResetAdmin($data),
|
||||
'provision' => actionProvision($data),
|
||||
'cleanup' => actionCleanup(),
|
||||
default => ['success' => false, 'message' => 'Unknown action: ' . $action],
|
||||
'preflight' => actionPreflight(),
|
||||
'extract' => actionExtract($data),
|
||||
'scanTables' => actionScanTables(),
|
||||
'testdb' => actionTestDb($data),
|
||||
'database' => actionDatabase($data),
|
||||
'config' => actionConfig($data),
|
||||
'listAdmins' => actionListAdmins($data),
|
||||
'resetAdmin' => actionResetAdmin($data),
|
||||
'postRestore' => actionPostRestore($data),
|
||||
'detectSanitized' => detectSanitizedPasswords($data),
|
||||
'provision' => actionProvision($data),
|
||||
'cleanup' => actionCleanup(),
|
||||
default => ['success' => false, 'message' => 'Unknown action: ' . $action],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -551,6 +554,65 @@ function actionExtract(array $data): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse database.sql and extract the list of table names.
|
||||
* Returns table names using the abstract #__ prefix so the UI
|
||||
* can display them before the user's target prefix is known.
|
||||
*/
|
||||
function actionScanTables(): array
|
||||
{
|
||||
$sqlFile = RESTORE_DIR . '/database.sql';
|
||||
|
||||
if (!is_file($sqlFile)) {
|
||||
return ['success' => true, 'tables' => [], 'message' => 'No database.sql found'];
|
||||
}
|
||||
|
||||
$sql = file_get_contents($sqlFile);
|
||||
$tables = [];
|
||||
|
||||
// Match DROP TABLE IF EXISTS `#__tablename` or CREATE TABLE ... `#__tablename`
|
||||
if (preg_match_all('/(?:DROP\s+TABLE\s+IF\s+EXISTS|CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?)\s+`([^`]+)`/i', $sql, $matches)) {
|
||||
foreach ($matches[1] as $name) {
|
||||
if (!in_array($name, $tables, true)) {
|
||||
$tables[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort alphabetically for easier scanning
|
||||
sort($tables, SORT_STRING);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'tables' => $tables,
|
||||
'count' => count($tables),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which table a SQL statement belongs to.
|
||||
* Returns the table name (with the prefix already applied) or empty string.
|
||||
*/
|
||||
function getStatementTable(string $stmt): string
|
||||
{
|
||||
// DROP TABLE IF EXISTS `prefix_tablename`
|
||||
if (preg_match('/^DROP\s+TABLE\s+IF\s+EXISTS\s+`([^`]+)`/i', $stmt, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
// CREATE TABLE `prefix_tablename` or CREATE TABLE IF NOT EXISTS `prefix_tablename`
|
||||
if (preg_match('/^CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`([^`]+)`/i', $stmt, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
// INSERT INTO `prefix_tablename`
|
||||
if (preg_match('/^INSERT\s+INTO\s+`([^`]+)`/i', $stmt, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function actionTestDb(array $data): array
|
||||
{
|
||||
$host = $data['db_host'] ?? 'localhost';
|
||||
@@ -608,10 +670,27 @@ function actionDatabase(array $data): array
|
||||
// Replace abstract #__ prefix with the user's target prefix
|
||||
$sql = str_replace('#__', $prefix, $sql);
|
||||
|
||||
// Decode per-table conflict resolution selections
|
||||
// Keys are abstract table names (#__xxx), values are: replace|skip|merge|dataonly
|
||||
$tableResolutions = [];
|
||||
|
||||
if (!empty($data['table_resolutions'])) {
|
||||
$decoded = json_decode($data['table_resolutions'], true);
|
||||
|
||||
if (is_array($decoded)) {
|
||||
// Remap from abstract #__ names to the real prefix
|
||||
foreach ($decoded as $abstractName => $mode) {
|
||||
$realName = str_replace('#__', $prefix, $abstractName);
|
||||
$tableResolutions[$realName] = $mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parts = explode(";\n", $sql);
|
||||
$statements = 0;
|
||||
$errors = 0;
|
||||
$errorList = [];
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$part = trim($part);
|
||||
@@ -620,6 +699,39 @@ function actionDatabase(array $data): array
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine which table this statement belongs to
|
||||
$table = getStatementTable($part);
|
||||
$mode = $tableResolutions[$table] ?? 'replace';
|
||||
|
||||
// Apply conflict resolution per table
|
||||
if ($mode === 'skip') {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$isDrop = (bool) preg_match('/^DROP\s+TABLE/i', $part);
|
||||
$isCreate = (bool) preg_match('/^CREATE\s+TABLE/i', $part);
|
||||
$isInsert = (bool) preg_match('/^INSERT\s+INTO/i', $part);
|
||||
|
||||
if ($mode === 'merge') {
|
||||
// Skip DROP and CREATE; convert INSERT INTO to INSERT IGNORE INTO
|
||||
if ($isDrop || $isCreate) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isInsert) {
|
||||
$part = preg_replace('/^INSERT\s+INTO/i', 'INSERT IGNORE INTO', $part);
|
||||
}
|
||||
} elseif ($mode === 'dataonly') {
|
||||
// Skip DROP and CREATE; execute INSERT as-is
|
||||
if ($isDrop || $isCreate) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// mode === 'replace' => execute everything as-is (default)
|
||||
|
||||
try {
|
||||
$pdo->exec($part);
|
||||
$statements++;
|
||||
@@ -634,11 +746,22 @@ function actionDatabase(array $data): array
|
||||
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
|
||||
|
||||
$msg = "Executed {$statements} statements";
|
||||
|
||||
if ($skipped > 0) {
|
||||
$msg .= " ({$skipped} skipped)";
|
||||
}
|
||||
|
||||
if ($errors > 0) {
|
||||
$msg .= " ({$errors} warnings)";
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => ($statements > 0 || $errors === 0),
|
||||
'message' => "Executed {$statements} statements" . ($errors ? " ({$errors} warnings)" : ''),
|
||||
'message' => $msg,
|
||||
'statements' => $statements,
|
||||
'errors' => $errors,
|
||||
'skipped' => $skipped,
|
||||
'errorList' => $errorList,
|
||||
];
|
||||
}
|
||||
@@ -989,6 +1112,127 @@ function actionResetAdmin(array $data): array
|
||||
return ['success' => true, 'message' => 'Admin password updated successfully'];
|
||||
}
|
||||
|
||||
function actionPostRestore(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = getValidatedPrefix($data);
|
||||
$tasks = json_decode($data['tasks'] ?? '[]', true) ?: [];
|
||||
$results = [];
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
try {
|
||||
switch ($task) {
|
||||
case 'reset_passwords':
|
||||
// Set all user passwords to a known temporary hash ("changeme"),
|
||||
// clear activation tokens, and force password reset on next login.
|
||||
$tempHash = password_hash('changeme', PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE {$prefix}users SET password = ?, activation = '', requireReset = 1"
|
||||
);
|
||||
$stmt->execute([$tempHash]);
|
||||
$affected = $stmt->rowCount();
|
||||
$results[] = "All {$affected} user password(s) reset to temporary password (changeme) with forced reset";
|
||||
break;
|
||||
|
||||
case 'reset_hits':
|
||||
$pdo->exec("UPDATE {$prefix}content SET hits = 0");
|
||||
$results[] = 'Content hits reset to 0';
|
||||
break;
|
||||
|
||||
case 'clear_versions':
|
||||
try {
|
||||
$pdo->exec("TRUNCATE TABLE {$prefix}history");
|
||||
$results[] = 'Content version history cleared';
|
||||
} catch (PDOException $e) {
|
||||
$results[] = 'Version history: table not found (skipped)';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'clear_sessions':
|
||||
$pdo->exec("TRUNCATE TABLE {$prefix}session");
|
||||
$results[] = 'Sessions cleared';
|
||||
break;
|
||||
|
||||
case 'clear_cache':
|
||||
// Clear Joomla cache tables
|
||||
foreach (['cache', 'cache_extension'] as $tbl) {
|
||||
try {
|
||||
$pdo->exec("TRUNCATE TABLE {$prefix}{$tbl}");
|
||||
} catch (PDOException $e) {
|
||||
// Table may not exist
|
||||
}
|
||||
}
|
||||
|
||||
// Delete files in cache/ directory
|
||||
$cacheDir = RESTORE_DIR . '/cache';
|
||||
$cacheCount = 0;
|
||||
|
||||
if (is_dir($cacheDir)) {
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($cacheDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($it as $item) {
|
||||
if ($item->isFile()) {
|
||||
@unlink($item->getPathname());
|
||||
$cacheCount++;
|
||||
} elseif ($item->isDir()) {
|
||||
@rmdir($item->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also clear administrator/cache/
|
||||
$adminCacheDir = RESTORE_DIR . '/administrator/cache';
|
||||
|
||||
if (is_dir($adminCacheDir)) {
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($adminCacheDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($it as $item) {
|
||||
if ($item->isFile()) {
|
||||
@unlink($item->getPathname());
|
||||
$cacheCount++;
|
||||
} elseif ($item->isDir()) {
|
||||
@rmdir($item->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results[] = "Cache tables cleared, {$cacheCount} cache file(s) removed";
|
||||
break;
|
||||
|
||||
default:
|
||||
$results[] = "Unknown task: {$task}";
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$results[] = "Error ({$task}): " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'results' => $results, 'message' => count($results) . ' post-restore task(s) completed'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the database contains sanitized sentinel password hashes.
|
||||
* Returns true if any user has the MokoSuiteBackup sanitized placeholder hash.
|
||||
*/
|
||||
function detectSanitizedPasswords(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
$prefix = getValidatedPrefix($data);
|
||||
$sentinel = '$2y$10$SANITIZED.MOKOSUITEBACKUP.INVALID.HASH.DO.NOT.USE.000000';
|
||||
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM {$prefix}users WHERE password = ?");
|
||||
$stmt->execute([$sentinel]);
|
||||
$count = (int) $stmt->fetchColumn();
|
||||
|
||||
return ['success' => true, 'detected' => $count > 0, 'count' => $count];
|
||||
}
|
||||
|
||||
function actionProvision(array $data): array
|
||||
{
|
||||
$pdo = getDbConnection($data);
|
||||
@@ -1233,11 +1477,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<div class="mr-steps" id="stepBar">
|
||||
<div class="mr-step active" data-step="1"><span class="mr-num">1</span>Checks</div>
|
||||
<div class="mr-step" data-step="2"><span class="mr-num">2</span>Extract</div>
|
||||
<div class="mr-step" data-step="3"><span class="mr-num">3</span>Database</div>
|
||||
<div class="mr-step" data-step="4"><span class="mr-num">4</span>Configuration</div>
|
||||
<div class="mr-step" data-step="5"><span class="mr-num">5</span>Admin</div>
|
||||
<div class="mr-step" data-step="6"><span class="mr-num">6</span>Provisioning</div>
|
||||
<div class="mr-step" data-step="7"><span class="mr-num">7</span>Complete</div>
|
||||
<div class="mr-step" data-step="3"><span class="mr-num">3</span>Tables</div>
|
||||
<div class="mr-step" data-step="4"><span class="mr-num">4</span>Database</div>
|
||||
<div class="mr-step" data-step="5"><span class="mr-num">5</span>Configuration</div>
|
||||
<div class="mr-step" data-step="6"><span class="mr-num">6</span>Admin</div>
|
||||
<div class="mr-step" data-step="7"><span class="mr-num">7</span>Post-Restore</div>
|
||||
<div class="mr-step" data-step="8"><span class="mr-num">8</span>Provisioning</div>
|
||||
<div class="mr-step" data-step="9"><span class="mr-num">9</span>Complete</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 0: Security Verification -->
|
||||
@@ -1292,8 +1538,36 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Database -->
|
||||
<!-- Step 3: Table Conflict Resolution -->
|
||||
<div class="mr-panel" id="panel3">
|
||||
<h2>Table Conflict Resolution</h2>
|
||||
<p class="mr-desc">Choose how each table should be handled during database import. This lets you protect specific tables (e.g. users) from being overwritten.</p>
|
||||
<div style="margin-bottom:1rem;display:flex;gap:0.5rem;flex-wrap:wrap">
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="setAllTableMode('replace')">All Replace</button>
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="setAllTableMode('skip')">All Skip</button>
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="setAllTableMode('merge')">All Merge</button>
|
||||
<button class="mr-btn mr-btn-outline" style="font-size:0.8rem;padding:0.4rem 0.8rem" onclick="presetExceptUsers()">Everything except users</button>
|
||||
</div>
|
||||
<div class="mr-alert mr-alert-info" style="font-size:0.85rem">
|
||||
<strong>Modes:</strong>
|
||||
<strong>Replace</strong> = drop + recreate + insert (default).
|
||||
<strong>Skip</strong> = ignore entirely.
|
||||
<strong>Merge</strong> = keep existing table, INSERT IGNORE new rows.
|
||||
<strong>Data Only</strong> = keep schema, INSERT data as-is (assumes matching structure).
|
||||
</div>
|
||||
<div id="tableResolutionList" style="max-height:400px;overflow-y:auto;border:1px solid #e2e8f0;border-radius:8px;margin-top:1rem">
|
||||
<div style="padding:1rem;color:#94a3b8;text-align:center">Scanning tables...</div>
|
||||
</div>
|
||||
<input type="hidden" id="tableResolutions" value="{}">
|
||||
<div class="mr-status" id="tableScanStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(2)">Back</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnTablesContinue" onclick="goStep(4)">Continue to Database</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Database -->
|
||||
<div class="mr-panel" id="panel4">
|
||||
<h2>Database Configuration</h2>
|
||||
<p class="mr-desc">Enter the database credentials for this server. The SQL dump will be imported.</p>
|
||||
<div class="mr-row">
|
||||
@@ -1312,13 +1586,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
<div class="mr-progress"><div class="mr-progress-bar" id="dbProgress" style="width:0%"></div></div>
|
||||
<div class="mr-status" id="dbStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(2)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(3)">Back</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnImport" onclick="runDatabase()">Import Database</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Site Configuration -->
|
||||
<div class="mr-panel" id="panel4">
|
||||
<!-- Step 5: Site Configuration -->
|
||||
<div class="mr-panel" id="panel5">
|
||||
<h2>Site Configuration</h2>
|
||||
<p class="mr-desc">Configure your site settings. Credentials were removed from the backup for security — enter the correct values for this server.</p>
|
||||
|
||||
@@ -1361,13 +1635,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</div>
|
||||
<div class="mr-status" id="configStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(3)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(4)">Back</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnConfig" onclick="runConfig()">Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Admin Password Reset -->
|
||||
<div class="mr-panel" id="panel5">
|
||||
<!-- Step 6: Admin Password Reset -->
|
||||
<div class="mr-panel" id="panel6">
|
||||
<h2>Super Admin Password</h2>
|
||||
<p class="mr-desc">Reset the password for a super administrator account. This is optional but recommended after restoring to a new server.</p>
|
||||
<div class="mr-field">
|
||||
@@ -1380,16 +1654,40 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</div>
|
||||
<div class="mr-status" id="adminStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(4)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(5)">Back</button>
|
||||
<div>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(6)">Skip</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(7)">Skip</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnResetAdmin" onclick="runResetAdmin()">Reset Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 6: Client Provisioning -->
|
||||
<div class="mr-panel" id="panel6">
|
||||
<!-- Step 7: Post-Restore Actions -->
|
||||
<div class="mr-panel" id="panel7">
|
||||
<h2>Post-Restore Actions</h2>
|
||||
<p class="mr-desc">Optional reset tasks to clean up the restored database. These are especially useful when restoring a sanitized backup.</p>
|
||||
<div class="mr-alert mr-alert-warn" id="postRestoreSanitizedWarn" style="display:none">
|
||||
<strong>Sanitized passwords detected!</strong> This backup contains placeholder password hashes that will prevent all users from logging in. The "Reset all user passwords" option below is strongly recommended.
|
||||
</div>
|
||||
<ul class="mr-provision-list" id="postRestoreList">
|
||||
<li><input type="checkbox" class="post-restore-task" id="prResetPasswords" value="reset_passwords"><span>Reset all user passwords</span><span class="mr-provision-desc">Set to "changeme" and force reset on next login</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="reset_hits"><span>Reset content hits</span><span class="mr-provision-desc">Set all article hit counters to 0</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="clear_versions"><span>Clear version history</span><span class="mr-provision-desc">Truncate the content version history table</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="clear_sessions" checked><span>Clear sessions</span><span class="mr-provision-desc">Remove all active user sessions</span></li>
|
||||
<li><input type="checkbox" class="post-restore-task" value="clear_cache" checked><span>Clear cache</span><span class="mr-provision-desc">Truncate cache tables and delete cache files</span></li>
|
||||
</ul>
|
||||
<div class="mr-status" id="postRestoreStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(6)">Back</button>
|
||||
<div>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(8)">Skip</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnPostRestore" onclick="runPostRestore()">Run Selected Tasks</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 8: Client Provisioning -->
|
||||
<div class="mr-panel" id="panel8">
|
||||
<h2>Client Provisioning</h2>
|
||||
<p class="mr-desc">Optional cleanup tasks for deploying this backup as a new client site. Check the tasks you want to run.</p>
|
||||
<ul class="mr-provision-list">
|
||||
@@ -1404,16 +1702,16 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica N
|
||||
</ul>
|
||||
<div class="mr-status" id="provisionStatus"></div>
|
||||
<div class="mr-actions">
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(5)">Back</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(7)">Back</button>
|
||||
<div>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(7)">Skip</button>
|
||||
<button class="mr-btn mr-btn-outline" onclick="goStep(9)">Skip</button>
|
||||
<button class="mr-btn mr-btn-primary" id="btnProvision" onclick="runProvision()">Run Selected Tasks</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 7: Complete -->
|
||||
<div class="mr-panel" id="panel7">
|
||||
<!-- Step 9: Complete -->
|
||||
<div class="mr-panel" id="panel9">
|
||||
<h2>Installation Complete</h2>
|
||||
<p class="mr-desc">Your Joomla site has been restored and configured.</p>
|
||||
<div class="mr-alert mr-alert-success">
|
||||
@@ -1484,7 +1782,9 @@ function goStep(n) {
|
||||
else if (sn < n) s.classList.add('done');
|
||||
});
|
||||
|
||||
if (n === 5) loadAdmins();
|
||||
if (n === 3) scanTables();
|
||||
if (n === 6) loadAdmins();
|
||||
if (n === 7) checkSanitizedPasswords();
|
||||
}
|
||||
|
||||
function setStatus(id, msg, type) {
|
||||
@@ -1621,7 +1921,111 @@ async function runExtract() {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3
|
||||
// Step 3: Table Conflict Resolution
|
||||
let tableList = [];
|
||||
|
||||
async function scanTables() {
|
||||
const container = document.getElementById('tableResolutionList');
|
||||
|
||||
// Only scan once
|
||||
if (tableList.length > 0) return;
|
||||
|
||||
log('Scanning database.sql for table names...');
|
||||
const r = await post('scanTables');
|
||||
|
||||
if (!r.success || !r.tables || r.tables.length === 0) {
|
||||
container.innerHTML = '<div style="padding:1rem;color:#94a3b8;text-align:center">No tables found in database.sql (or file not present). You can skip this step.</div>';
|
||||
setStatus('tableScanStatus', r.tables ? 'No tables found' : (r.message || 'Scan failed'), r.success ? '' : 'error');
|
||||
log(r.message || 'No tables found');
|
||||
return;
|
||||
}
|
||||
|
||||
tableList = r.tables;
|
||||
log('Found ' + r.count + ' tables');
|
||||
setStatus('tableScanStatus', 'Found ' + r.count + ' tables', 'success');
|
||||
|
||||
renderTableList();
|
||||
}
|
||||
|
||||
function renderTableList() {
|
||||
const container = document.getElementById('tableResolutionList');
|
||||
container.innerHTML = '';
|
||||
|
||||
var resolutions = {};
|
||||
|
||||
tableList.forEach(function(name) {
|
||||
var row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:0.5rem 0.75rem;border-bottom:1px solid #f1f5f9;font-size:0.85rem;';
|
||||
|
||||
var label = document.createElement('span');
|
||||
label.style.cssText = 'font-family:monospace;color:#334155;word-break:break-all;flex:1;margin-right:0.75rem;';
|
||||
label.textContent = name;
|
||||
|
||||
var sel = document.createElement('select');
|
||||
sel.dataset.table = name;
|
||||
sel.className = 'table-mode-select';
|
||||
sel.style.cssText = 'padding:0.3rem 0.5rem;border:1px solid #d1d5db;border-radius:4px;font-size:0.8rem;min-width:120px;background:#fff;';
|
||||
|
||||
var modes = [
|
||||
['replace', 'Replace'],
|
||||
['skip', 'Skip'],
|
||||
['merge', 'Merge'],
|
||||
['dataonly', 'Data Only']
|
||||
];
|
||||
|
||||
modes.forEach(function(m) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = m[0];
|
||||
opt.textContent = m[1];
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
|
||||
sel.addEventListener('change', updateTableResolutions);
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(sel);
|
||||
container.appendChild(row);
|
||||
|
||||
resolutions[name] = 'replace';
|
||||
});
|
||||
|
||||
document.getElementById('tableResolutions').value = JSON.stringify(resolutions);
|
||||
}
|
||||
|
||||
function updateTableResolutions() {
|
||||
var resolutions = {};
|
||||
document.querySelectorAll('.table-mode-select').forEach(function(sel) {
|
||||
resolutions[sel.dataset.table] = sel.value;
|
||||
});
|
||||
document.getElementById('tableResolutions').value = JSON.stringify(resolutions);
|
||||
}
|
||||
|
||||
function setAllTableMode(mode) {
|
||||
document.querySelectorAll('.table-mode-select').forEach(function(sel) {
|
||||
sel.value = mode;
|
||||
});
|
||||
updateTableResolutions();
|
||||
log('Set all tables to: ' + mode);
|
||||
}
|
||||
|
||||
function presetExceptUsers() {
|
||||
var userTables = ['#__users', '#__user_usergroup_map', '#__user_profiles'];
|
||||
|
||||
document.querySelectorAll('.table-mode-select').forEach(function(sel) {
|
||||
var tableName = sel.dataset.table;
|
||||
|
||||
if (userTables.indexOf(tableName) !== -1) {
|
||||
sel.value = 'skip';
|
||||
} else {
|
||||
sel.value = 'replace';
|
||||
}
|
||||
});
|
||||
|
||||
updateTableResolutions();
|
||||
log('Preset: Replace all except user tables (skipped)');
|
||||
}
|
||||
|
||||
// Step 4
|
||||
function getDbParams() {
|
||||
return {
|
||||
db_host: document.getElementById('dbHost').value,
|
||||
@@ -1647,7 +2051,12 @@ async function runDatabase() {
|
||||
log('Importing database...');
|
||||
|
||||
dbConfig = getDbParams();
|
||||
const r = await post('database', dbConfig);
|
||||
// Include table conflict resolution selections
|
||||
var tableRes = document.getElementById('tableResolutions');
|
||||
var dbParams = Object.assign({}, dbConfig, {
|
||||
table_resolutions: tableRes ? tableRes.value : '{}'
|
||||
});
|
||||
const r = await post('database', dbParams);
|
||||
|
||||
document.getElementById('dbProgress').style.width = '100%';
|
||||
setBtnLoading(btn, false);
|
||||
@@ -1655,17 +2064,20 @@ async function runDatabase() {
|
||||
if (r.success) {
|
||||
setStatus('dbStatus', r.message, 'success');
|
||||
log(r.message);
|
||||
if (r.skipped && r.skipped > 0) {
|
||||
log(' Skipped ' + r.skipped + ' statements due to conflict resolution');
|
||||
}
|
||||
if (r.errorList && r.errorList.length > 0) {
|
||||
r.errorList.forEach(function(e) { log(' Warning: ' + e); });
|
||||
}
|
||||
setTimeout(function() { goStep(4); }, 500);
|
||||
setTimeout(function() { goStep(5); }, 500);
|
||||
} else {
|
||||
setStatus('dbStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4
|
||||
// Step 5
|
||||
async function runConfig() {
|
||||
const btn = document.getElementById('btnConfig');
|
||||
setBtnLoading(btn, true);
|
||||
@@ -1686,14 +2098,14 @@ async function runConfig() {
|
||||
if (r.success) {
|
||||
setStatus('configStatus', r.message, 'success');
|
||||
log(r.message);
|
||||
setTimeout(function() { goStep(5); }, 500);
|
||||
setTimeout(function() { goStep(6); }, 500);
|
||||
} else {
|
||||
setStatus('configStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5
|
||||
// Step 6
|
||||
async function loadAdmins() {
|
||||
const sel = document.getElementById('adminSelect');
|
||||
while (sel.firstChild) sel.removeChild(sel.firstChild);
|
||||
@@ -1738,20 +2150,65 @@ async function runResetAdmin() {
|
||||
if (r.success) {
|
||||
setStatus('adminStatus', r.message, 'success');
|
||||
log(r.message);
|
||||
setTimeout(function() { goStep(6); }, 500);
|
||||
setTimeout(function() { goStep(7); }, 500);
|
||||
} else {
|
||||
setStatus('adminStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6
|
||||
// Step 7: Post-Restore
|
||||
async function checkSanitizedPasswords() {
|
||||
log('Checking for sanitized password hashes...');
|
||||
|
||||
try {
|
||||
const r = await post('detectSanitized', dbConfig);
|
||||
|
||||
if (r.success && r.detected) {
|
||||
document.getElementById('postRestoreSanitizedWarn').style.display = '';
|
||||
document.getElementById('prResetPasswords').checked = true;
|
||||
log('WARNING: ' + r.count + ' user(s) have sanitized placeholder passwords');
|
||||
} else {
|
||||
document.getElementById('postRestoreSanitizedWarn').style.display = 'none';
|
||||
log('No sanitized passwords detected');
|
||||
}
|
||||
} catch (e) {
|
||||
log('Could not check for sanitized passwords: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function runPostRestore() {
|
||||
const btn = document.getElementById('btnPostRestore');
|
||||
const tasks = [];
|
||||
document.querySelectorAll('.post-restore-task:checked').forEach(function(cb) { tasks.push(cb.value); });
|
||||
|
||||
if (tasks.length === 0) { goStep(8); return; }
|
||||
|
||||
setBtnLoading(btn, true);
|
||||
log('Running ' + tasks.length + ' post-restore task(s)...');
|
||||
|
||||
const params = Object.assign({}, dbConfig, { tasks: JSON.stringify(tasks) });
|
||||
const r = await post('postRestore', params);
|
||||
|
||||
setBtnLoading(btn, false);
|
||||
|
||||
if (r.success) {
|
||||
setStatus('postRestoreStatus', r.message, 'success');
|
||||
r.results.forEach(function(msg) { log(' ' + msg); });
|
||||
setTimeout(function() { goStep(8); }, 500);
|
||||
} else {
|
||||
setStatus('postRestoreStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8
|
||||
async function runProvision() {
|
||||
const btn = document.getElementById('btnProvision');
|
||||
const tasks = [];
|
||||
document.querySelectorAll('.prov-task:checked').forEach(function(cb) { tasks.push(cb.value); });
|
||||
|
||||
if (tasks.length === 0) { goStep(7); return; }
|
||||
if (tasks.length === 0) { goStep(9); return; }
|
||||
|
||||
setBtnLoading(btn, true);
|
||||
log('Running ' + tasks.length + ' provisioning tasks...');
|
||||
@@ -1764,14 +2221,14 @@ async function runProvision() {
|
||||
if (r.success) {
|
||||
setStatus('provisionStatus', r.message, 'success');
|
||||
r.results.forEach(function(msg) { log(' ' + msg); });
|
||||
setTimeout(function() { goStep(7); }, 500);
|
||||
setTimeout(function() { goStep(9); }, 500);
|
||||
} else {
|
||||
setStatus('provisionStatus', r.message, 'error');
|
||||
log('FAILED: ' + r.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7
|
||||
// Step 9
|
||||
async function runCleanup() {
|
||||
log('Cleaning up restore files...');
|
||||
const r = await post('cleanup');
|
||||
|
||||
@@ -114,6 +114,7 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
if (!empty($key)) {
|
||||
$this->savedDownloadKey = $key;
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: Could not save download key: ' . $e->getMessage());
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
@@ -121,6 +122,10 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
. 'Please verify your license key is still configured in System → Update Sites after this update completes.',
|
||||
'warning'
|
||||
);
|
||||
=======
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: Could not save download key: ' . $e->getMessage());
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,16 +149,118 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
}
|
||||
|
||||
if ($type === 'install') {
|
||||
<<<<<<< Updated upstream
|
||||
/* Enable all bundled plugins on fresh install */
|
||||
$this->enableBundledPlugins();
|
||||
=======
|
||||
// Enable the system plugin automatically on fresh install
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
/* Create default backup directory in site root */
|
||||
$this->createBackupDirectory();
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
/* Generate a random webcron secret word */
|
||||
$this->generateWebcronSecret();
|
||||
|
||||
/* Create default scheduled task for backup automation */
|
||||
=======
|
||||
// Enable the quickicon plugin automatically
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('quickicon'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Enable the task plugin automatically
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('task'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Enable the webservices plugin automatically
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('webservices'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Enable the console plugin automatically
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('console'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Enable the content plugin automatically
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('content'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Enable the actionlog plugin automatically
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__extensions'))
|
||||
->set($db->quoteName('enabled') . ' = 1')
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
|
||||
->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog'))
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('mokosuitebackup'));
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Create and protect default backup directory
|
||||
$backupDir = JPATH_ADMINISTRATOR . '/components/com_mokosuitebackup/backups';
|
||||
|
||||
if (!is_dir($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
|
||||
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
|
||||
>>>>>>> Stashed changes
|
||||
$this->createDefaultScheduledTask();
|
||||
}
|
||||
|
||||
@@ -323,13 +430,20 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->where($db->quoteName('published') . ' = 1')
|
||||
<<<<<<< Updated upstream
|
||||
->where('(' . $db->quoteName('backup_dir') . ' IN ('
|
||||
. implode(',', array_map([$db, 'quote'], $oldDefaults))
|
||||
. ') OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
||||
=======
|
||||
->where('(' . $db->quoteName('backup_dir') . ' = ' . $db->quote('administrator/components/com_mokosuitebackup/backups')
|
||||
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]')
|
||||
. ' OR ' . $db->quoteName('backup_dir') . ' = ' . $db->quote('')
|
||||
>>>>>>> Stashed changes
|
||||
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
||||
$db->setQuery($query);
|
||||
|
||||
if ((int) $db->loadResult() > 0) {
|
||||
<<<<<<< Updated upstream
|
||||
$update = $db->getQuery(true)
|
||||
->update($db->quoteName('#__mokosuitebackup_profiles'))
|
||||
->set($db->quoteName('backup_dir') . ' = ' . $db->quote('[DEFAULT_DIR]'))
|
||||
@@ -339,6 +453,9 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
. ' OR ' . $db->quoteName('backup_dir') . ' IS NULL)');
|
||||
$db->setQuery($update);
|
||||
$db->execute();
|
||||
=======
|
||||
$profileUrl = Route::_('index.php?option=com_mokosuitebackup&view=profiles');
|
||||
>>>>>>> Stashed changes
|
||||
|
||||
$migrated = $db->getAffectedRows();
|
||||
|
||||
@@ -346,6 +463,7 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
error_log('MokoSuiteBackup: Migrated ' . $migrated . ' profile(s) from legacy backup_dir to [DEFAULT_DIR]');
|
||||
}
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: migrateDefaultBackupDir() failed: ' . $e->getMessage());
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
@@ -353,6 +471,10 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
. 'Please review your backup profiles and ensure the backup directory is set correctly.',
|
||||
'warning'
|
||||
);
|
||||
=======
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: warnDefaultBackupDir() failed: ' . $e->getMessage());
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +483,11 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
try {
|
||||
$db = Factory::getDbo();
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
/* Check if a MokoSuiteBackup task already exists */
|
||||
=======
|
||||
// Check if a MokoSuiteBackup task already exists
|
||||
>>>>>>> Stashed changes
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__scheduler_tasks'))
|
||||
@@ -411,6 +537,7 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
];
|
||||
|
||||
$db->insertObject('#__scheduler_tasks', $task);
|
||||
<<<<<<< Updated upstream
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: createDefaultScheduledTask() failed: ' . $e->getMessage());
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
@@ -575,6 +702,10 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
. 'please reinstall the package.',
|
||||
'warning'
|
||||
);
|
||||
=======
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: createDefaultScheduledTask() failed: ' . $e->getMessage());
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +726,11 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
->update($db->quoteName('#__menu'))
|
||||
->set($db->quoteName('img') . ' = ' . $db->quote($icon))
|
||||
->where($db->quoteName('client_id') . ' = 1')
|
||||
<<<<<<< Updated upstream
|
||||
->where($db->quoteName('link') . ' LIKE ' . $db->quote('index.php?option=com_mokosuitebackup%' . $linkFragment . '%'));
|
||||
=======
|
||||
->where($db->quoteName('link') . ' LIKE ' . $db->quote('%com_mokosuitebackup%' . $linkFragment . '%'));
|
||||
>>>>>>> Stashed changes
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
@@ -608,12 +743,17 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
->where($db->quoteName('level') . ' = 1');
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
<<<<<<< Updated upstream
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: syncMenuIcons() failed: ' . $e->getMessage());
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
'MokoSuiteBackup could not update sidebar menu icons. This is cosmetic and does not affect functionality.',
|
||||
'notice'
|
||||
);
|
||||
=======
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: syncMenuIcons() failed: ' . $e->getMessage());
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,6 +791,7 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
} catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: Could not restore download key: ' . $e->getMessage());
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
@@ -658,6 +799,10 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
. 'Please re-enter it in the Update Sites configuration to continue receiving updates.',
|
||||
'warning'
|
||||
);
|
||||
=======
|
||||
} catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: Could not restore download key: ' . $e->getMessage());
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,6 +838,7 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
<<<<<<< Updated upstream
|
||||
catch (\Exception $e) {
|
||||
error_log('MokoSuiteBackup: License key check failed: ' . $e->getMessage());
|
||||
Factory::getApplication()->enqueueMessage(
|
||||
@@ -700,6 +846,10 @@ class Pkg_MokoSuiteBackupInstallerScript
|
||||
. 'Please check System → Update Sites to ensure a valid license key is configured.',
|
||||
'warning'
|
||||
);
|
||||
=======
|
||||
catch (\Throwable $e) {
|
||||
error_log('MokoSuiteBackup: License key check failed: ' . $e->getMessage());
|
||||
>>>>>>> Stashed changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user