308 lines
8.2 KiB
PHP
308 lines
8.2 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
|
||
|
|
*
|
||
|
|
* 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;
|
||
|
|
}
|
||
|
|
}
|