* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * * AJAX controller for step-based backups. * Handles init and step requests from the admin UI JavaScript. */ namespace Joomla\Component\MokoBackup\Administrator\Controller; defined('_JEXEC') or die; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Session\Session; use Joomla\Component\MokoBackup\Administrator\Engine\SteppedBackupEngine; class AjaxController extends BaseController { /** * Initialize a new stepped backup. * POST: task=ajax.init&profile_id=1&description=... */ public function init(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { $this->sendJson(['error' => true, 'message' => 'Invalid token']); return; } $profileId = $this->input->getInt('profile_id', 1); $description = $this->input->getString('description', ''); $engine = new SteppedBackupEngine(); $result = $engine->init($profileId, $description, 'backend'); $this->sendJson($result); } /** * Run the next step of a backup session. * POST: task=ajax.step&session_id=mb_... */ public function step(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { $this->sendJson(['error' => true, 'message' => 'Invalid token']); return; } $sessionId = $this->input->getString('session_id', ''); if (empty($sessionId)) { $this->sendJson(['error' => true, 'message' => 'Missing session_id']); return; } $engine = new SteppedBackupEngine(); $result = $engine->runStep($sessionId); $this->sendJson($result); } /** * Browse server directories for the folder picker field. * POST: task=ajax.browseDir&path=/some/path */ public function browseDir(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { $this->sendJson(['error' => true, 'message' => 'Invalid token']); return; } $path = $this->input->getString('path', JPATH_ROOT); $path = realpath($path) ?: $path; if (!is_dir($path)) { $this->sendJson(['error' => true, 'message' => 'Directory not found: ' . $path]); return; } // Security: only allow browsing within JPATH_ROOT or parent directories // that could contain a backup folder (e.g., /home/user/backups) $dirs = []; $handle = @opendir($path); if ($handle) { while (($entry = readdir($handle)) !== false) { if ($entry === '.' || $entry === '..') { continue; } $fullPath = $path . '/' . $entry; if (is_dir($fullPath) && $entry[0] !== '.') { $dirs[] = [ 'name' => $entry, 'path' => $fullPath, ]; } } closedir($handle); } usort($dirs, fn($a, $b) => strcasecmp($a['name'], $b['name'])); $parent = dirname($path); $this->sendJson([ 'error' => false, 'current' => $path, 'parent' => ($parent !== $path) ? $parent : null, 'dirs' => $dirs, ]); } /** * Load and return the log file contents for a backup record. * POST: task=ajax.viewLog&id=123 */ public function viewLog(): void { if (!Session::checkToken('get') && !Session::checkToken('post')) { $this->sendJson(['error' => true, 'message' => 'Invalid token']); return; } $id = $this->input->getInt('id', 0); if (!$id) { $this->sendJson(['error' => true, 'message' => 'Missing record ID']); return; } $db = \Joomla\CMS\Factory::getDbo(); $query = $db->getQuery(true) ->select($db->quoteName(['absolute_path', 'log'])) ->from($db->quoteName('#__mokobackup_records')) ->where($db->quoteName('id') . ' = ' . $id); $db->setQuery($query); $record = $db->loadObject(); if (!$record) { $this->sendJson(['error' => true, 'message' => 'Record not found']); return; } // Try to load log from file alongside the archive $logPath = preg_replace('/\.(zip|tar\.gz)$/i', '.log', $record->absolute_path); $logContent = ''; if (is_file($logPath)) { $logContent = file_get_contents($logPath); } elseif (!empty($record->log)) { // Fall back to database-stored log $logContent = $record->log; } $this->sendJson([ 'error' => false, 'log' => $logContent ?: '(no log available)', 'source' => is_file($logPath) ? 'file' : 'database', ]); } /** * Send a JSON response and close the application. */ private function sendJson(array $data): void { $app = $this->app; $app->setHeader('Content-Type', 'application/json; charset=utf-8'); $app->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); $app->sendHeaders(); echo json_encode($data); $app->close(); } }