feat(api): add install-from-URL endpoint #87
@@ -8,7 +8,7 @@
|
||||
<name>MokoWaaS</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>White-label identity, security hardening, and tenant restriction layer for WaaS-managed Joomla environments</description>
|
||||
<version>02.20.00</version>
|
||||
<version>02.21.01</version>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: moko-platform.Automation
|
||||
# VERSION: 01.00.00
|
||||
# VERSION: 02.21.01
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
+9
-1
@@ -14,12 +14,20 @@
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: ./CHANGELOG.md
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
BRIEF: Version history using `Keep a Changelog`
|
||||
-->
|
||||
|
||||
# Changelog
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- API endpoint `POST /api/index.php/v1/mokowaas/install` — install extensions from a remote ZIP URL
|
||||
- Demo Mode with configurable warning banner on frontend when enabled
|
||||
- `DemoResetService` — baseline snapshot and restore for DB tables + media files
|
||||
- API endpoints `POST /?mokowaas=reset` and `POST /?mokowaas=snapshot` (query-string)
|
||||
- REST endpoints `POST /api/v1/mokowaas/reset` and `GET/POST /api/v1/mokowaas/snapshot`
|
||||
- `plg_task_mokowaasdemo` — Joomla Scheduled Task plugin for automatic demo site reset
|
||||
- Admin toggles: Take Snapshot Now and Restore Baseline Now in plugin config
|
||||
|
||||
## [02.20.00] --- 2026-05-28
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||
-->
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
DEFGROUP: mokoconsulting-tech.MokoWaaSBrand
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoWaaSBrand
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for MokoWaaSBrand
|
||||
-->
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: ./LICENSE.md
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
BRIEF: Project license (GPL-3.0-or-later)
|
||||
-->
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
||||
INGROUP: [PROJECT_NAME].Documentation
|
||||
REPO: [REPOSITORY_URL]
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
INGROUP: MokoWaaS.Build
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
FILE: build-guide.md
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/
|
||||
BRIEF: Build and packaging guide for the MokoWaaS system plugin
|
||||
NOTE: Defines environment setup, repository layout, packaging rules, and release preparation
|
||||
-->
|
||||
|
||||
# MokoWaaS Build Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Build Guide (VERSION: 02.21.01)
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/configuration-guide.md
|
||||
BRIEF: Configuration guide for the MokoWaaS system plugin
|
||||
NOTE: Defines plugin parameters, expected behaviors, and recommended defaults
|
||||
-->
|
||||
|
||||
# MokoWaaS Configuration Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Configuration Guide (VERSION: 02.21.01)
|
||||
|
||||
## 1. Objective
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/installation-guide.md
|
||||
BRIEF: Installation guide for the MokoWaaS system plugin
|
||||
NOTE: First document in the guide set
|
||||
-->
|
||||
|
||||
# MokoWaaS Installation Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Installation Guide (VERSION: 02.21.01)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/operations-guide.md
|
||||
BRIEF: Operational guide for administering and managing the MokoWaaS system plugin
|
||||
NOTE: Defines lifecycle, responsibilities, and operational behaviors
|
||||
-->
|
||||
|
||||
# MokoWaaS Operations Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Operations Guide (VERSION: 02.21.01)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/rollback-and-recovery-guide.md
|
||||
BRIEF: Rollback and recovery guide for restoring stable operation after plugin related incidents
|
||||
NOTE: Completes the core guide set for WaaS plugin governance
|
||||
-->
|
||||
|
||||
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Rollback and Recovery Guide (VERSION: 02.21.01)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/testing-guide.md
|
||||
BRIEF: Testing guide for MokoWaaS v02.01.08
|
||||
NOTE: Covers manual test procedures for language overrides, install/uninstall, and configuration
|
||||
-->
|
||||
|
||||
# MokoWaaS Testing Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Testing Guide (VERSION: 02.21.01)
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/troubleshooting-guide.md
|
||||
BRIEF: Troubleshooting guide for diagnosing and resolving issues related to the MokoWaaS plugin
|
||||
NOTE: Designed for administrators and WaaS operations teams
|
||||
-->
|
||||
|
||||
# MokoWaaS Troubleshooting Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Troubleshooting Guide (VERSION: 02.21.01)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Guides
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/guides/upgrade-and-versioning-guide.md
|
||||
BRIEF: Guide for updating, versioning, and maintaining the MokoWaaS plugin
|
||||
NOTE: Defines release flow, version rules, and upgrade validation
|
||||
-->
|
||||
|
||||
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.01.08)
|
||||
# MokoWaaS Upgrade and Versioning Guide (VERSION: 02.21.01)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
+2
-2
@@ -10,13 +10,13 @@
|
||||
DEFGROUP: Joomla.Plugin
|
||||
INGROUP: MokoWaaS.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
PATH: /docs/index.md
|
||||
BRIEF: Master index of all documentation for the MokoWaaS plugin
|
||||
NOTE: Automatically maintained index for all guide canvases
|
||||
-->
|
||||
|
||||
# MokoWaaS Documentation Index (VERSION: 02.01.08)
|
||||
# MokoWaaS Documentation Index (VERSION: 02.21.01)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
INGROUP: MokoWaaS
|
||||
REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
PATH: /docs/plugin-basic.md
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
BRIEF: Baseline documentation for the MokoWaaS system plugin
|
||||
NOTE: Foundational reference for internal and external stakeholders
|
||||
-->
|
||||
|
||||
# MokoWaaS Plugin Overview (VERSION: 02.01.08)
|
||||
# MokoWaaS Plugin Overview (VERSION: 02.21.01)
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ DEFGROUP: MokoWaaS.Documentation
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://github.com/mokoconsulting-tech/MokoWaaS
|
||||
PATH: /docs/update-server.md
|
||||
VERSION: 02.01.08
|
||||
VERSION: 02.21.01
|
||||
BRIEF: How this extension's Joomla update server file (update.xml) is managed
|
||||
-->
|
||||
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoWaaS\Api\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Installer\Installer;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
|
||||
/**
|
||||
* Extension install-from-URL API controller.
|
||||
*
|
||||
* POST /api/index.php/v1/mokowaas/install
|
||||
* Body: {"url": "https://example.com/path/to/extension.zip"}
|
||||
*
|
||||
* Downloads a ZIP from the given URL and installs it via Joomla's Installer.
|
||||
* Requires a Joomla API token with core.manage on com_installer.
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
class InstallController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Maximum allowed download size in bytes (64 MB).
|
||||
*
|
||||
* @var int
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private const MAX_DOWNLOAD_BYTES = 67108864;
|
||||
|
||||
/**
|
||||
* Install an extension from a remote ZIP URL.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if ($app->input->getMethod() !== 'POST')
|
||||
{
|
||||
$this->sendJson(405, ['error' => 'POST required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user->authorise('core.manage', 'com_installer'))
|
||||
{
|
||||
$this->sendJson(403, ['error' => 'Not authorized — requires core.manage on com_installer']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse JSON body
|
||||
$body = json_decode($app->input->json->getRaw(), true);
|
||||
$url = $body['url'] ?? '';
|
||||
|
||||
if ($url === '')
|
||||
{
|
||||
$this->sendJson(400, ['error' => 'Missing "url" in request body']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate URL scheme
|
||||
if (!preg_match('#^https?://#i', $url))
|
||||
{
|
||||
$this->sendJson(400, ['error' => 'URL must use http or https scheme']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Must point to a .zip file
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
|
||||
if (!$path || !str_ends_with(strtolower($path), '.zip'))
|
||||
{
|
||||
$this->sendJson(400, ['error' => 'URL must point to a .zip file']);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$result = $this->downloadAndInstall($url);
|
||||
$this->sendJson(200, $result);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendJson(500, [
|
||||
'error' => 'Installation failed',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download ZIP from URL, extract, and install via Joomla Installer.
|
||||
*
|
||||
* @param string $url The remote ZIP URL
|
||||
*
|
||||
* @return array Result payload
|
||||
*
|
||||
* @throws \RuntimeException on failure
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function downloadAndInstall(string $url): array
|
||||
{
|
||||
$config = Factory::getConfig();
|
||||
$tmpPath = $config->get('tmp_path', JPATH_ROOT . '/tmp');
|
||||
$zipFile = $tmpPath . '/mokowaas_install_' . bin2hex(random_bytes(8)) . '.zip';
|
||||
|
||||
// Download
|
||||
$this->downloadFile($url, $zipFile);
|
||||
|
||||
try
|
||||
{
|
||||
// Extract
|
||||
$extractDir = $tmpPath . '/mokowaas_extract_' . bin2hex(random_bytes(8));
|
||||
|
||||
if (!mkdir($extractDir, 0755, true))
|
||||
{
|
||||
throw new \RuntimeException('Failed to create extraction directory');
|
||||
}
|
||||
|
||||
$archive = new \Joomla\Archive\Archive;
|
||||
$archive->extract($zipFile, $extractDir);
|
||||
|
||||
// Install
|
||||
$installer = Installer::getInstance();
|
||||
$result = $installer->install($extractDir);
|
||||
|
||||
if (!$result)
|
||||
{
|
||||
throw new \RuntimeException('Joomla Installer returned failure — check server logs for details');
|
||||
}
|
||||
|
||||
// Read installed extension info from the installer
|
||||
$manifest = $installer->getManifest();
|
||||
$name = $manifest ? (string) $manifest->name : 'Unknown';
|
||||
$version = $manifest ? (string) $manifest->version : 'Unknown';
|
||||
$type = $installer->get('extension.type', 'Unknown');
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'Extension installed successfully',
|
||||
'extension' => [
|
||||
'name' => $name,
|
||||
'version' => $version,
|
||||
'type' => $type,
|
||||
],
|
||||
'source_url' => $url,
|
||||
];
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up temp files
|
||||
@unlink($zipFile);
|
||||
|
||||
if (isset($extractDir) && is_dir($extractDir))
|
||||
{
|
||||
$this->removeDirectory($extractDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from a URL with size limit enforcement.
|
||||
*
|
||||
* @param string $url Remote URL
|
||||
* @param string $destPath Local destination path
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \RuntimeException on failure
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function downloadFile(string $url, string $destPath): void
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
|
||||
if ($ch === false)
|
||||
{
|
||||
throw new \RuntimeException('Failed to initialise cURL');
|
||||
}
|
||||
|
||||
$fp = fopen($destPath, 'wb');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
curl_close($ch);
|
||||
throw new \RuntimeException('Failed to open temp file for writing');
|
||||
}
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_FILE => $fp,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => 120,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_USERAGENT => 'MokoWaaS-Installer/1.0',
|
||||
]);
|
||||
|
||||
$success = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
$fileSize = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);
|
||||
|
||||
curl_close($ch);
|
||||
fclose($fp);
|
||||
|
||||
if (!$success)
|
||||
{
|
||||
@unlink($destPath);
|
||||
throw new \RuntimeException('Download failed (HTTP ' . $httpCode . '): ' . $error);
|
||||
}
|
||||
|
||||
if ($fileSize > self::MAX_DOWNLOAD_BYTES)
|
||||
{
|
||||
@unlink($destPath);
|
||||
throw new \RuntimeException('Download exceeds maximum size of ' . (self::MAX_DOWNLOAD_BYTES / 1048576) . ' MB');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove a directory and its contents.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function removeDirectory(string $dir): void
|
||||
{
|
||||
$items = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($items as $item)
|
||||
{
|
||||
if ($item->isDir())
|
||||
{
|
||||
@rmdir($item->getPathname());
|
||||
}
|
||||
else
|
||||
{
|
||||
@unlink($item->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON response and close.
|
||||
*
|
||||
* @param int $code HTTP status code
|
||||
* @param array $payload Response data
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function sendJson(int $code, array $payload): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$app->setHeader('Content-Type', 'application/json', true);
|
||||
$app->setHeader('Status', (string) $code, true);
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
$app->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoWaaS\Api\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
/**
|
||||
* Demo site reset API controller.
|
||||
*
|
||||
* POST /api/index.php/v1/mokowaas/reset
|
||||
* Body: {"baseline": "default"}
|
||||
*
|
||||
* Restores the site to a named baseline snapshot.
|
||||
* Requires a Joomla API token with core.manage on com_plugins.
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
class ResetController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Restore site to a baseline snapshot.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if ($app->input->getMethod() !== 'POST')
|
||||
{
|
||||
$this->sendJson(405, ['error' => 'POST required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user->authorise('core.manage', 'com_plugins'))
|
||||
{
|
||||
$this->sendJson(403, ['error' => 'Not authorized']);
|
||||
return;
|
||||
}
|
||||
|
||||
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
|
||||
|
||||
if (!$plugin)
|
||||
{
|
||||
$this->sendJson(503, ['error' => 'MokoWaaS system plugin not enabled']);
|
||||
return;
|
||||
}
|
||||
|
||||
$params = new Registry($plugin->params);
|
||||
|
||||
try
|
||||
{
|
||||
$body = json_decode($app->input->json->getRaw(), true);
|
||||
$baseline = $body['baseline']
|
||||
?? $params->get('demo_active_baseline', 'default');
|
||||
|
||||
$service = $this->createService($params);
|
||||
$result = $service->restoreSnapshot($baseline);
|
||||
|
||||
$this->sendJson(200, $result);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendJson(500, [
|
||||
'error' => 'Reset failed',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DemoResetService from plugin params.
|
||||
*
|
||||
* @param Registry $params Plugin parameters
|
||||
*
|
||||
* @return \Moko\Plugin\System\MokoWaaS\Service\DemoResetService
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function createService(Registry $params)
|
||||
{
|
||||
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php';
|
||||
|
||||
if (!file_exists($serviceFile))
|
||||
{
|
||||
throw new \RuntimeException('DemoResetService not found — is the MokoWaaS plugin installed?');
|
||||
}
|
||||
|
||||
require_once $serviceFile;
|
||||
|
||||
$tablesRaw = $params->get('demo_snapshot_tables', '');
|
||||
$tables = array_filter(array_map('trim', explode("\n", $tablesRaw)));
|
||||
$media = (bool) $params->get('demo_snapshot_include_media', 1);
|
||||
|
||||
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($tables, $media);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code HTTP status code
|
||||
* @param array $payload Response data
|
||||
* @return void
|
||||
*/
|
||||
private function sendJson(int $code, array $payload): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$app->setHeader('Content-Type', 'application/json', true);
|
||||
$app->setHeader('Status', (string) $code, true);
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
$app->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage com_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Component\MokoWaaS\Api\Controller;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Controller\BaseController;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
/**
|
||||
* Snapshot management API controller.
|
||||
*
|
||||
* GET /api/index.php/v1/mokowaas/snapshot — list snapshots
|
||||
* POST /api/index.php/v1/mokowaas/snapshot — create snapshot
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
class SnapshotController extends BaseController
|
||||
{
|
||||
/**
|
||||
* List all available snapshots.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function displayList(): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user->authorise('core.manage', 'com_plugins'))
|
||||
{
|
||||
$this->sendJson(403, ['error' => 'Not authorized']);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$service = $this->createService();
|
||||
|
||||
$this->sendJson(200, [
|
||||
'status' => 'ok',
|
||||
'snapshots' => $service->listSnapshots(),
|
||||
]);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendJson(500, [
|
||||
'error' => 'Failed to list snapshots',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new snapshot.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
|
||||
if ($app->input->getMethod() !== 'POST')
|
||||
{
|
||||
$this->sendJson(405, ['error' => 'POST required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user->authorise('core.manage', 'com_plugins'))
|
||||
{
|
||||
$this->sendJson(403, ['error' => 'Not authorized']);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
|
||||
$params = $plugin ? new Registry($plugin->params) : new Registry;
|
||||
|
||||
$body = json_decode($app->input->json->getRaw(), true);
|
||||
$name = $body['name']
|
||||
?? $params->get('demo_active_baseline', 'default');
|
||||
|
||||
$service = $this->createService();
|
||||
$result = $service->createSnapshot($name);
|
||||
|
||||
$this->sendJson(200, $result);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendJson(500, [
|
||||
'error' => 'Snapshot failed',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DemoResetService from plugin params.
|
||||
*
|
||||
* @return \Moko\Plugin\System\MokoWaaS\Service\DemoResetService
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function createService()
|
||||
{
|
||||
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php';
|
||||
|
||||
if (!file_exists($serviceFile))
|
||||
{
|
||||
throw new \RuntimeException('DemoResetService not found');
|
||||
}
|
||||
|
||||
require_once $serviceFile;
|
||||
|
||||
$plugin = PluginHelper::getPlugin('system', 'mokowaas');
|
||||
$params = $plugin ? new Registry($plugin->params) : new Registry;
|
||||
|
||||
$tablesRaw = $params->get('demo_snapshot_tables', '');
|
||||
$tables = array_filter(array_map('trim', explode("\n", $tablesRaw)));
|
||||
$media = (bool) $params->get('demo_snapshot_include_media', 1);
|
||||
|
||||
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($tables, $media);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $code HTTP status code
|
||||
* @param array $payload Response data
|
||||
* @return void
|
||||
*/
|
||||
private function sendJson(int $code, array $payload): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$app->setHeader('Content-Type', 'application/json', true);
|
||||
$app->setHeader('Status', (string) $code, true);
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
$app->close();
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
* VERSION: 02.01.08
|
||||
* VERSION: 02.21.01
|
||||
* PATH: /src/Extension/MokoWaaS.php
|
||||
* NOTE: Handles Joomla system events for rebranding functionality
|
||||
*/
|
||||
@@ -862,6 +862,60 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
);
|
||||
}
|
||||
|
||||
// Demo Mode: Take Snapshot Now
|
||||
if ((int) $params->get('demo_take_snapshot_now', 0) === 1)
|
||||
{
|
||||
$params->set('demo_take_snapshot_now', '0');
|
||||
$changed = true;
|
||||
|
||||
try
|
||||
{
|
||||
$this->params = $params;
|
||||
$service = $this->createDemoResetService();
|
||||
$baseline = $params->get('demo_active_baseline', 'default');
|
||||
$result = $service->createSnapshot($baseline);
|
||||
|
||||
$app->enqueueMessage(
|
||||
sprintf('Demo snapshot "%s" created (%d tables).', $baseline, $result['tables']),
|
||||
'message'
|
||||
);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$app->enqueueMessage(
|
||||
'Snapshot failed: ' . $e->getMessage(),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Demo Mode: Restore Baseline Now
|
||||
if ((int) $params->get('demo_restore_now', 0) === 1)
|
||||
{
|
||||
$params->set('demo_restore_now', '0');
|
||||
$changed = true;
|
||||
|
||||
try
|
||||
{
|
||||
$this->params = $params;
|
||||
$service = $this->createDemoResetService();
|
||||
$baseline = $params->get('demo_active_baseline', 'default');
|
||||
$result = $service->restoreSnapshot($baseline);
|
||||
|
||||
$app->enqueueMessage(
|
||||
sprintf('Site restored to baseline "%s" (%d tables).', $baseline, $result['restored_tables']),
|
||||
'message'
|
||||
);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$app->enqueueMessage(
|
||||
'Restore failed: ' . $e->getMessage(),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($changed)
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
@@ -965,6 +1019,12 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
$this->injectAliasRobots($doc);
|
||||
}
|
||||
|
||||
// Demo mode banner (frontend only)
|
||||
if ($this->app->isClient('site') && (int) $this->params->get('demo_mode_enabled', 0))
|
||||
{
|
||||
$this->injectDemoBanner($doc);
|
||||
}
|
||||
|
||||
if (!$this->app->isClient('administrator'))
|
||||
{
|
||||
return;
|
||||
@@ -980,6 +1040,65 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject demo mode warning banner into the frontend site.
|
||||
*
|
||||
* Renders a fixed-position bar at the top of the page with a configurable
|
||||
* message, color, optional countdown, and session-dismissable behavior.
|
||||
*
|
||||
* @param \Joomla\CMS\Document\HtmlDocument $doc Document object
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
protected function injectDemoBanner($doc)
|
||||
{
|
||||
$message = htmlspecialchars($this->params->get('demo_banner_message', 'This is a demo site. All changes will be reset periodically.'), ENT_QUOTES, 'UTF-8');
|
||||
$bgColor = htmlspecialchars($this->params->get('demo_banner_color', '#d9534f'), ENT_QUOTES, 'UTF-8');
|
||||
$showCountdown = (int) $this->params->get('demo_banner_show_countdown', 0);
|
||||
$intervalHours = (int) $this->params->get('demo_reset_interval_hours', 24);
|
||||
$resetAt = time() + ($intervalHours * 3600);
|
||||
|
||||
$countdownJs = '';
|
||||
|
||||
if ($showCountdown)
|
||||
{
|
||||
$countdownJs = "
|
||||
var resetAt = {$resetAt} * 1000;
|
||||
var cdSpan = document.getElementById('mokowaas-demo-countdown');
|
||||
if (cdSpan) {
|
||||
setInterval(function() {
|
||||
var now = Date.now();
|
||||
var diff = Math.max(0, Math.floor((resetAt - now) / 1000));
|
||||
var h = Math.floor(diff / 3600);
|
||||
var m = Math.floor((diff % 3600) / 60);
|
||||
var s = diff % 60;
|
||||
cdSpan.textContent = ' — Resets in ' + h + 'h ' + m + 'm ' + s + 's';
|
||||
}, 1000);
|
||||
}
|
||||
";
|
||||
}
|
||||
|
||||
$doc->addScriptDeclaration("
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (sessionStorage.getItem('mokowaas_banner_dismissed') === '1') return;
|
||||
|
||||
var bar = document.createElement('div');
|
||||
bar.id = 'mokowaas-demo-banner';
|
||||
bar.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999999;background:{$bgColor};color:#fff;padding:10px 40px 10px 20px;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:14px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.3);';
|
||||
bar.innerHTML = '<span>{$message}</span>' +
|
||||
'" . ($showCountdown ? "<span id=\"mokowaas-demo-countdown\"></span>" : "") . "' +
|
||||
'<button onclick=\"this.parentNode.remove();sessionStorage.setItem(\\x27mokowaas_banner_dismissed\\x27,\\x271\\x27);document.body.style.paddingTop=\\x270\\x27\" style=\"position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:#fff;font-size:20px;cursor:pointer;line-height:1;padding:0 5px\">×</button>';
|
||||
|
||||
document.body.insertBefore(bar, document.body.firstChild);
|
||||
document.body.style.paddingTop = bar.offsetHeight + 'px';
|
||||
|
||||
{$countdownJs}
|
||||
});
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide MokoWaaS plugin and package from the extensions list via JS.
|
||||
*
|
||||
@@ -1407,11 +1526,17 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
case 'info':
|
||||
$this->handleInfoAction();
|
||||
break;
|
||||
case 'reset':
|
||||
$this->handleDemoResetAction();
|
||||
break;
|
||||
case 'snapshot':
|
||||
$this->handleSnapshotAction();
|
||||
break;
|
||||
default:
|
||||
$this->sendHealthResponse(400, [
|
||||
'error' => 'Unknown action',
|
||||
'action' => $action,
|
||||
'available' => ['health', 'install', 'update', 'cache', 'backup', 'info'],
|
||||
'available' => ['health', 'install', 'update', 'cache', 'backup', 'info', 'reset', 'snapshot'],
|
||||
]);
|
||||
break;
|
||||
}
|
||||
@@ -1421,6 +1546,117 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
// API Actions
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle demo site reset via API.
|
||||
*
|
||||
* POST /?mokowaas=reset
|
||||
* Body: {"baseline": "default"} (optional, defaults to active baseline)
|
||||
*
|
||||
* @return void
|
||||
* @since 02.21.00
|
||||
*/
|
||||
protected function handleDemoResetAction()
|
||||
{
|
||||
if ($this->app->input->getMethod() !== 'POST')
|
||||
{
|
||||
$this->sendHealthResponse(405, ['error' => 'POST required']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$baseline = $body['baseline']
|
||||
?? $this->params->get('demo_active_baseline', 'default');
|
||||
|
||||
$service = $this->createDemoResetService();
|
||||
$result = $service->restoreSnapshot($baseline);
|
||||
|
||||
$this->sendHealthResponse(200, $result);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendHealthResponse(500, [
|
||||
'error' => 'Reset failed',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle snapshot create/list via API.
|
||||
*
|
||||
* GET /?mokowaas=snapshot — list snapshots
|
||||
* POST /?mokowaas=snapshot — create snapshot
|
||||
* Body: {"name": "my-baseline"} (optional, defaults to active baseline)
|
||||
*
|
||||
* @return void
|
||||
* @since 02.21.00
|
||||
*/
|
||||
protected function handleSnapshotAction()
|
||||
{
|
||||
$service = $this->createDemoResetService();
|
||||
|
||||
if ($this->app->input->getMethod() === 'GET')
|
||||
{
|
||||
$this->sendHealthResponse(200, [
|
||||
'status' => 'ok',
|
||||
'snapshots' => $service->listSnapshots(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->app->input->getMethod() !== 'POST')
|
||||
{
|
||||
$this->sendHealthResponse(405, ['error' => 'GET or POST required']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$name = $body['name']
|
||||
?? $this->params->get('demo_active_baseline', 'default');
|
||||
|
||||
$result = $service->createSnapshot($name);
|
||||
|
||||
$this->sendHealthResponse(200, $result);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendHealthResponse(500, [
|
||||
'error' => 'Snapshot failed',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DemoResetService instance from current plugin params.
|
||||
*
|
||||
* @return \Moko\Plugin\System\MokoWaaS\Service\DemoResetService
|
||||
* @since 02.21.00
|
||||
*/
|
||||
protected function createDemoResetService()
|
||||
{
|
||||
require_once __DIR__ . '/../Service/DemoResetService.php';
|
||||
|
||||
$tablesRaw = $this->params->get('demo_snapshot_tables', '');
|
||||
$tables = array_filter(
|
||||
array_map('trim', explode("\n", $tablesRaw))
|
||||
);
|
||||
|
||||
$includeMedia = (bool) $this->params->get('demo_snapshot_include_media', 1);
|
||||
|
||||
return new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService(
|
||||
$tables,
|
||||
$includeMedia
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger Joomla update finder check.
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.01.08
|
||||
* VERSION: 02.21.01
|
||||
* PATH: /src/Field/AllowedIpsField.php
|
||||
* BRIEF: Custom form field that displays the current IP whitelist
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* VERSION: 02.11.00
|
||||
* VERSION: 02.21.01
|
||||
* PATH: /src/Field/CurrentIpField.php
|
||||
* BRIEF: Read-only field that displays the current user's IP address
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_system_mokowaas
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*
|
||||
* FILE INFORMATION
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_system_mokowaas/Service/DemoResetService.php
|
||||
* VERSION: 02.21.01
|
||||
* BRIEF: Core snapshot/restore service for demo site reset
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoWaaS\Service;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
|
||||
/**
|
||||
* Demo Reset Service — manages baseline snapshots and restore operations.
|
||||
*
|
||||
* This is the single source of truth for all snapshot logic. Called by:
|
||||
* - Admin one-shot toggles (onExtensionAfterSave)
|
||||
* - Query-string API (?mokowaas=reset / ?mokowaas=snapshot)
|
||||
* - REST API (/api/v1/mokowaas/reset, /api/v1/mokowaas/snapshot)
|
||||
* - Joomla Scheduled Task (plg_task_mokowaasdemo)
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
class DemoResetService
|
||||
{
|
||||
/**
|
||||
* Maximum snapshot name length.
|
||||
*
|
||||
* @var int
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private const MAX_NAME_LENGTH = 64;
|
||||
|
||||
/**
|
||||
* Rows per batch for paginated table dump/restore.
|
||||
*
|
||||
* @var int
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private const BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* Default tables to snapshot if none configured.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private const DEFAULT_TABLES = [
|
||||
'#__content',
|
||||
'#__categories',
|
||||
'#__fields',
|
||||
'#__fields_values',
|
||||
'#__fields_groups',
|
||||
'#__menu',
|
||||
'#__menu_types',
|
||||
'#__modules',
|
||||
'#__modules_menu',
|
||||
'#__users',
|
||||
'#__user_usergroup_map',
|
||||
'#__user_profiles',
|
||||
'#__tags',
|
||||
'#__contentitem_tag_map',
|
||||
'#__assets',
|
||||
];
|
||||
|
||||
/**
|
||||
* Root directory for all snapshots.
|
||||
*
|
||||
* @var string
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private string $snapshotDir;
|
||||
|
||||
/**
|
||||
* Tables to include in snapshots.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private array $tables;
|
||||
|
||||
/**
|
||||
* Whether to include media files in snapshots.
|
||||
*
|
||||
* @var bool
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private bool $includeMedia;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $tables Table names with #__ prefix
|
||||
* @param bool $includeMedia Include /images/ directory in snapshot
|
||||
* @param string $baseDir Override snapshot root (for testing)
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function __construct(array $tables = [], bool $includeMedia = true, string $baseDir = '')
|
||||
{
|
||||
$this->tables = !empty($tables) ? $tables : self::DEFAULT_TABLES;
|
||||
$this->includeMedia = $includeMedia;
|
||||
$this->snapshotDir = $baseDir ?: JPATH_ROOT . '/mokowaas-snapshots';
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available snapshots.
|
||||
*
|
||||
* @return array Array of manifest data keyed by snapshot name
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function listSnapshots(): array
|
||||
{
|
||||
$snapshots = [];
|
||||
|
||||
if (!is_dir($this->snapshotDir))
|
||||
{
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
$dirs = glob($this->snapshotDir . '/*/manifest.json');
|
||||
|
||||
foreach ($dirs as $manifestPath)
|
||||
{
|
||||
$data = json_decode(file_get_contents($manifestPath), true);
|
||||
|
||||
if ($data && isset($data['name']))
|
||||
{
|
||||
$snapshots[$data['name']] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a named snapshot of the current site state.
|
||||
*
|
||||
* @param string $name Snapshot name (alphanumeric, hyphens, underscores)
|
||||
*
|
||||
* @return array Result payload with status, tables count, size info
|
||||
*
|
||||
* @throws \InvalidArgumentException On invalid snapshot name
|
||||
* @throws \RuntimeException On filesystem/database failures
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function createSnapshot(string $name): array
|
||||
{
|
||||
$this->validateSnapshotName($name);
|
||||
$this->ensureSnapshotDir();
|
||||
|
||||
$path = $this->getSnapshotPath($name);
|
||||
|
||||
// Remove existing snapshot with the same name
|
||||
if (is_dir($path))
|
||||
{
|
||||
$this->removeDirectory($path);
|
||||
}
|
||||
|
||||
if (!mkdir($path, 0755, true))
|
||||
{
|
||||
throw new \RuntimeException('Failed to create snapshot directory: ' . $path);
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$prefix = $db->getPrefix();
|
||||
$tables = $db->getTableList();
|
||||
$dumped = 0;
|
||||
|
||||
foreach ($this->tables as $tableName)
|
||||
{
|
||||
$realTable = str_replace('#__', $prefix, $tableName);
|
||||
|
||||
if (!in_array($realTable, $tables))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->dumpTable($tableName, $realTable, $path, $db);
|
||||
$dumped++;
|
||||
}
|
||||
|
||||
// Media snapshot
|
||||
$hasMedia = false;
|
||||
|
||||
if ($this->includeMedia)
|
||||
{
|
||||
$hasMedia = $this->snapshotMedia($path);
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
$manifest = [
|
||||
'name' => $name,
|
||||
'created_at' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'tables' => $dumped,
|
||||
'table_list' => $this->tables,
|
||||
'has_media' => $hasMedia,
|
||||
'joomla_version' => JVERSION,
|
||||
];
|
||||
|
||||
file_put_contents(
|
||||
$path . '/manifest.json',
|
||||
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
|
||||
Log::add(
|
||||
sprintf('Demo snapshot "%s" created (%d tables, media=%s)', $name, $dumped, $hasMedia ? 'yes' : 'no'),
|
||||
Log::INFO,
|
||||
'mokowaas'
|
||||
);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'Snapshot created',
|
||||
'name' => $name,
|
||||
'tables' => $dumped,
|
||||
'has_media' => $hasMedia,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the site to a named snapshot.
|
||||
*
|
||||
* @param string $name Snapshot name to restore
|
||||
*
|
||||
* @return array Result payload
|
||||
*
|
||||
* @throws \InvalidArgumentException On invalid name
|
||||
* @throws \RuntimeException On missing snapshot or restore failure
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function restoreSnapshot(string $name): array
|
||||
{
|
||||
$this->validateSnapshotName($name);
|
||||
|
||||
$path = $this->getSnapshotPath($name);
|
||||
$manifestFile = $path . '/manifest.json';
|
||||
|
||||
if (!file_exists($manifestFile))
|
||||
{
|
||||
throw new \RuntimeException('Snapshot not found: ' . $name);
|
||||
}
|
||||
|
||||
$manifest = json_decode(file_get_contents($manifestFile), true);
|
||||
|
||||
if (!$manifest)
|
||||
{
|
||||
throw new \RuntimeException('Invalid manifest for snapshot: ' . $name);
|
||||
}
|
||||
|
||||
// Clear Joomla cache before restore
|
||||
try
|
||||
{
|
||||
$cache = Factory::getCache('');
|
||||
$cache->clean('');
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
// Cache clear is best-effort
|
||||
}
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$prefix = $db->getPrefix();
|
||||
$restored = 0;
|
||||
|
||||
// Restore tables — assets first for ACL integrity
|
||||
$sqlFiles = glob($path . '/*.sql');
|
||||
|
||||
// Sort: #__assets first
|
||||
usort($sqlFiles, function ($a, $b) {
|
||||
$aIsAssets = str_contains(basename($a), '__assets');
|
||||
$bIsAssets = str_contains(basename($b), '__assets');
|
||||
|
||||
if ($aIsAssets) return -1;
|
||||
if ($bIsAssets) return 1;
|
||||
|
||||
return strcmp($a, $b);
|
||||
});
|
||||
|
||||
foreach ($sqlFiles as $sqlFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->restoreTable($sqlFile, $db, $prefix);
|
||||
$restored++;
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
Log::add(
|
||||
sprintf('Demo reset: failed to restore %s: %s', basename($sqlFile), $e->getMessage()),
|
||||
Log::ERROR,
|
||||
'mokowaas'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore media
|
||||
$mediaRestored = false;
|
||||
|
||||
if ($manifest['has_media'] ?? false)
|
||||
{
|
||||
$mediaRestored = $this->restoreMedia($path);
|
||||
}
|
||||
|
||||
Log::add(
|
||||
sprintf('Demo site reset to baseline "%s" (%d tables, media=%s)', $name, $restored, $mediaRestored ? 'yes' : 'no'),
|
||||
Log::WARNING,
|
||||
'mokowaas'
|
||||
);
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'Site restored to baseline: ' . $name,
|
||||
'baseline' => $name,
|
||||
'restored_tables' => $restored,
|
||||
'media_restored' => $mediaRestored,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a named snapshot.
|
||||
*
|
||||
* @param string $name Snapshot name
|
||||
*
|
||||
* @return bool True on success
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function deleteSnapshot(string $name): bool
|
||||
{
|
||||
$this->validateSnapshotName($name);
|
||||
|
||||
$path = $this->getSnapshotPath($name);
|
||||
|
||||
if (!is_dir($path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->removeDirectory($path);
|
||||
|
||||
Log::add(
|
||||
sprintf('Demo snapshot "%s" deleted', $name),
|
||||
Log::INFO,
|
||||
'mokowaas'
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump a single table to a SQL file using paginated reads.
|
||||
*
|
||||
* @param string $logicalName Table name with #__ prefix
|
||||
* @param string $realName Actual table name with prefix
|
||||
* @param string $dir Snapshot directory
|
||||
* @param \Joomla\Database\DatabaseInterface $db Database driver
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function dumpTable(string $logicalName, string $realName, string $dir, $db): void
|
||||
{
|
||||
$safeFileName = str_replace('#__', 'jml__', $logicalName);
|
||||
$fp = fopen($dir . '/' . $safeFileName . '.sql', 'w');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
throw new \RuntimeException('Cannot write dump file for: ' . $logicalName);
|
||||
}
|
||||
|
||||
// Get column names for consistent INSERT statements
|
||||
$columns = $db->getTableColumns($realName, false);
|
||||
$colNames = array_keys($columns);
|
||||
$quotedCols = array_map([$db, 'quoteName'], $colNames);
|
||||
$colList = implode(', ', $quotedCols);
|
||||
|
||||
$offset = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
$query = $db->getQuery(true)
|
||||
->select('*')
|
||||
->from($db->quoteName($realName))
|
||||
->setLimit(self::BATCH_SIZE, $offset);
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
if (empty($rows))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Build multi-value INSERT
|
||||
$values = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$vals = [];
|
||||
|
||||
foreach ($colNames as $col)
|
||||
{
|
||||
$val = $row[$col];
|
||||
|
||||
if ($val === null)
|
||||
{
|
||||
$vals[] = 'NULL';
|
||||
}
|
||||
else
|
||||
{
|
||||
$vals[] = $db->quote($val);
|
||||
}
|
||||
}
|
||||
|
||||
$values[] = '(' . implode(', ', $vals) . ')';
|
||||
}
|
||||
|
||||
fwrite($fp, 'INSERT INTO ' . $db->quoteName($realName)
|
||||
. ' (' . $colList . ') VALUES ' . "\n"
|
||||
. implode(",\n", $values) . ";\n\n");
|
||||
|
||||
$offset += self::BATCH_SIZE;
|
||||
|
||||
if (count($rows) < self::BATCH_SIZE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a table from a SQL dump file.
|
||||
*
|
||||
* @param string $sqlFile Path to the .sql file
|
||||
* @param \Joomla\Database\DatabaseInterface $db Database driver
|
||||
* @param string $prefix Table prefix
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function restoreTable(string $sqlFile, $db, string $prefix): void
|
||||
{
|
||||
// Derive table name from filename: jml__content.sql -> {prefix}content
|
||||
$baseName = basename($sqlFile, '.sql');
|
||||
$realTable = str_replace('jml__', $prefix, $baseName);
|
||||
|
||||
// Truncate the table first
|
||||
$db->setQuery('TRUNCATE TABLE ' . $db->quoteName($realTable));
|
||||
$db->execute();
|
||||
|
||||
$sql = file_get_contents($sqlFile);
|
||||
|
||||
if (empty(trim($sql)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Split by semicolons and execute each statement
|
||||
$statements = array_filter(
|
||||
array_map('trim', explode(";\n", $sql)),
|
||||
function ($s) { return !empty($s) && $s !== ';'; }
|
||||
);
|
||||
|
||||
foreach ($statements as $statement)
|
||||
{
|
||||
$statement = rtrim($statement, ';');
|
||||
|
||||
if (empty($statement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$db->setQuery($statement);
|
||||
$db->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ZIP archive of the /images/ directory.
|
||||
*
|
||||
* @param string $snapshotDir Snapshot directory path
|
||||
*
|
||||
* @return bool True if media was archived
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function snapshotMedia(string $snapshotDir): bool
|
||||
{
|
||||
$imagesDir = JPATH_ROOT . '/images';
|
||||
|
||||
if (!is_dir($imagesDir))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$zipPath = $snapshotDir . '/media.zip';
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($imagesDir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $item)
|
||||
{
|
||||
$relativePath = substr($item->getPathname(), strlen($imagesDir) + 1);
|
||||
$relativePath = str_replace('\\', '/', $relativePath);
|
||||
|
||||
if ($item->isDir())
|
||||
{
|
||||
$zip->addEmptyDir($relativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
$zip->addFile($item->getPathname(), $relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore media files from a snapshot ZIP.
|
||||
*
|
||||
* @param string $snapshotDir Snapshot directory path
|
||||
*
|
||||
* @return bool True if media was restored
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function restoreMedia(string $snapshotDir): bool
|
||||
{
|
||||
$zipPath = $snapshotDir . '/media.zip';
|
||||
$imagesDir = JPATH_ROOT . '/images';
|
||||
|
||||
if (!file_exists($zipPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear existing images directory contents (keep the directory itself)
|
||||
$this->clearDirectory($imagesDir);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
if ($zip->open($zipPath) !== true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$zip->extractTo($imagesDir);
|
||||
$zip->close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the snapshot root directory exists with .htaccess protection.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function ensureSnapshotDir(): void
|
||||
{
|
||||
if (!is_dir($this->snapshotDir))
|
||||
{
|
||||
if (!mkdir($this->snapshotDir, 0755, true))
|
||||
{
|
||||
throw new \RuntimeException('Cannot create snapshot directory: ' . $this->snapshotDir);
|
||||
}
|
||||
}
|
||||
|
||||
$htaccess = $this->snapshotDir . '/.htaccess';
|
||||
|
||||
if (!file_exists($htaccess))
|
||||
{
|
||||
file_put_contents($htaccess, "Deny from all\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path for a named snapshot.
|
||||
*
|
||||
* @param string $name Snapshot name
|
||||
*
|
||||
* @return string Full directory path
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function getSnapshotPath(string $name): string
|
||||
{
|
||||
return $this->snapshotDir . '/' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a snapshot name to prevent path traversal.
|
||||
*
|
||||
* @param string $name Snapshot name to validate
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException On invalid name
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function validateSnapshotName(string $name): void
|
||||
{
|
||||
if ($name === '' || strlen($name) > self::MAX_NAME_LENGTH)
|
||||
{
|
||||
throw new \InvalidArgumentException(
|
||||
'Snapshot name must be 1-' . self::MAX_NAME_LENGTH . ' characters'
|
||||
);
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name))
|
||||
{
|
||||
throw new \InvalidArgumentException(
|
||||
'Snapshot name must contain only letters, numbers, hyphens, and underscores'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove a directory and all contents.
|
||||
*
|
||||
* @param string $dir Directory to remove
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function removeDirectory(string $dir): void
|
||||
{
|
||||
$items = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($items as $item)
|
||||
{
|
||||
if ($item->isDir())
|
||||
{
|
||||
@rmdir($item->getPathname());
|
||||
}
|
||||
else
|
||||
{
|
||||
@unlink($item->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all contents of a directory without removing the directory itself.
|
||||
*
|
||||
* @param string $dir Directory to clear
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function clearDirectory(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$items = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
|
||||
foreach ($items as $item)
|
||||
{
|
||||
if ($item->isDir())
|
||||
{
|
||||
@rmdir($item->getPathname());
|
||||
}
|
||||
else
|
||||
{
|
||||
@unlink($item->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<!DOCTYPE html><title></title>
|
||||
@@ -137,6 +137,31 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file exte
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
|
||||
|
||||
; ===== Demo Mode fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_INTERVAL_LABEL="Reset Interval (Hours)"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_INTERVAL_DESC="Hours between scheduled demo resets. Used for countdown display and scheduled task interval."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Media Files"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Include the /images/ directory in snapshots. Disabling this speeds up snapshot/restore for sites with large media libraries."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
|
||||
|
||||
; ===== Site Aliases fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior."
|
||||
|
||||
@@ -137,6 +137,31 @@ PLG_SYSTEM_MOKOWAAS_UPLOAD_TYPES_DESC="Comma-separated list of allowed file exte
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_LABEL="Max Upload Size (MB)"
|
||||
PLG_SYSTEM_MOKOWAAS_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes."
|
||||
|
||||
; ===== Demo Mode fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL="Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC="Configure demo site behavior with baseline snapshots and automatic periodic reset. When enabled, a warning banner is shown on the frontend."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL="Enable Demo Mode"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC="When enabled, shows a warning banner on the frontend and enables snapshot/restore functionality."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL="Banner Message"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC="Message displayed in the demo warning banner on the frontend."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL="Banner Color"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC="Background color for the demo warning banner."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL="Show Reset Countdown"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC="Display a countdown timer in the banner showing time until the next scheduled reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_INTERVAL_LABEL="Reset Interval (Hours)"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_INTERVAL_DESC="Hours between scheduled demo resets. Used for countdown display and scheduled task interval."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL="Snapshot Tables"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC="Database tables to include in snapshots. One per line, using #__ prefix. These tables will be truncated and restored during a reset."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL="Include Media Files"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC="Include the /images/ directory in snapshots. Disabling this speeds up snapshot/restore for sites with large media libraries."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL="Active Baseline Name"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC="Name of the baseline snapshot used by admin toggles and scheduled tasks. Alphanumeric, hyphens, and underscores only."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL="Take Snapshot Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC="Save the current site state as a baseline snapshot. Uses the Active Baseline Name above. Resets to No after execution."
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL="Restore Baseline Now"
|
||||
PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC="Immediately restore the site to the active baseline snapshot. WARNING: This will overwrite current content. Resets to No after execution."
|
||||
|
||||
; ===== Site Aliases fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL="Site Aliases"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC="Configure additional domains that mirror this site. Each alias can have its own offline status, robots directive, and backend redirect behavior."
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<license>GNU General Public License version 3 or later; see LICENSE.md</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.20.00</version>
|
||||
<version>02.21.01-dev</version>
|
||||
<description>This plugin rebrands the Joomla system interface with MokoWaaS identity. It applies language overrides and ensures consistent branding across the platform.</description>
|
||||
<namespace path=".">Moko\Plugin\System\MokoWaaS</namespace>
|
||||
<scriptfile>script.php</scriptfile>
|
||||
@@ -39,6 +39,7 @@
|
||||
<filename plugin="mokowaas">script.php</filename>
|
||||
<folder>Extension</folder>
|
||||
<folder>Field</folder>
|
||||
<folder>Service</folder>
|
||||
<folder>forms</folder>
|
||||
<folder>payload</folder>
|
||||
<folder>services</folder>
|
||||
@@ -264,7 +265,68 @@
|
||||
description="PLG_SYSTEM_MOKOWAAS_HIDDEN_MENUS_DESC"
|
||||
rows="5" filter="raw" />
|
||||
</fieldset>
|
||||
<fieldset name="site_aliases"
|
||||
<fieldset name="demo_mode"
|
||||
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_DEMO_DESC"
|
||||
>
|
||||
<field name="demo_mode_enabled" type="radio" default="0"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_ENABLED_DESC"
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="demo_banner_message" type="text"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_MSG_DESC"
|
||||
default="This is a demo site. All changes will be reset periodically." />
|
||||
<field name="demo_banner_color" type="color"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_BANNER_COLOR_DESC"
|
||||
default="#d9534f" />
|
||||
<field name="demo_banner_show_countdown" type="radio" default="0"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_COUNTDOWN_DESC"
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="demo_reset_interval_hours" type="number"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_INTERVAL_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_INTERVAL_DESC"
|
||||
default="24" hint="Hours between scheduled resets" />
|
||||
<field name="demo_snapshot_tables" type="textarea"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_TABLES_DESC"
|
||||
rows="8" filter="raw"
|
||||
default="#__content #__categories #__fields #__fields_values #__fields_groups #__menu #__menu_types #__modules #__modules_menu #__users #__user_usergroup_map #__user_profiles #__tags #__contentitem_tag_map #__assets" />
|
||||
<field name="demo_snapshot_include_media" type="radio" default="1"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_MEDIA_DESC"
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="demo_active_baseline" type="text"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_ACTIVE_BASELINE_DESC"
|
||||
default="default" />
|
||||
<field name="demo_take_snapshot_now" type="radio" default="0"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_TAKE_SNAPSHOT_DESC"
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field name="demo_restore_now" type="radio" default="0"
|
||||
label="PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_DEMO_RESTORE_NOW_DESC"
|
||||
class="btn-group btn-group-yesno">
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
</fieldset>
|
||||
<fieldset name="site_aliases"
|
||||
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_ALIASES_DESC"
|
||||
>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
* VERSION: 02.01.08
|
||||
* VERSION: 02.21.01
|
||||
* PATH: /src/script.php
|
||||
* BRIEF: Installation script for MokoWaaS plugin
|
||||
* NOTE: Handles installation, update, and uninstallation tasks including language override deployment
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* DEFGROUP: Joomla.Plugin
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
* VERSION: 02.01.08
|
||||
* VERSION: 02.21.01
|
||||
* PATH: /src/services/provider.php
|
||||
* BRIEF: Service provider for dependency injection in Joomla 5.x
|
||||
* NOTE: Registers the plugin with Joomla's DI container
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form>
|
||||
<fieldset name="reset_params">
|
||||
<field name="baseline" type="text"
|
||||
label="PLG_TASK_MOKOWAASDEMO_BASELINE_LABEL"
|
||||
description="PLG_TASK_MOKOWAASDEMO_BASELINE_DESC"
|
||||
default="default" />
|
||||
</fieldset>
|
||||
</form>
|
||||
@@ -0,0 +1,10 @@
|
||||
; MokoWaaS Demo Reset Task Plugin
|
||||
; Copyright (C) 2026 Moko Consulting
|
||||
; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
PLG_TASK_MOKOWAASDEMO="Task - MokoWaaS Demo Reset"
|
||||
PLG_TASK_MOKOWAASDEMO_DESC="Scheduled task to periodically reset a demo site to a saved baseline snapshot."
|
||||
PLG_TASK_MOKOWAASDEMO_RESET_TITLE="MokoWaaS Demo Reset"
|
||||
PLG_TASK_MOKOWAASDEMO_RESET_DESC="Restore the site to a named baseline snapshot. Configure the baseline name and schedule interval."
|
||||
PLG_TASK_MOKOWAASDEMO_BASELINE_LABEL="Baseline Name"
|
||||
PLG_TASK_MOKOWAASDEMO_BASELINE_DESC="Name of the snapshot to restore. Must match a snapshot created via the MokoWaaS Demo Mode settings."
|
||||
@@ -0,0 +1,6 @@
|
||||
; MokoWaaS Demo Reset Task Plugin (sys)
|
||||
; Copyright (C) 2026 Moko Consulting
|
||||
; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
PLG_TASK_MOKOWAASDEMO="Task - MokoWaaS Demo Reset"
|
||||
PLG_TASK_MOKOWAASDEMO_DESC="Scheduled task to periodically reset a demo site to a saved baseline snapshot."
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
<extension type="plugin" group="task" method="upgrade">
|
||||
<name>Task - MokoWaaS Demo Reset</name>
|
||||
<element>mokowaasdemo</element>
|
||||
<author>Moko Consulting</author>
|
||||
<creationDate>2026-05-30</creationDate>
|
||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>02.21.01-dev</version>
|
||||
<description>PLG_TASK_MOKOWAASDEMO_DESC</description>
|
||||
<namespace path="src">Moko\Plugin\Task\MokoWaaSDemo</namespace>
|
||||
|
||||
<files>
|
||||
<folder>src</folder>
|
||||
<folder>services</folder>
|
||||
<folder>forms</folder>
|
||||
<folder>language</folder>
|
||||
</files>
|
||||
|
||||
<languages folder="language">
|
||||
<language tag="en-GB">en-GB/plg_task_mokowaasdemo.ini</language>
|
||||
<language tag="en-GB">en-GB/plg_task_mokowaasdemo.sys.ini</language>
|
||||
</languages>
|
||||
</extension>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_task_mokowaasdemo
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Extension\PluginInterface;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\DI\Container;
|
||||
use Joomla\DI\ServiceProviderInterface;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Moko\Plugin\Task\MokoWaaSDemo\Extension\DemoReset;
|
||||
|
||||
return new class implements ServiceProviderInterface
|
||||
{
|
||||
public function register(Container $container): void
|
||||
{
|
||||
$container->set(
|
||||
PluginInterface::class,
|
||||
function (Container $container) {
|
||||
$dispatcher = $container->get(DispatcherInterface::class);
|
||||
$plugin = new DemoReset(
|
||||
$dispatcher,
|
||||
(array) PluginHelper::getPlugin('task', 'mokowaasdemo')
|
||||
);
|
||||
$plugin->setApplication(Factory::getApplication());
|
||||
|
||||
return $plugin;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoWaaS
|
||||
* @subpackage plg_task_mokowaasdemo
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\Task\MokoWaaSDemo\Extension;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent;
|
||||
use Joomla\Component\Scheduler\Administrator\Task\Status;
|
||||
use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait;
|
||||
use Joomla\Event\DispatcherInterface;
|
||||
use Joomla\Event\SubscriberInterface;
|
||||
use Joomla\Registry\Registry;
|
||||
|
||||
/**
|
||||
* MokoWaaS Demo Reset — Joomla Scheduled Task Plugin.
|
||||
*
|
||||
* Registers a task routine that restores the site to a named baseline
|
||||
* snapshot on a configurable schedule (e.g. every 24 hours).
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
final class DemoReset extends CMSPlugin implements SubscriberInterface
|
||||
{
|
||||
use TaskPluginTrait;
|
||||
|
||||
/**
|
||||
* Task routine map.
|
||||
*
|
||||
* @var array
|
||||
* @since 02.21.00
|
||||
*/
|
||||
protected const TASKS_MAP = [
|
||||
'mokowaas.demo.reset' => [
|
||||
'langConstPrefix' => 'PLG_TASK_MOKOWAASDEMO_RESET',
|
||||
'method' => 'resetDemoSite',
|
||||
'form' => 'reset_params',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'onTaskOptionsList' => 'advertiseRoutines',
|
||||
'onExecuteTask' => 'standardRoutineHandler',
|
||||
'onContentPrepareForm' => 'enhanceTaskItemForm',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the demo site to a baseline snapshot.
|
||||
*
|
||||
* @param ExecuteTaskEvent $event The task event
|
||||
*
|
||||
* @return int Status::OK or Status::KNOCKOUT
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
private function resetDemoSite(ExecuteTaskEvent $event): int
|
||||
{
|
||||
$params = $event->getArgument('params');
|
||||
$baseline = $params->baseline ?? 'default';
|
||||
|
||||
// Load system plugin params for table list and media setting
|
||||
$sysPlugin = PluginHelper::getPlugin('system', 'mokowaas');
|
||||
|
||||
if (!$sysPlugin)
|
||||
{
|
||||
$this->logTask('MokoWaaS system plugin not enabled — cannot reset');
|
||||
|
||||
return Status::KNOCKOUT;
|
||||
}
|
||||
|
||||
$sysParams = new Registry($sysPlugin->params);
|
||||
|
||||
// Load the service
|
||||
$serviceFile = JPATH_PLUGINS . '/system/mokowaas/Service/DemoResetService.php';
|
||||
|
||||
if (!file_exists($serviceFile))
|
||||
{
|
||||
$this->logTask('DemoResetService.php not found');
|
||||
|
||||
return Status::KNOCKOUT;
|
||||
}
|
||||
|
||||
require_once $serviceFile;
|
||||
|
||||
$tablesRaw = $sysParams->get('demo_snapshot_tables', '');
|
||||
$tables = array_filter(array_map('trim', explode("\n", $tablesRaw)));
|
||||
$media = (bool) $sysParams->get('demo_snapshot_include_media', 1);
|
||||
|
||||
$service = new \Moko\Plugin\System\MokoWaaS\Service\DemoResetService($tables, $media);
|
||||
|
||||
try
|
||||
{
|
||||
$result = $service->restoreSnapshot($baseline);
|
||||
$this->logTask(sprintf(
|
||||
'Demo site reset to "%s" — %d tables restored, media=%s',
|
||||
$baseline,
|
||||
$result['restored_tables'],
|
||||
$result['media_restored'] ? 'yes' : 'no'
|
||||
));
|
||||
|
||||
return Status::OK;
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->logTask('Demo reset failed: ' . $e->getMessage());
|
||||
|
||||
return Status::KNOCKOUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,5 +64,23 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface
|
||||
'update',
|
||||
['component' => 'com_mokowaas']
|
||||
);
|
||||
|
||||
$router->createCRUDRoutes(
|
||||
'v1/mokowaas/install',
|
||||
'install',
|
||||
['component' => 'com_mokowaas']
|
||||
);
|
||||
|
||||
$router->createCRUDRoutes(
|
||||
'v1/mokowaas/reset',
|
||||
'reset',
|
||||
['component' => 'com_mokowaas']
|
||||
);
|
||||
|
||||
$router->createCRUDRoutes(
|
||||
'v1/mokowaas/snapshot',
|
||||
'snapshot',
|
||||
['component' => 'com_mokowaas']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_webservices_perfectpublisher/services/provider.php
|
||||
* VERSION: 02.13.01
|
||||
* VERSION: 02.21.01
|
||||
* BRIEF: DI service provider for Perfect Publisher Web Services plugin
|
||||
*/
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* INGROUP: MokoWaaS
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS
|
||||
* PATH: /src/packages/plg_webservices_perfectpublisher/src/Extension/PerfectPublisherApi.php
|
||||
* VERSION: 02.13.01
|
||||
* VERSION: 02.21.01
|
||||
* BRIEF: Web Services API plugin for Perfect Publisher (com_autotweet)
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>MokoWaaS</name>
|
||||
<packagename>mokowaas</packagename>
|
||||
<version>02.20.00</version>
|
||||
<version>02.21.01-dev</version>
|
||||
<creationDate>2026-05-23</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
@@ -17,6 +17,7 @@
|
||||
<file type="component" id="com_mokowaas">com_mokowaas.zip</file>
|
||||
<file type="plugin" id="plg_webservices_mokowaas" group="webservices">plg_webservices_mokowaas.zip</file>
|
||||
<file type="plugin" id="plg_webservices_perfectpublisher" group="webservices">plg_webservices_perfectpublisher.zip</file>
|
||||
<file type="plugin" id="plg_task_mokowaasdemo" group="task">plg_task_mokowaasdemo.zip</file>
|
||||
</files>
|
||||
|
||||
<updateservers>
|
||||
|
||||
@@ -36,6 +36,7 @@ class Pkg_MokowaasInstallerScript
|
||||
{
|
||||
$this->enablePlugin('system', 'mokowaas');
|
||||
$this->enablePlugin('webservices', 'mokowaas');
|
||||
$this->enablePlugin('task', 'mokowaasdemo');
|
||||
|
||||
// Mark MokoWaaS extensions as protected (prevents disable/uninstall at framework level)
|
||||
$this->protectExtensions();
|
||||
|
||||
+109
-6
@@ -164,7 +164,7 @@ Requesting an unknown action returns HTTP 400 with the list of available actions
|
||||
{
|
||||
"error": "Unknown action",
|
||||
"action": "invalid",
|
||||
"available": ["health", "install", "update", "cache", "backup", "info"]
|
||||
"available": ["health", "install", "update", "cache", "backup", "info", "reset", "snapshot"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -172,10 +172,113 @@ Requesting an unknown action returns HTTP 400 with the list of available actions
|
||||
|
||||
In addition to the query-string endpoints above, MokoWaaS registers standard Joomla API routes via the `plg_webservices_mokowaas` plugin:
|
||||
|
||||
| Route | Controller |
|
||||
|---|---|
|
||||
| `GET /api/v1/mokowaas/health` | HealthController |
|
||||
| `POST /api/v1/mokowaas/cache` | CacheController |
|
||||
| `POST /api/v1/mokowaas/update` | UpdateController |
|
||||
| Route | Controller | Description |
|
||||
|---|---|---|
|
||||
| `GET /api/v1/mokowaas/health` | HealthController | Full site health diagnostics |
|
||||
| `POST /api/v1/mokowaas/cache` | CacheController | Clear all caches |
|
||||
| `POST /api/v1/mokowaas/update` | UpdateController | Trigger update check |
|
||||
| `POST /api/v1/mokowaas/install` | InstallController | Install extension from ZIP URL |
|
||||
| `POST /api/v1/mokowaas/reset` | ResetController | Restore site to baseline snapshot |
|
||||
| `GET/POST /api/v1/mokowaas/snapshot` | SnapshotController | List or create snapshots |
|
||||
|
||||
These routes use Joomla's standard API authentication (API token in `X-Joomla-Token` header) and are useful for integrations that already use the Joomla API framework.
|
||||
|
||||
### Install Endpoint (REST API)
|
||||
|
||||
```
|
||||
POST /api/index.php/v1/mokowaas/install
|
||||
X-Joomla-Token: <api-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"url": "https://git.mokoconsulting.tech/.../plg_system_mokowaas-02.20.00.zip"}
|
||||
```
|
||||
|
||||
Downloads the ZIP from the given URL and installs it via Joomla's Installer. Requires a Joomla API token with `core.manage` permission on `com_installer`.
|
||||
|
||||
**Constraints:**
|
||||
- URL must use `http` or `https` scheme
|
||||
- URL path must end in `.zip`
|
||||
- Maximum download size: 64 MB
|
||||
|
||||
**Success Response** (HTTP 200):
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "Extension installed successfully",
|
||||
"extension": {
|
||||
"name": "MokoWaaS",
|
||||
"version": "02.20.00",
|
||||
"type": "plugin"
|
||||
},
|
||||
"source_url": "https://git.mokoconsulting.tech/.../plg_system_mokowaas-02.20.00.zip"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| HTTP Status | Condition |
|
||||
|---|---|
|
||||
| 400 | Missing `url` field, non-HTTPS URL, or URL not ending in `.zip` |
|
||||
| 403 | API token lacks `core.manage` on `com_installer` |
|
||||
| 405 | Request method is not POST |
|
||||
| 500 | Download or installation failed |
|
||||
|
||||
### Reset Endpoint (REST API)
|
||||
|
||||
```
|
||||
POST /api/index.php/v1/mokowaas/reset
|
||||
X-Joomla-Token: <api-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"baseline": "default"}
|
||||
```
|
||||
|
||||
Restores the site to a named baseline snapshot. The `baseline` field is optional and defaults to the active baseline configured in the plugin.
|
||||
|
||||
Requires `core.manage` permission on `com_plugins`.
|
||||
|
||||
**Success Response** (HTTP 200):
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "Site restored to baseline: default",
|
||||
"baseline": "default",
|
||||
"restored_tables": 15,
|
||||
"media_restored": true
|
||||
}
|
||||
```
|
||||
|
||||
### Snapshot Endpoint (REST API)
|
||||
|
||||
**List snapshots:**
|
||||
|
||||
```
|
||||
GET /api/index.php/v1/mokowaas/snapshot
|
||||
X-Joomla-Token: <api-token>
|
||||
```
|
||||
|
||||
**Create snapshot:**
|
||||
|
||||
```
|
||||
POST /api/index.php/v1/mokowaas/snapshot
|
||||
X-Joomla-Token: <api-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"name": "my-baseline"}
|
||||
```
|
||||
|
||||
The `name` field is optional and defaults to the active baseline name.
|
||||
|
||||
**Success Response** (HTTP 200):
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "Snapshot created",
|
||||
"name": "my-baseline",
|
||||
"tables": 15,
|
||||
"has_media": true
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user