* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE * * REST API controller for content snapshot operations. * * Endpoints: * GET /api/index.php/v1/mokosuitebackup/snapshots — List snapshots * POST /api/index.php/v1/mokosuitebackup/snapshot — Create snapshot * POST /api/index.php/v1/mokosuitebackup/snapshot/:id/restore — Restore snapshot * DELETE /api/index.php/v1/mokosuitebackup/snapshot/:id — Delete snapshot * GET /api/index.php/v1/mokosuitebackup/snapshot/:id/download — Download snapshot JSON */ namespace Joomla\Component\MokoSuiteBackup\Api\Controller; defined('_JEXEC') or die; use Joomla\CMS\Factory; use Joomla\CMS\MVC\Controller\ApiController; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\SnapshotEngine; use Joomla\Component\MokoSuiteBackup\Administrator\Engine\SnapshotRestoreEngine; class SnapshotsController extends ApiController { protected $contentType = 'snapshots'; protected $default_view = 'snapshots'; /** * List all snapshots with pagination (GET /api/index.php/v1/mokosuitebackup/snapshots) */ public function displayList(): 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; } $db = Factory::getDbo(); $limit = $this->input->getInt('limit', 20); $offset = $this->input->getInt('offset', 0); // Clamp limits $limit = max(1, min($limit, 100)); $offset = max(0, $offset); // Get total count $countQuery = $db->getQuery(true) ->select('COUNT(*)') ->from($db->quoteName('#__mokosuitebackup_snapshots')); $db->setQuery($countQuery); $total = (int) $db->loadResult(); // Get paginated results $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__mokosuitebackup_snapshots')) ->order($db->quoteName('created') . ' DESC'); $db->setQuery($query, $offset, $limit); $items = $db->loadObjectList() ?: []; $data = []; foreach ($items as $item) { $data[] = [ 'type' => 'snapshots', 'id' => $item->id, 'attributes' => $item, ]; } $this->app->setHeader('status', 200); echo json_encode([ 'data' => $data, 'meta' => [ 'total' => $total, 'limit' => $limit, 'offset' => $offset, ], ]); $this->app->close(); return $this; } /** * Create a new content snapshot (POST /api/index.php/v1/mokosuitebackup/snapshot) */ public function create(): static { if (!$this->app->getIdentity()->authorise('mokosuitebackup.snapshot.manage', '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) ?: []; $contentTypes = $data['content_types'] ?? []; $description = $data['description'] ?? ''; if (empty($contentTypes) || !is_array($contentTypes)) { $this->app->setHeader('status', 400); echo json_encode(['errors' => [['title' => 'content_types array is required']]]); $this->app->close(); return $this; } $engine = new SnapshotEngine(); $result = $engine->create($contentTypes, $description); 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; } /** * Restore from a snapshot (POST /api/index.php/v1/mokosuitebackup/snapshot/:id/restore) */ public function restore(): static { if (!$this->app->getIdentity()->authorise('mokosuitebackup.snapshot.manage', '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); if (!$id) { $this->app->setHeader('status', 400); echo json_encode(['errors' => [['title' => 'Snapshot ID is required']]]); $this->app->close(); return $this; } $data = json_decode($this->input->json->getRaw(), true) ?: []; $mode = $data['mode'] ?? 'replace'; $contentTypes = $data['content_types'] ?? []; // Enforce valid restore mode if (!in_array($mode, ['replace', 'merge'], true)) { $mode = 'replace'; } $engine = new SnapshotRestoreEngine(); $result = $engine->restore($id, $mode, $contentTypes); 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; } /** * Delete a snapshot record and its data file (DELETE /api/index.php/v1/mokosuitebackup/snapshot/:id) */ public function delete(): static { if (!$this->app->getIdentity()->authorise('mokosuitebackup.snapshot.manage', '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); if (!$id) { $this->app->setHeader('status', 400); echo json_encode(['errors' => [['title' => 'Snapshot ID is required']]]); $this->app->close(); return $this; } $db = Factory::getDbo(); // Load record to get file path $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__mokosuitebackup_snapshots')) ->where($db->quoteName('id') . ' = ' . $id); $db->setQuery($query); $record = $db->loadObject(); if (!$record) { $this->app->setHeader('status', 404); echo json_encode(['errors' => [['title' => 'Snapshot not found']]]); $this->app->close(); return $this; } // Delete data file if ($record->data_file && is_file($record->data_file)) { if (!unlink($record->data_file)) { error_log('MokoSuiteBackup: Failed to delete snapshot file: ' . $record->data_file); } } // Delete record $query = $db->getQuery(true) ->delete($db->quoteName('#__mokosuitebackup_snapshots')) ->where($db->quoteName('id') . ' = ' . $id); $db->setQuery($query); $db->execute(); $this->app->setHeader('status', 200); echo json_encode(['data' => ['success' => true, 'message' => 'Snapshot deleted']]); $this->app->close(); return $this; } /** * Stream the JSON snapshot file (GET /api/index.php/v1/mokosuitebackup/snapshot/:id/download) */ public function download(): 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; } $id = $this->input->getInt('id', 0); if (!$id) { $this->app->setHeader('status', 400); echo json_encode(['errors' => [['title' => 'Snapshot ID is required']]]); $this->app->close(); return $this; } $db = Factory::getDbo(); $query = $db->getQuery(true) ->select('*') ->from($db->quoteName('#__mokosuitebackup_snapshots')) ->where($db->quoteName('id') . ' = ' . $id); $db->setQuery($query); $record = $db->loadObject(); if (!$record || !is_file($record->data_file) || !is_readable($record->data_file)) { $this->app->setHeader('status', 404); echo json_encode(['errors' => [['title' => 'Snapshot file not found']]]); $this->app->close(); return $this; } // Stream as download while (@ob_end_clean()) { // clear all buffers } $filename = basename($record->data_file); $filesize = filesize($record->data_file); header('Content-Type: application/json'); header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($filename)); header('Content-Length: ' . $filesize); header('Cache-Control: no-cache, must-revalidate'); readfile($record->data_file); $this->app->close(); return $this; } }