Files
MokoSuiteBackup/source/packages/com_mokosuitebackup/src/Engine/SteppedSession.php
T
jmiller 07fb4dcc24
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 10s
fix: remove run/backup buttons, move actions to detail view, custom restore script name, version bump 01.43.11-dev
- 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
2026-06-25 10:54:35 -05:00

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);
}
}
}
}