c72e950a25
Universal: PR Check / Branch Policy (pull_request) Failing after 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Universal: PR Check / Secret Scan (pull_request) Successful in 10s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 15s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 39s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 17s
Universal: Workflow Sync Trigger / Sync workflows to live repos (pull_request) Failing after 1m49s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled
Add five endpoints matching the existing backup API pattern: - GET /snapshots — list with pagination - POST /snapshot — create (content_types, description) - POST /snapshot/:id/restore — restore (mode, content_types) - DELETE /snapshot/:id — delete record + file - GET /snapshot/:id/download — stream JSON file ACL: mokosuitebackup.snapshot.manage for write ops, core.manage for read. Routes registered in webservices plugin alongside backup routes. Closes #54
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;
|
|
}
|
|
}
|