feat: Moko Extensions manager - browse and install packages
New view at index.php?option=com_mokowaas&view=extensions showing: - Curated catalog of Moko Consulting Joomla packages - Install status (installed, update available, not installed) - Local vs remote version comparison - One-click install/update from Gitea releases - Repo link for each package - Grouped by category (Platform, Templates, Components, Modules, Plugins) - Quick access button on the dashboard Catalog includes: MokoWaaS, MokoOnyx, MokoCassiopeia, MokoJoomTOS, MokoJoomHero, MokoWaaSAnnounce, MokoDPCalendarAPI, MokoGalleryCalendar Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,3 +16,6 @@ COM_MOKOWAAS_CONFIGURE="Configure"
|
||||
COM_MOKOWAAS_TOGGLE_SUCCESS="Plugin state updated."
|
||||
COM_MOKOWAAS_TOGGLE_FAIL="Failed to update plugin state."
|
||||
COM_MOKOWAAS_CACHE_CLEARED="Cache cleared successfully."
|
||||
COM_MOKOWAAS_EXTENSIONS_TITLE="Moko Extensions"
|
||||
COM_MOKOWAAS_EXTENSIONS_INFO="Install and manage Moko Consulting Joomla packages. Extensions are downloaded from the official Gitea release server."
|
||||
COM_MOKOWAAS_EXTENSIONS_LINK="Moko Extensions"
|
||||
|
||||
@@ -88,4 +88,38 @@ class DisplayController extends BaseController
|
||||
echo json_encode($result);
|
||||
$app->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a Moko extension from a download URL.
|
||||
*/
|
||||
public function installExtension()
|
||||
{
|
||||
Session::checkToken() or die(Text::_('JINVALID_TOKEN'));
|
||||
|
||||
$app = Factory::getApplication();
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user->authorise('core.admin'))
|
||||
{
|
||||
$app->setHeader('Content-Type', 'application/json');
|
||||
echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]);
|
||||
$app->close();
|
||||
}
|
||||
|
||||
$downloadUrl = $app->getInput()->getString('download_url', '');
|
||||
|
||||
if (empty($downloadUrl))
|
||||
{
|
||||
$app->setHeader('Content-Type', 'application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Missing download URL.']);
|
||||
$app->close();
|
||||
}
|
||||
|
||||
$model = $this->getModel('Extensions');
|
||||
$result = $model->installFromUrl($downloadUrl);
|
||||
|
||||
$app->setHeader('Content-Type', 'application/json');
|
||||
echo json_encode($result);
|
||||
$app->close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
<?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\Administrator\Model;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
|
||||
/**
|
||||
* Extension manager model — fetches Moko Consulting Joomla packages
|
||||
* from the Gitea API and checks local install status.
|
||||
*
|
||||
* @since 02.32.00
|
||||
*/
|
||||
class ExtensionsModel extends BaseDatabaseModel
|
||||
{
|
||||
/**
|
||||
* Curated catalog of Moko Consulting Joomla packages.
|
||||
* Each entry maps a Gitea repo name to local extension metadata.
|
||||
*/
|
||||
private const CATALOG = [
|
||||
'MokoWaaS' => [
|
||||
'label' => 'MokoWaaS',
|
||||
'description' => 'Admin dashboard, security firewall, tenant restrictions, health monitoring, and REST API.',
|
||||
'element' => 'pkg_mokowaas',
|
||||
'type' => 'package',
|
||||
'icon' => 'icon-shield-alt',
|
||||
'category' => 'Platform',
|
||||
],
|
||||
'MokoOnyx' => [
|
||||
'label' => 'MokoOnyx',
|
||||
'description' => 'Modern Joomla site template with dark mode, custom layouts, and MokoWaaS integration.',
|
||||
'element' => 'mokoonyx',
|
||||
'type' => 'template',
|
||||
'icon' => 'icon-paint-brush',
|
||||
'category' => 'Templates',
|
||||
],
|
||||
'MokoCassiopeia' => [
|
||||
'label' => 'MokoCassiopeia',
|
||||
'description' => 'Enhancement layer for Joomla\'s Cassiopeia template.',
|
||||
'element' => 'mokocassiopeia',
|
||||
'type' => 'template',
|
||||
'icon' => 'icon-paint-brush',
|
||||
'category' => 'Templates',
|
||||
],
|
||||
'MokoJoomTOS' => [
|
||||
'label' => 'MokoJoomTOS',
|
||||
'description' => 'Terms of Service and privacy policy component with consent tracking.',
|
||||
'element' => 'com_mokojoomtos',
|
||||
'type' => 'component',
|
||||
'icon' => 'icon-file-contract',
|
||||
'category' => 'Components',
|
||||
],
|
||||
'MokoJoomHero' => [
|
||||
'label' => 'MokoJoomHero',
|
||||
'description' => 'Random hero image module from a configurable folder.',
|
||||
'element' => 'mod_mokojoomhero',
|
||||
'type' => 'module',
|
||||
'icon' => 'icon-image',
|
||||
'category' => 'Modules',
|
||||
],
|
||||
'MokoWaaSAnnounce' => [
|
||||
'label' => 'MokoWaaS Announce',
|
||||
'description' => 'Centralized announcement system via admin module.',
|
||||
'element' => 'mod_mokowaas_announce',
|
||||
'type' => 'module',
|
||||
'icon' => 'icon-bullhorn',
|
||||
'category' => 'Modules',
|
||||
],
|
||||
'MokoDPCalendarAPI' => [
|
||||
'label' => 'DPCalendar API',
|
||||
'description' => 'Web Services plugin exposing DPCalendar events and calendars via REST API.',
|
||||
'element' => 'mokodpcalendarapi',
|
||||
'type' => 'plugin',
|
||||
'icon' => 'icon-calendar',
|
||||
'category' => 'Plugins',
|
||||
],
|
||||
'MokoGalleryCalendar' => [
|
||||
'label' => 'Gallery Calendar',
|
||||
'description' => 'JoomGallery and DPCalendar integration — link galleries to events.',
|
||||
'element' => 'mokogallerycalendar',
|
||||
'type' => 'plugin',
|
||||
'icon' => 'icon-images',
|
||||
'category' => 'Plugins',
|
||||
],
|
||||
];
|
||||
|
||||
private const GITEA_URL = 'https://git.mokoconsulting.tech';
|
||||
private const GITEA_ORG = 'MokoConsulting';
|
||||
|
||||
/**
|
||||
* Get the full catalog with install status and release info.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCatalog(): array
|
||||
{
|
||||
$installed = $this->getInstalledVersions();
|
||||
$packages = [];
|
||||
|
||||
foreach (self::CATALOG as $repo => $meta)
|
||||
{
|
||||
$release = $this->fetchLatestRelease($repo);
|
||||
|
||||
$localVersion = $installed[$meta['element']] ?? null;
|
||||
$remoteVersion = $release['version'] ?? '';
|
||||
$downloadUrl = $release['download_url'] ?? '';
|
||||
|
||||
$status = 'not_installed';
|
||||
|
||||
if ($localVersion !== null)
|
||||
{
|
||||
$status = 'installed';
|
||||
|
||||
if ($remoteVersion && version_compare(
|
||||
preg_replace('/[^0-9.]/', '', $remoteVersion),
|
||||
preg_replace('/[^0-9.]/', '', $localVersion),
|
||||
'>'
|
||||
))
|
||||
{
|
||||
$status = 'update_available';
|
||||
}
|
||||
}
|
||||
|
||||
$packages[] = (object) [
|
||||
'repo' => $repo,
|
||||
'label' => $meta['label'],
|
||||
'description' => $meta['description'],
|
||||
'element' => $meta['element'],
|
||||
'type' => $meta['type'],
|
||||
'icon' => $meta['icon'],
|
||||
'category' => $meta['category'],
|
||||
'local_version' => $localVersion ?? '',
|
||||
'remote_version' => $remoteVersion,
|
||||
'download_url' => $downloadUrl,
|
||||
'status' => $status,
|
||||
'repo_url' => self::GITEA_URL . '/' . self::GITEA_ORG . '/' . $repo,
|
||||
];
|
||||
}
|
||||
|
||||
return $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an extension from a remote ZIP URL.
|
||||
*
|
||||
* @param string $url The download URL.
|
||||
*
|
||||
* @return array Result with success, message, and extension info.
|
||||
*/
|
||||
public function installFromUrl(string $url): array
|
||||
{
|
||||
$tmpPath = Factory::getConfig()->get('tmp_path', JPATH_ROOT . '/tmp');
|
||||
$tmpFile = $tmpPath . '/mokowaas_install_' . md5($url) . '.zip';
|
||||
|
||||
try
|
||||
{
|
||||
// Download
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$data = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error || $code !== 200 || empty($data))
|
||||
{
|
||||
return ['success' => false, 'message' => 'Download failed: ' . ($error ?: "HTTP {$code}")];
|
||||
}
|
||||
|
||||
file_put_contents($tmpFile, $data);
|
||||
|
||||
// Install via Joomla Installer
|
||||
$installer = new \Joomla\CMS\Installer\Installer();
|
||||
$result = $installer->install($tmpFile);
|
||||
|
||||
@unlink($tmpFile);
|
||||
|
||||
if (!$result)
|
||||
{
|
||||
return ['success' => false, 'message' => 'Installation failed.'];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Installed successfully.',
|
||||
];
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
@unlink($tmpFile);
|
||||
|
||||
return ['success' => false, 'message' => 'Error: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installed versions of all Moko extensions.
|
||||
*
|
||||
* @return array element => version
|
||||
*/
|
||||
private function getInstalledVersions(): array
|
||||
{
|
||||
$db = $this->getDatabase();
|
||||
$elements = [];
|
||||
|
||||
foreach (self::CATALOG as $meta)
|
||||
{
|
||||
$elements[] = $db->quote($meta['element']);
|
||||
}
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select([$db->quoteName('element'), $db->quoteName('manifest_cache')])
|
||||
->from($db->quoteName('#__extensions'))
|
||||
->where($db->quoteName('element') . ' IN (' . implode(',', $elements) . ')');
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadObjectList() ?: [];
|
||||
|
||||
$versions = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$mc = json_decode($row->manifest_cache ?? '{}');
|
||||
$versions[$row->element] = $mc->version ?? '0.0.0';
|
||||
}
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest release from Gitea for a repo.
|
||||
*
|
||||
* @param string $repo Repository name.
|
||||
*
|
||||
* @return array [version, download_url] or empty.
|
||||
*/
|
||||
private function fetchLatestRelease(string $repo): array
|
||||
{
|
||||
$url = self::GITEA_URL . '/api/v1/repos/' . self::GITEA_ORG . '/' . $repo . '/releases?limit=1';
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
|
||||
$response = curl_exec($ch);
|
||||
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code !== 200 || empty($response))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$releases = json_decode($response, true);
|
||||
|
||||
if (empty($releases[0]))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$release = $releases[0];
|
||||
$version = $release['tag_name'] ?? '';
|
||||
|
||||
// Find the first .zip asset
|
||||
$downloadUrl = '';
|
||||
|
||||
foreach ($release['assets'] ?? [] as $asset)
|
||||
{
|
||||
if (str_ends_with(strtolower($asset['name'] ?? ''), '.zip'))
|
||||
{
|
||||
$downloadUrl = $asset['browser_download_url'] ?? '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => $version,
|
||||
'download_url' => $downloadUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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\Administrator\View\Extensions;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
protected $packages = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$model = $this->getModel();
|
||||
|
||||
$this->packages = $model->getCatalog();
|
||||
|
||||
$this->addToolbar();
|
||||
|
||||
$wa = Factory::getApplication()->getDocument()->getWebAssetManager();
|
||||
$wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css');
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
|
||||
protected function addToolbar(): void
|
||||
{
|
||||
ToolbarHelper::title(Text::_('COM_MOKOWAAS_EXTENSIONS_TITLE'), 'puzzle-piece');
|
||||
ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas');
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,10 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api'];
|
||||
<span class="icon-refresh" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOWAAS_CHECK_UPDATES'); ?>
|
||||
</a>
|
||||
<a href="<?php echo Route::_('index.php?option=com_mokowaas&view=extensions'); ?>" class="btn btn-outline-primary btn-sm">
|
||||
<span class="icon-puzzle-piece" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOWAAS_EXTENSIONS_LINK'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<?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
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
|
||||
/** @var \Moko\Component\MokoWaaS\Administrator\View\Extensions\HtmlView $this */
|
||||
|
||||
$packages = $this->packages;
|
||||
$token = Session::getFormToken();
|
||||
|
||||
// Group by category
|
||||
$grouped = [];
|
||||
foreach ($packages as $pkg)
|
||||
{
|
||||
$grouped[$pkg->category][] = $pkg;
|
||||
}
|
||||
|
||||
$statusBadge = [
|
||||
'installed' => ['bg-success', 'Installed'],
|
||||
'update_available' => ['bg-warning text-dark', 'Update Available'],
|
||||
'not_installed' => ['bg-secondary', 'Not Installed'],
|
||||
];
|
||||
?>
|
||||
|
||||
<div id="mokowaas-extensions">
|
||||
<div class="alert alert-info">
|
||||
<span class="icon-info-circle" aria-hidden="true"></span>
|
||||
<?php echo Text::_('COM_MOKOWAAS_EXTENSIONS_INFO'); ?>
|
||||
</div>
|
||||
|
||||
<?php foreach ($grouped as $category => $pkgs): ?>
|
||||
<h3 class="mb-3"><?php echo htmlspecialchars($category); ?></h3>
|
||||
<div class="row g-3 mb-4">
|
||||
<?php foreach ($pkgs as $pkg): ?>
|
||||
<?php
|
||||
$badge = $statusBadge[$pkg->status] ?? $statusBadge['not_installed'];
|
||||
?>
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="d-flex align-items-start justify-content-between mb-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="<?php echo htmlspecialchars($pkg->icon); ?>" aria-hidden="true" style="font-size:1.5rem;color:#1a2744"></span>
|
||||
<div>
|
||||
<h5 class="card-title mb-0"><?php echo htmlspecialchars($pkg->label); ?></h5>
|
||||
<small class="text-muted"><?php echo htmlspecialchars($pkg->type); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge <?php echo $badge[0]; ?>"><?php echo $badge[1]; ?></span>
|
||||
</div>
|
||||
|
||||
<p class="card-text text-muted flex-grow-1"><?php echo htmlspecialchars($pkg->description); ?></p>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mt-auto pt-2 border-top">
|
||||
<div class="small text-muted">
|
||||
<?php if ($pkg->local_version): ?>
|
||||
v<?php echo htmlspecialchars($pkg->local_version); ?>
|
||||
<?php if ($pkg->remote_version && $pkg->status === 'update_available'): ?>
|
||||
<span class="icon-arrow-right" aria-hidden="true"></span>
|
||||
<span class="text-warning"><?php echo htmlspecialchars($pkg->remote_version); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($pkg->remote_version): ?>
|
||||
Latest: <?php echo htmlspecialchars($pkg->remote_version); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="<?php echo htmlspecialchars($pkg->repo_url); ?>" target="_blank" class="btn btn-sm btn-outline-secondary" title="View on Gitea">
|
||||
<span class="icon-code-branch" aria-hidden="true"></span>
|
||||
</a>
|
||||
<?php if ($pkg->download_url && $pkg->status !== 'installed'): ?>
|
||||
<button type="button" class="btn btn-sm btn-primary mokowaas-install-btn"
|
||||
data-url="<?php echo Route::_('index.php?option=com_mokowaas&task=display.installExtension&format=json'); ?>"
|
||||
data-download="<?php echo htmlspecialchars($pkg->download_url); ?>"
|
||||
data-token="<?php echo $token; ?>"
|
||||
data-label="<?php echo htmlspecialchars($pkg->label); ?>">
|
||||
<span class="icon-download" aria-hidden="true"></span>
|
||||
<?php echo $pkg->status === 'update_available' ? 'Update' : 'Install'; ?>
|
||||
</button>
|
||||
<?php elseif ($pkg->status === 'installed'): ?>
|
||||
<span class="btn btn-sm btn-outline-success disabled">
|
||||
<span class="icon-check" aria-hidden="true"></span> Installed
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="btn btn-sm btn-outline-secondary disabled">No release</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.mokowaas-install-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var el = this;
|
||||
var url = el.dataset.url;
|
||||
var downloadUrl = el.dataset.download;
|
||||
var token = el.dataset.token;
|
||||
var label = el.dataset.label;
|
||||
|
||||
if (!confirm('Install ' + label + '?')) return;
|
||||
|
||||
el.disabled = true;
|
||||
var origHtml = el.textContent;
|
||||
el.textContent = ' Installing...';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('download_url', downloadUrl);
|
||||
fd.append(token, '1');
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.success) {
|
||||
Joomla.renderMessages({message: [label + ': ' + d.message]});
|
||||
location.reload();
|
||||
} else {
|
||||
Joomla.renderMessages({error: [label + ': ' + (d.message || 'Failed')]});
|
||||
el.disabled = false;
|
||||
el.textContent = origHtml;
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
Joomla.renderMessages({error: ['Network error']});
|
||||
el.disabled = false;
|
||||
el.textContent = origHtml;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user