feat(api): add extensions list endpoint with filters and update server info
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Site Health (push) Has been cancelled
Generic: Repo Health / Access control (push) Has been cancelled
Universal: Auto Version Bump / Version Bump (push) Has been cancelled
Update Server / Update Server (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
GET /?mokowaas=extensions and GET /api/v1/mokowaas/extensions returns all installed extensions with version, enabled/protected/locked status, and update server details. Supports ?type, ?search, and ?enabled filters. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
- Content Sync: configurable sync targets with URL + API token in plugin settings
|
||||
- Package installer: protect all MokoWaaS extensions (not just system plugin) and ensure update server stays enabled
|
||||
- Package installer: clean up legacy `mokowaasbrand` extension entries and files on install/update
|
||||
- API endpoint `GET /?mokowaas=extensions` and `GET /api/v1/mokowaas/extensions` — list installed extensions with version, status, and update server info
|
||||
|
||||
## [02.20.00] --- 2026-05-28
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Extensions list API controller.
|
||||
*
|
||||
* GET /api/index.php/v1/mokowaas/extensions
|
||||
*
|
||||
* Returns all installed extensions with type, element, folder, version,
|
||||
* enabled/protected/locked status, and update server info.
|
||||
*
|
||||
* Optional filters via query params:
|
||||
* ?type=plugin — filter by extension type
|
||||
* ?search=moko — search name or element
|
||||
* ?enabled=1 — only enabled/disabled
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
class ExtensionsController extends BaseController
|
||||
{
|
||||
/**
|
||||
* List installed extensions.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.21.00
|
||||
*/
|
||||
public function displayList(): void
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user->authorise('core.manage', 'com_installer'))
|
||||
{
|
||||
$this->sendJson(403, ['error' => 'Not authorized — requires core.manage on com_installer']);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('e.extension_id'),
|
||||
$db->quoteName('e.name'),
|
||||
$db->quoteName('e.type'),
|
||||
$db->quoteName('e.element'),
|
||||
$db->quoteName('e.folder'),
|
||||
$db->quoteName('e.client_id'),
|
||||
$db->quoteName('e.enabled'),
|
||||
$db->quoteName('e.protected'),
|
||||
$db->quoteName('e.locked'),
|
||||
$db->quoteName('e.manifest_cache'),
|
||||
])
|
||||
->from($db->quoteName('#__extensions', 'e'))
|
||||
->order($db->quoteName('e.type') . ' ASC, ' . $db->quoteName('e.name') . ' ASC');
|
||||
|
||||
// Filter by type
|
||||
$typeFilter = $app->input->get('type', '', 'CMD');
|
||||
|
||||
if ($typeFilter !== '')
|
||||
{
|
||||
$query->where($db->quoteName('e.type') . ' = ' . $db->quote($typeFilter));
|
||||
}
|
||||
|
||||
// Filter by enabled
|
||||
$enabledFilter = $app->input->get('enabled', '', 'CMD');
|
||||
|
||||
if ($enabledFilter !== '')
|
||||
{
|
||||
$query->where($db->quoteName('e.enabled') . ' = ' . (int) $enabledFilter);
|
||||
}
|
||||
|
||||
// Search name or element
|
||||
$search = $app->input->get('search', '', 'STRING');
|
||||
|
||||
if ($search !== '')
|
||||
{
|
||||
$searchQuoted = $db->quote('%' . $db->escape($search, true) . '%');
|
||||
$query->where(
|
||||
'(' . $db->quoteName('e.name') . ' LIKE ' . $searchQuoted
|
||||
. ' OR ' . $db->quoteName('e.element') . ' LIKE ' . $searchQuoted . ')'
|
||||
);
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
// Get update sites for cross-reference
|
||||
$usQuery = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('us.update_site_id'),
|
||||
$db->quoteName('us.name', 'site_name'),
|
||||
$db->quoteName('us.location'),
|
||||
$db->quoteName('us.enabled', 'site_enabled'),
|
||||
$db->quoteName('usm.extension_id'),
|
||||
])
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->innerJoin(
|
||||
$db->quoteName('#__update_sites_extensions', 'usm')
|
||||
. ' ON ' . $db->quoteName('us.update_site_id')
|
||||
. ' = ' . $db->quoteName('usm.update_site_id')
|
||||
);
|
||||
$db->setQuery($usQuery);
|
||||
$updateSites = [];
|
||||
|
||||
foreach ($db->loadAssocList() ?: [] as $us)
|
||||
{
|
||||
$updateSites[(int) $us['extension_id']] = [
|
||||
'name' => $us['site_name'],
|
||||
'location' => $us['location'],
|
||||
'enabled' => (bool) $us['site_enabled'],
|
||||
];
|
||||
}
|
||||
|
||||
// Build response
|
||||
$extensions = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$manifest = json_decode($row['manifest_cache'] ?: '{}', true);
|
||||
$extId = (int) $row['extension_id'];
|
||||
|
||||
$ext = [
|
||||
'extension_id' => $extId,
|
||||
'name' => $row['name'],
|
||||
'type' => $row['type'],
|
||||
'element' => $row['element'],
|
||||
'folder' => $row['folder'] ?: null,
|
||||
'client_id' => (int) $row['client_id'],
|
||||
'enabled' => (bool) $row['enabled'],
|
||||
'protected' => (bool) $row['protected'],
|
||||
'locked' => (bool) $row['locked'],
|
||||
'version' => $manifest['version'] ?? null,
|
||||
'author' => $manifest['author'] ?? null,
|
||||
'description' => $manifest['description'] ?? null,
|
||||
];
|
||||
|
||||
if (isset($updateSites[$extId]))
|
||||
{
|
||||
$ext['update_server'] = $updateSites[$extId];
|
||||
}
|
||||
|
||||
$extensions[] = $ext;
|
||||
}
|
||||
|
||||
$this->sendJson(200, [
|
||||
'status' => 'ok',
|
||||
'count' => count($extensions),
|
||||
'extensions' => $extensions,
|
||||
]);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendJson(500, [
|
||||
'error' => 'Failed to list extensions',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
@@ -1567,11 +1567,14 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
case 'sync-receive':
|
||||
$this->handleSyncReceiveAction();
|
||||
break;
|
||||
case 'extensions':
|
||||
$this->handleExtensionsAction();
|
||||
break;
|
||||
default:
|
||||
$this->sendHealthResponse(400, [
|
||||
'error' => 'Unknown action',
|
||||
'action' => $action,
|
||||
'available' => ['health', 'install', 'update', 'cache', 'backup', 'info', 'reset', 'snapshot', 'sync', 'sync-receive'],
|
||||
'available' => ['health', 'install', 'update', 'cache', 'backup', 'info', 'reset', 'snapshot', 'sync', 'sync-receive', 'extensions'],
|
||||
]);
|
||||
break;
|
||||
}
|
||||
@@ -1773,6 +1776,136 @@ class MokoWaaS extends CMSPlugin implements BootableExtensionInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List installed extensions with version, status, and update server info.
|
||||
*
|
||||
* GET /?mokowaas=extensions
|
||||
* Optional: ?type=plugin&search=moko&enabled=1
|
||||
*
|
||||
* @return void
|
||||
* @since 02.21.00
|
||||
*/
|
||||
protected function handleExtensionsAction()
|
||||
{
|
||||
try
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
$input = $this->app->input;
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('e.extension_id'),
|
||||
$db->quoteName('e.name'),
|
||||
$db->quoteName('e.type'),
|
||||
$db->quoteName('e.element'),
|
||||
$db->quoteName('e.folder'),
|
||||
$db->quoteName('e.client_id'),
|
||||
$db->quoteName('e.enabled'),
|
||||
$db->quoteName('e.protected'),
|
||||
$db->quoteName('e.locked'),
|
||||
$db->quoteName('e.manifest_cache'),
|
||||
])
|
||||
->from($db->quoteName('#__extensions', 'e'))
|
||||
->order($db->quoteName('e.type') . ' ASC, ' . $db->quoteName('e.name') . ' ASC');
|
||||
|
||||
$typeFilter = $input->get('type', '', 'CMD');
|
||||
|
||||
if ($typeFilter !== '')
|
||||
{
|
||||
$query->where($db->quoteName('e.type') . ' = ' . $db->quote($typeFilter));
|
||||
}
|
||||
|
||||
$enabledFilter = $input->get('enabled', '', 'CMD');
|
||||
|
||||
if ($enabledFilter !== '')
|
||||
{
|
||||
$query->where($db->quoteName('e.enabled') . ' = ' . (int) $enabledFilter);
|
||||
}
|
||||
|
||||
$search = $input->get('search', '', 'STRING');
|
||||
|
||||
if ($search !== '')
|
||||
{
|
||||
$like = $db->quote('%' . $db->escape($search, true) . '%');
|
||||
$query->where(
|
||||
'(' . $db->quoteName('e.name') . ' LIKE ' . $like
|
||||
. ' OR ' . $db->quoteName('e.element') . ' LIKE ' . $like . ')'
|
||||
);
|
||||
}
|
||||
|
||||
$db->setQuery($query);
|
||||
$rows = $db->loadAssocList();
|
||||
|
||||
// Get update sites
|
||||
$usQuery = $db->getQuery(true)
|
||||
->select([
|
||||
$db->quoteName('us.name', 'site_name'),
|
||||
$db->quoteName('us.location'),
|
||||
$db->quoteName('us.enabled', 'site_enabled'),
|
||||
$db->quoteName('usm.extension_id'),
|
||||
])
|
||||
->from($db->quoteName('#__update_sites', 'us'))
|
||||
->innerJoin(
|
||||
$db->quoteName('#__update_sites_extensions', 'usm')
|
||||
. ' ON ' . $db->quoteName('us.update_site_id')
|
||||
. ' = ' . $db->quoteName('usm.update_site_id')
|
||||
);
|
||||
$db->setQuery($usQuery);
|
||||
$updateSites = [];
|
||||
|
||||
foreach ($db->loadAssocList() ?: [] as $us)
|
||||
{
|
||||
$updateSites[(int) $us['extension_id']] = [
|
||||
'name' => $us['site_name'],
|
||||
'location' => $us['location'],
|
||||
'enabled' => (bool) $us['site_enabled'],
|
||||
];
|
||||
}
|
||||
|
||||
$extensions = [];
|
||||
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$manifest = json_decode($row['manifest_cache'] ?: '{}', true);
|
||||
$extId = (int) $row['extension_id'];
|
||||
|
||||
$ext = [
|
||||
'extension_id' => $extId,
|
||||
'name' => $row['name'],
|
||||
'type' => $row['type'],
|
||||
'element' => $row['element'],
|
||||
'folder' => $row['folder'] ?: null,
|
||||
'client_id' => (int) $row['client_id'],
|
||||
'enabled' => (bool) $row['enabled'],
|
||||
'protected' => (bool) $row['protected'],
|
||||
'locked' => (bool) $row['locked'],
|
||||
'version' => $manifest['version'] ?? null,
|
||||
'author' => $manifest['author'] ?? null,
|
||||
];
|
||||
|
||||
if (isset($updateSites[$extId]))
|
||||
{
|
||||
$ext['update_server'] = $updateSites[$extId];
|
||||
}
|
||||
|
||||
$extensions[] = $ext;
|
||||
}
|
||||
|
||||
$this->sendHealthResponse(200, [
|
||||
'status' => 'ok',
|
||||
'count' => count($extensions),
|
||||
'extensions' => $extensions,
|
||||
]);
|
||||
}
|
||||
catch (\Throwable $e)
|
||||
{
|
||||
$this->sendHealthResponse(500, [
|
||||
'error' => 'Failed to list extensions',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger Joomla update finder check.
|
||||
*
|
||||
|
||||
@@ -94,5 +94,11 @@ final class MokoWaaSApi extends CMSPlugin implements SubscriberInterface
|
||||
'syncreceive',
|
||||
['component' => 'com_mokowaas']
|
||||
);
|
||||
|
||||
$router->createCRUDRoutes(
|
||||
'v1/mokowaas/extensions',
|
||||
'extensions',
|
||||
['component' => 'com_mokowaas']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ In addition to the query-string endpoints above, MokoWaaS registers standard Joo
|
||||
| `GET/POST /api/v1/mokowaas/snapshot` | SnapshotController | List or create snapshots |
|
||||
| `POST /api/v1/mokowaas/sync` | SyncController | Push content to all sync targets |
|
||||
| `POST /api/v1/mokowaas/sync-receive` | SyncReceiveController | Receive content from source site |
|
||||
| `GET /api/v1/mokowaas/extensions` | ExtensionsController | List installed extensions |
|
||||
|
||||
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.
|
||||
|
||||
@@ -284,3 +285,53 @@ The `name` field is optional and defaults to the active baseline name.
|
||||
"has_media": true
|
||||
}
|
||||
```
|
||||
|
||||
### Extensions Endpoint (REST API)
|
||||
|
||||
```
|
||||
GET /api/index.php/v1/mokowaas/extensions
|
||||
X-Joomla-Token: <api-token>
|
||||
```
|
||||
|
||||
Lists all installed Joomla extensions with version, enabled/protected/locked status, and update server info.
|
||||
|
||||
**Query filters:**
|
||||
|
||||
| Parameter | Description | Example |
|
||||
|---|---|---|
|
||||
| `type` | Filter by extension type | `?type=plugin` |
|
||||
| `search` | Search name or element | `?search=moko` |
|
||||
| `enabled` | Filter by enabled status | `?enabled=1` |
|
||||
|
||||
**Query-string equivalent:** `GET /?mokowaas=extensions&search=moko&type=plugin`
|
||||
|
||||
Requires `core.manage` on `com_installer`.
|
||||
|
||||
**Success Response** (HTTP 200):
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"count": 3,
|
||||
"extensions": [
|
||||
{
|
||||
"extension_id": 456,
|
||||
"name": "System - MokoWaaS",
|
||||
"type": "plugin",
|
||||
"element": "mokowaas",
|
||||
"folder": "system",
|
||||
"client_id": 0,
|
||||
"enabled": true,
|
||||
"protected": true,
|
||||
"locked": false,
|
||||
"version": "02.21.00",
|
||||
"author": "Moko Consulting",
|
||||
"update_server": {
|
||||
"name": "MokoWaaS Update Server",
|
||||
"location": "https://git.mokoconsulting.tech/.../updates.xml",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user