* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ namespace Joomla\Component\MokoSuiteBackup\Api\Controller; defined('_JEXEC') or die; use Joomla\CMS\MVC\Controller\ApiController; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\BackupEngine; class BackupsController extends ApiController { protected $contentType = 'backups'; protected $default_view = 'backups'; /** * Start a new backup (POST /api/index.php/v1/mokosuitebackup/backup) */ public function backup(): static { if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.run', 'com_mokosuitebackup')) { $this->app->setHeader('status', 403); echo json_encode(['errors' => [['title' => 'Access denied']]]); $this->app->close(); return $this; } $data = json_decode($this->input->json->getRaw(), true) ?: []; $profileId = (int) ($data['profile'] ?? 1); $description = $data['description'] ?? 'API backup ' . date('Y-m-d H:i:s'); $engine = new BackupEngine(); $result = $engine->run($profileId, $description, 'api'); if ($result['success']) { $this->app->setHeader('status', 200); echo json_encode(['data' => $result]); } else { $this->app->setHeader('status', 500); echo json_encode(['errors' => [['title' => $result['message']]]]); } $this->app->close(); return $this; } /** * Download a backup archive (GET /api/index.php/v1/mokosuitebackup/backup/:id/download) */ public function download(): static { if (!$this->app->getIdentity()->authorise('mokosuitebackup.backup.download', 'com_mokosuitebackup')) { $this->app->setHeader('status', 403); echo json_encode(['errors' => [['title' => 'Access denied']]]); $this->app->close(); return $this; } $id = $this->input->getInt('id', 0); $model = $this->getModel('Backup', 'Administrator'); $item = $model->getItem($id); if (!$item || !$item->id || !$item->filesexist || !is_file($item->absolute_path)) { $this->app->setHeader('status', 404); echo json_encode(['errors' => [['title' => 'Backup file not found']]]); $this->app->close(); return $this; } // Stream as binary download instead of base64 to avoid memory exhaustion while (@ob_end_clean()) { // clear all buffers } $filename = basename($item->archivename ?? $item->absolute_path); $filesize = filesize($item->absolute_path); $contentType = str_ends_with($filename, '.tar.gz') ? 'application/gzip' : 'application/zip'; header('Content-Type: ' . $contentType); header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename)); header('Content-Length: ' . $filesize); header('Cache-Control: no-cache, must-revalidate'); readfile($item->absolute_path); $this->app->close(); return $this; } /** * List backup profiles (GET /api/index.php/v1/mokosuitebackup/profiles) */ public function profiles(): static { if (!$this->app->getIdentity()->authorise('core.manage', 'com_mokosuitebackup')) { $this->app->setHeader('status', 403); echo json_encode(['errors' => [['title' => 'Access denied']]]); $this->app->close(); return $this; } $model = $this->getModel('Profiles', 'Administrator'); $items = $model->getItems(); $data = []; // Strip sensitive credentials before serialization $sensitiveFields = [ 'ftp_password', 'ftp_username', 's3_access_key', 's3_secret_key', 'gdrive_client_secret', 'gdrive_refresh_token', 'encryption_password', 'ntfy_token', ]; foreach ($items as $item) { $safe = clone $item; foreach ($sensitiveFields as $field) { if (isset($safe->$field) && $safe->$field !== '') { $safe->$field = '***'; } } $data[] = [ 'type' => 'profiles', 'id' => $safe->id, 'attributes' => $safe, ]; } $this->app->setHeader('status', 200); echo json_encode(['data' => $data]); $this->app->close(); return $this; } }