07fb4dcc24
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
- Remove Run Backup / Backup Now buttons from profiles list, profile edit toolbar, and backup records view - Move download, browse archive, and view log from backup list rows into individual backup record detail view - Add download button to backup detail toolbar - Link profile column in backup records list to profile edit - Complete restore script filename customization across BackupEngine, SteppedBackupEngine, and MokoRestore - Remove ordering field from profiles, default sort by ID ascending - Fix untranslated JFIELD language keys - Bump all manifests to 01.43.11-dev
193 lines
4.3 KiB
PHP
193 lines
4.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteBackup
|
|
* @subpackage com_mokosuitebackup
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
* @license GNU General Public License version 3 or later; see LICENSE
|
|
*
|
|
* Manages state for AJAX step-based backups.
|
|
*
|
|
* On shared hosting where max_execution_time cannot be overridden,
|
|
* the backup runs as a series of small AJAX requests. Each request
|
|
* loads the session, does one chunk of work, saves state, and returns.
|
|
* The browser JS fires the next step automatically.
|
|
*
|
|
* Phases: init → database → files → finalize → upload → complete
|
|
*/
|
|
|
|
namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine;
|
|
|
|
defined('_JEXEC') or die;
|
|
|
|
class SteppedSession
|
|
{
|
|
public string $sessionId;
|
|
public string $phase = 'init';
|
|
public int $recordId = 0;
|
|
public int $profileId = 0;
|
|
public string $archivePath = '';
|
|
public string $archiveName = '';
|
|
public string $description = '';
|
|
public string $origin = 'backend';
|
|
|
|
// Database phase tracking
|
|
public array $tables = [];
|
|
public int $tableIndex = 0;
|
|
public int $tablesCount = 0;
|
|
public int $dbSize = 0;
|
|
|
|
// Files phase tracking
|
|
public array $fileBatches = [];
|
|
public int $batchIndex = 0;
|
|
public int $filesCount = 0;
|
|
public int $batchSize = 200;
|
|
|
|
// Profile settings (cached so we don't re-query each step)
|
|
public string $backupType = 'full';
|
|
public string $backupDir = '';
|
|
public array $excludeDirs = [];
|
|
public array $excludeFiles = [];
|
|
public array $excludeTables = [];
|
|
public string $remoteStorage = 'none';
|
|
public string $includeMokoRestore = '0';
|
|
public string $restoreScriptName = 'restore.php';
|
|
public string $restoreScriptPath = '';
|
|
public bool $remoteKeepLocal = true;
|
|
public string $encryptionPassword = '';
|
|
|
|
// Multi-remote destinations (loaded from #__mokosuitebackup_remotes)
|
|
public array $remoteDestinations = [];
|
|
public int $remoteIndex = 0;
|
|
|
|
// Progress
|
|
public int $totalSteps = 0;
|
|
public int $currentStep = 0;
|
|
public string $statusMessage = '';
|
|
public array $log = [];
|
|
|
|
private static function getSessionDir(): string
|
|
{
|
|
$dir = JPATH_ROOT . '/tmp/mokosuitebackup-sessions';
|
|
|
|
if (!is_dir($dir)) {
|
|
if (!mkdir($dir, 0755, true)) {
|
|
throw new \RuntimeException('Cannot create session directory: ' . $dir);
|
|
}
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
private static function getSessionPath(string $sessionId): string
|
|
{
|
|
// Sanitize session ID to prevent path traversal
|
|
$safe = preg_replace('/[^a-zA-Z0-9_-]/', '', $sessionId);
|
|
|
|
return self::getSessionDir() . '/' . $safe . '.json';
|
|
}
|
|
|
|
/**
|
|
* Create a new session.
|
|
*/
|
|
public static function create(): self
|
|
{
|
|
$session = new self();
|
|
$session->sessionId = 'mb_' . bin2hex(random_bytes(8));
|
|
|
|
return $session;
|
|
}
|
|
|
|
/**
|
|
* Load an existing session from disk.
|
|
*/
|
|
public static function load(string $sessionId): ?self
|
|
{
|
|
$path = self::getSessionPath($sessionId);
|
|
|
|
if (!is_file($path)) {
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode(file_get_contents($path), true);
|
|
|
|
if (!$data) {
|
|
return null;
|
|
}
|
|
|
|
$session = new self();
|
|
|
|
foreach ($data as $key => $value) {
|
|
if (property_exists($session, $key)) {
|
|
$session->$key = $value;
|
|
}
|
|
}
|
|
|
|
return $session;
|
|
}
|
|
|
|
/**
|
|
* Save session state to disk.
|
|
*/
|
|
public function save(): void
|
|
{
|
|
$path = self::getSessionPath($this->sessionId);
|
|
if (file_put_contents($path, json_encode(get_object_vars($this), JSON_PRETTY_PRINT)) === false) {
|
|
throw new \RuntimeException('Cannot save backup session: ' . $path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete session file.
|
|
*/
|
|
public function destroy(): void
|
|
{
|
|
$path = self::getSessionPath($this->sessionId);
|
|
|
|
if (is_file($path)) {
|
|
@unlink($path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a log entry.
|
|
*/
|
|
public function log(string $message): void
|
|
{
|
|
$this->log[] = '[' . date('H:i:s') . '] ' . $message;
|
|
}
|
|
|
|
/**
|
|
* Calculate progress percentage.
|
|
*/
|
|
public function getProgress(): int
|
|
{
|
|
if ($this->totalSteps <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
return min(100, (int) round(($this->currentStep / $this->totalSteps) * 100));
|
|
}
|
|
|
|
/**
|
|
* Clean up old session files (older than 24 hours).
|
|
*/
|
|
public static function cleanupOldSessions(): void
|
|
{
|
|
$dir = self::getSessionDir();
|
|
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
$cutoff = time() - 86400;
|
|
|
|
foreach (glob($dir . '/mb_*.json') as $file) {
|
|
if (filemtime($file) < $cutoff) {
|
|
@unlink($file);
|
|
}
|
|
}
|
|
}
|
|
}
|