chore: cascade main → dev (e7de6e4) [skip ci] #22

Merged
jmiller merged 3 commits from main into dev 2026-05-23 05:09:28 +00:00
3 changed files with 85 additions and 902 deletions
+1 -1
View File
@@ -15,7 +15,7 @@
DEFGROUP: Joomla.Plugin
INGROUP: MokoWaaS
REPO: https://github.com/mokoconsulting-tech/mokowaas
VERSION: 02.01.36
VERSION: 02.01.35
PATH: /README.md
BRIEF: Rebranding plugin for MokoWaaS platform
NOTE: Internal WaaS identity abstraction layer
+8 -897
View File
@@ -1090,89 +1090,25 @@ class MokoWaaS extends CMSPlugin
// Collect diagnostics
$checks = $this->collectHealthChecks();
// Determine overall status and collect reasons
// Determine overall status from individual checks
$overall = 'ok';
$reasons = [];
foreach ($checks as $name => $check)
foreach ($checks as $check)
{
$checkStatus = $check['status'] ?? 'ok';
if ($checkStatus === 'error')
if (($check['status'] ?? 'ok') === 'error')
{
$overall = 'error';
$reasons[] = $name . ': ' . ($check['message'] ?? 'error');
break;
}
elseif ($checkStatus === 'degraded')
{
if ($overall !== 'error')
{
$overall = 'degraded';
}
// Build human-readable reason
if ($name === 'extensions'
&& isset($check['pending_updates']))
{
$reasons[] = $check['pending_updates']
. ' extension update'
. ($check['pending_updates'] > 1 ? 's' : '')
. ' available';
}
elseif ($name === 'filesystem'
&& isset($check['free_disk_mb'])
&& $check['free_disk_mb'] < 100)
{
$reasons[] = 'Low disk space: '
. $check['free_disk_mb'] . ' MB free';
}
elseif ($name === 'backup')
{
if (!empty($check['message']))
{
$reasons[] = $check['message'];
}
elseif (isset($check['days_since'])
&& $check['days_since'] > 7)
{
$reasons[] = 'Last backup '
. $check['days_since'] . ' days ago';
}
elseif (isset($check['last_status'])
&& $check['last_status'] !== 'complete')
{
$reasons[] = 'Last backup status: '
. $check['last_status'];
}
else
{
$reasons[] = 'Backup: degraded';
}
}
elseif ($name === 'ssl' && isset($check['days_left']))
{
$reasons[] = 'SSL expires in '
. $check['days_left'] . ' days';
}
elseif ($name === 'cron' && isset($check['failed_24h']))
{
$reasons[] = $check['failed_24h']
. ' scheduled task(s) failed';
}
elseif ($name === 'config' && !empty($check['issues']))
{
$reasons[] = implode(', ', $check['issues']);
}
else
{
$reasons[] = $name . ': degraded';
}
if (($check['status'] ?? 'ok') === 'degraded')
{
$overall = 'degraded';
}
}
$payload = [
'status' => $overall,
'reason' => implode('; ', $reasons) ?: null,
'timestamp' => gmdate('Y-m-d\TH:i:s\Z'),
'checks' => $checks,
'meta' => $this->collectHealthMeta(),
@@ -1193,26 +1129,12 @@ class MokoWaaS extends CMSPlugin
*/
protected function collectHealthChecks()
{
$checks = [
return [
'database' => $this->checkDatabase(),
'filesystem' => $this->checkFilesystem(),
'cache' => $this->checkCache(),
'extensions' => $this->checkExtensions(),
'backup' => $this->checkAkeebaBackup(),
'security' => $this->checkAdminTools(),
'ssl' => $this->checkSsl(),
'cron' => $this->checkScheduledTasks(),
'errors' => $this->checkErrorLog(),
'db_size' => $this->checkDatabaseSize(),
'content' => $this->checkContent(),
'users' => $this->checkUserActivity(),
'mail' => $this->checkMail(),
'seo' => $this->checkSeo(),
'template' => $this->checkTemplate(),
'config' => $this->checkConfigDrift(),
];
return $checks;
}
/**
@@ -1310,55 +1232,12 @@ class MokoWaaS extends CMSPlugin
$status = 'degraded';
}
// Total disk and site size
$totalBytes = @disk_total_space(JPATH_ROOT);
$totalMb = $totalBytes !== false
? round($totalBytes / 1048576)
: null;
// Site directory size (quick estimate via common dirs)
$siteMb = null;
try
{
$siteSize = 0;
foreach (['images', 'media', 'tmp', 'cache',
'administrator/logs', 'administrator/cache'] as $dir)
{
$path = JPATH_ROOT . '/' . $dir;
if (is_dir($path))
{
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$path,
\FilesystemIterator::SKIP_DOTS
)
);
foreach ($iter as $file)
{
$siteSize += $file->getSize();
}
}
}
$siteMb = round($siteSize / 1048576);
}
catch (\Exception $e)
{
// Ignore — siteMb stays null
}
return [
'status' => $status,
'tmp_writable' => $tmpWritable,
'log_writable' => $logWritable,
'cache_writable' => $cacheWritable,
'free_disk_mb' => $freeMb,
'total_disk_mb' => $totalMb,
'site_size_mb' => $siteMb,
];
}
@@ -1441,774 +1320,6 @@ class MokoWaaS extends CMSPlugin
}
}
/**
* Check Akeeba Backup status — last backup date, status, and profile.
*
* Queries the #__ak_stats table (Akeeba Backup) for the most recent
* backup record. Returns 'not_installed' if the table doesn't exist.
*
* @return array Check result with backup info
*
* @since 02.01.39
*/
protected function checkAkeebaBackup()
{
try
{
$db = Factory::getDbo();
// Check if Akeeba Backup is installed
$tables = $db->getTableList();
$prefix = $db->getPrefix();
$akTable = $prefix . 'ak_stats';
if (!in_array($akTable, $tables))
{
return [
'status' => 'ok',
'installed' => false,
];
}
// Get the most recent backup
$query = $db->getQuery(true)
->select([
$db->quoteName('id'),
$db->quoteName('description'),
$db->quoteName('status'),
$db->quoteName('backupstart'),
$db->quoteName('backupend'),
$db->quoteName('profile_id'),
$db->quoteName('total_size'),
])
->from($db->quoteName('#__ak_stats'))
->order($db->quoteName('id') . ' DESC');
$db->setQuery($query, 0, 1);
$latest = $db->loadObject();
if (!$latest)
{
return [
'status' => 'degraded',
'installed' => true,
'message' => 'No backups found',
];
}
// Count total backups and recent (last 7 days)
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__ak_stats'))
);
$totalBackups = (int) $db->loadResult();
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__ak_stats'))
->where($db->quoteName('backupstart')
. ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)')
);
$recentBackups = (int) $db->loadResult();
// Check if last backup is older than 7 days
$lastDate = $latest->backupstart;
$daysSince = (int) ((time() - strtotime($lastDate)) / 86400);
$backupSize = $latest->total_size
? round($latest->total_size / 1048576)
: null;
$status = 'ok';
if ($latest->status !== 'complete')
{
$status = 'degraded';
}
elseif ($daysSince > 7)
{
$status = 'degraded';
}
return [
'status' => $status,
'installed' => true,
'last_backup' => $lastDate,
'last_status' => $latest->status,
'last_size_mb' => $backupSize,
'days_since' => $daysSince,
'profile_id' => (int) $latest->profile_id,
'total_backups' => $totalBackups,
'recent_7d' => $recentBackups,
'description' => $latest->description,
];
}
catch (\Exception $e)
{
return [
'status' => 'ok',
'installed' => false,
];
}
}
/**
* Check Admin Tools status — WAF status, security exceptions.
*
* Queries Admin Tools tables for firewall status and recent blocks.
* Returns 'not_installed' if tables don't exist.
*
* @return array Check result with security info
*
* @since 02.01.39
*/
protected function checkAdminTools()
{
try
{
$db = Factory::getDbo();
$tables = $db->getTableList();
$prefix = $db->getPrefix();
// Check if Admin Tools is installed
$atTable = $prefix . 'admintools_log';
if (!in_array($atTable, $tables))
{
return [
'status' => 'ok',
'installed' => false,
];
}
// Count blocked requests in last 24h
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__admintools_log'))
->where($db->quoteName('logdate')
. ' >= DATE_SUB(NOW(), INTERVAL 1 DAY)')
);
$blocked24h = (int) $db->loadResult();
// Count blocked in last 7 days
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__admintools_log'))
->where($db->quoteName('logdate')
. ' >= DATE_SUB(NOW(), INTERVAL 7 DAY)')
);
$blocked7d = (int) $db->loadResult();
// Check WAF config if available
$wafEnabled = null;
$wafTable = $prefix . 'admintools_wafconfig';
if (in_array($wafTable, $tables))
{
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('value'))
->from($db->quoteName('#__admintools_wafconfig'))
->where($db->quoteName('key') . ' = '
. $db->quote('ipworkarounds'))
);
$wafEnabled = $db->loadResult() !== null;
}
return [
'status' => 'ok',
'installed' => true,
'blocked_24h' => $blocked24h,
'blocked_7d' => $blocked7d,
'waf_active' => $wafEnabled,
];
}
catch (\Exception $e)
{
return [
'status' => 'ok',
'installed' => false,
];
}
}
/**
* Check SSL certificate expiry.
*
* @return array
* @since 02.01.39
*/
protected function checkSsl()
{
try
{
$siteUrl = Uri::root();
$host = parse_url($siteUrl, PHP_URL_HOST);
if (empty($host) || parse_url($siteUrl, PHP_URL_SCHEME) !== 'https')
{
return ['status' => 'ok', 'https' => false];
}
$ctx = stream_context_create([
'ssl' => ['capture_peer_cert' => true, 'verify_peer' => false],
]);
$stream = @stream_socket_client(
"ssl://{$host}:443", $errno, $errstr, 10,
STREAM_CLIENT_CONNECT, $ctx
);
if (!$stream)
{
return ['status' => 'degraded', 'https' => true, 'message' => 'Cannot connect'];
}
$params = stream_context_get_params($stream);
$cert = openssl_x509_parse($params['options']['ssl']['peer_certificate']);
fclose($stream);
$expiresTs = $cert['validTo_time_t'] ?? 0;
$daysLeft = (int) (($expiresTs - time()) / 86400);
$issuer = $cert['issuer']['O'] ?? $cert['issuer']['CN'] ?? 'Unknown';
$status = $daysLeft < 7 ? 'error' : ($daysLeft < 30 ? 'degraded' : 'ok');
return [
'status' => $status,
'https' => true,
'expires' => gmdate('Y-m-d', $expiresTs),
'days_left' => $daysLeft,
'issuer' => $issuer,
];
}
catch (\Exception $e)
{
return ['status' => 'ok', 'https' => false];
}
}
/**
* Check Joomla scheduled tasks (Joomla 4.1+).
*
* @return array
* @since 02.01.39
*/
protected function checkScheduledTasks()
{
try
{
$db = Factory::getDbo();
$tables = $db->getTableList();
$prefix = $db->getPrefix();
if (!in_array($prefix . 'scheduler_tasks', $tables))
{
return ['status' => 'ok', 'available' => false];
}
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('state') . ' = 1')
);
$enabled = (int) $db->loadResult();
$db->setQuery(
$db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('last_execution'),
$db->quoteName('last_exit_code'),
$db->quoteName('next_execution'),
])
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('state') . ' = 1')
->order($db->quoteName('last_execution') . ' DESC')
);
$db->setQuery($db->getQuery(true), 0, 5);
// Re-run the query
$db->setQuery(
$db->getQuery(true)
->select([
$db->quoteName('title'),
$db->quoteName('last_execution'),
$db->quoteName('last_exit_code'),
$db->quoteName('next_execution'),
])
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('state') . ' = 1')
->order($db->quoteName('last_execution') . ' DESC'),
0, 1
);
$last = $db->loadObject();
// Count failed in last 24h
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__scheduler_tasks'))
->where($db->quoteName('last_exit_code') . ' != 0')
->where($db->quoteName('last_execution')
. ' >= DATE_SUB(NOW(), INTERVAL 1 DAY)')
);
$failed24h = (int) $db->loadResult();
$status = $failed24h > 0 ? 'degraded' : 'ok';
return [
'status' => $status,
'available' => true,
'enabled_tasks' => $enabled,
'failed_24h' => $failed24h,
'last_run' => $last->last_execution ?? null,
'last_exit_code' => $last ? (int) $last->last_exit_code : null,
'last_task' => $last->title ?? null,
];
}
catch (\Exception $e)
{
return ['status' => 'ok', 'available' => false];
}
}
/**
* Check PHP error log for recent errors.
*
* @return array
* @since 02.01.39
*/
protected function checkErrorLog()
{
$logFile = JPATH_ROOT . '/administrator/logs/error.php';
$altLog = ini_get('error_log');
$file = null;
if (file_exists($logFile) && is_readable($logFile))
{
$file = $logFile;
}
elseif ($altLog && file_exists($altLog) && is_readable($altLog))
{
$file = $altLog;
}
if (!$file)
{
return [
'status' => 'ok',
'log_available' => false,
];
}
$size = filesize($file);
$sizeMb = round($size / 1048576, 1);
// Count recent lines (tail last 50 lines, count errors)
$lines = file_exists($file) ? @file($file) : [];
$recent = array_slice($lines, -50);
$errors24h = 0;
$lastError = null;
$yesterday = date('Y-m-d', strtotime('-1 day'));
foreach ($recent as $line)
{
if (stripos($line, 'error') !== false
|| stripos($line, 'fatal') !== false)
{
$errors24h++;
$lastError = trim(substr($line, 0, 200));
}
}
return [
'status' => 'ok',
'log_available' => true,
'log_size_mb' => $sizeMb,
'recent_errors' => $errors24h,
'last_error' => $lastError,
];
}
/**
* Check database size and largest tables.
*
* @return array
* @since 02.01.39
*/
protected function checkDatabaseSize()
{
try
{
$db = Factory::getDbo();
$config = Factory::getConfig();
$dbName = $config->get('db');
$db->setQuery(
"SELECT ROUND(SUM(data_length + index_length) / 1048576, 1) AS size_mb "
. "FROM information_schema.tables WHERE table_schema = "
. $db->quote($dbName)
);
$totalMb = (float) $db->loadResult();
// Largest tables
$db->setQuery(
"SELECT table_name, "
. "ROUND((data_length + index_length) / 1048576, 1) AS size_mb "
. "FROM information_schema.tables "
. "WHERE table_schema = " . $db->quote($dbName)
. " ORDER BY (data_length + index_length) DESC LIMIT 5"
);
$largest = [];
foreach ($db->loadObjectList() as $t)
{
$largest[$t->table_name] = (float) $t->size_mb;
}
// Table count
$db->setQuery(
"SELECT COUNT(*) FROM information_schema.tables "
. "WHERE table_schema = " . $db->quote($dbName)
);
$tableCount = (int) $db->loadResult();
return [
'status' => 'ok',
'total_mb' => $totalMb,
'table_count' => $tableCount,
'largest' => $largest,
];
}
catch (\Exception $e)
{
return ['status' => 'ok', 'total_mb' => null];
}
}
/**
* Check content statistics.
*
* @return array
* @since 02.01.39
*/
protected function checkContent()
{
try
{
$db = Factory::getDbo();
$counts = [];
foreach ([
'articles' => '#__content',
'categories' => '#__categories',
'menu_items' => '#__menu',
'modules' => '#__modules',
'media' => '#__media_files',
] as $label => $table)
{
try
{
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName($table))
);
$counts[$label] = (int) $db->loadResult();
}
catch (\Exception $e)
{
// Table might not exist
}
}
return [
'status' => 'ok',
'counts' => $counts,
];
}
catch (\Exception $e)
{
return ['status' => 'ok', 'counts' => []];
}
}
/**
* Check user activity — last login, active sessions, failed logins.
*
* @return array
* @since 02.01.39
*/
protected function checkUserActivity()
{
try
{
$db = Factory::getDbo();
// Total users
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__users'))
);
$totalUsers = (int) $db->loadResult();
// Last login
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('lastvisitDate'))
->from($db->quoteName('#__users'))
->where($db->quoteName('lastvisitDate')
. ' IS NOT NULL')
->order($db->quoteName('lastvisitDate') . ' DESC'),
0, 1
);
$lastLogin = $db->loadResult();
// Active sessions
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__session'))
->where($db->quoteName('guest') . ' = 0')
);
$activeSessions = (int) $db->loadResult();
// Failed logins (from action logs if available)
$failedLogins = 0;
try
{
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__action_logs'))
->where($db->quoteName('message_language_key')
. ' LIKE ' . $db->quote('%LOGIN_FAILED%'))
->where($db->quoteName('log_date')
. ' >= DATE_SUB(NOW(), INTERVAL 1 DAY)')
);
$failedLogins = (int) $db->loadResult();
}
catch (\Exception $e)
{
// Action logs might not track this
}
return [
'status' => 'ok',
'total_users' => $totalUsers,
'last_login' => $lastLogin,
'active_sessions' => $activeSessions,
'failed_24h' => $failedLogins,
];
}
catch (\Exception $e)
{
return ['status' => 'ok', 'total_users' => null];
}
}
/**
* Check mail system status.
*
* @return array
* @since 02.01.39
*/
protected function checkMail()
{
try
{
$config = Factory::getConfig();
$mailer = $config->get('mailer', 'mail');
$from = $config->get('mailfrom', '');
$smtpHost = $config->get('smtphost', '');
// Check mail queue if available
$db = Factory::getDbo();
$tables = $db->getTableList();
$prefix = $db->getPrefix();
$queueCount = 0;
if (in_array($prefix . 'mail_queue', $tables))
{
$db->setQuery(
$db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mail_queue'))
);
$queueCount = (int) $db->loadResult();
}
return [
'status' => 'ok',
'mailer' => $mailer,
'from' => $from,
'smtp_host' => $mailer === 'smtp' ? $smtpHost : null,
'queue' => $queueCount,
];
}
catch (\Exception $e)
{
return ['status' => 'ok', 'mailer' => null];
}
}
/**
* Check basic SEO health indicators.
*
* @return array
* @since 02.01.39
*/
protected function checkSeo()
{
$robotsTxt = file_exists(JPATH_ROOT . '/robots.txt');
$htaccess = file_exists(JPATH_ROOT . '/.htaccess');
// Check for sitemap
$sitemapXml = file_exists(JPATH_ROOT . '/sitemap.xml');
$sitemapIdx = file_exists(JPATH_ROOT . '/sitemap_index.xml');
$config = Factory::getConfig();
$sef = (bool) $config->get('sef', 0);
return [
'status' => 'ok',
'robots_txt' => $robotsTxt,
'htaccess' => $htaccess,
'sitemap' => $sitemapXml || $sitemapIdx,
'sef_enabled' => $sef,
];
}
/**
* Check active template info.
*
* @return array
* @since 02.01.39
*/
protected function checkTemplate()
{
try
{
$db = Factory::getDbo();
// Site template
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('template'))
->from($db->quoteName('#__template_styles'))
->where($db->quoteName('client_id') . ' = 0')
->where($db->quoteName('home') . ' = 1')
);
$siteTemplate = $db->loadResult() ?: 'unknown';
// Admin template
$db->setQuery(
$db->getQuery(true)
->select($db->quoteName('template'))
->from($db->quoteName('#__template_styles'))
->where($db->quoteName('client_id') . ' = 1')
->where($db->quoteName('home') . ' = 1')
);
$adminTemplate = $db->loadResult() ?: 'unknown';
// Count template overrides
$overrideCount = 0;
$overridePath = JPATH_ROOT . '/templates/' . $siteTemplate . '/html';
if (is_dir($overridePath))
{
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
$overridePath,
\FilesystemIterator::SKIP_DOTS
)
);
foreach ($iter as $file)
{
if ($file->isFile())
{
$overrideCount++;
}
}
}
return [
'status' => 'ok',
'site_template' => $siteTemplate,
'admin_template' => $adminTemplate,
'override_count' => $overrideCount,
];
}
catch (\Exception $e)
{
return ['status' => 'ok', 'site_template' => null];
}
}
/**
* Check configuration for common misconfigurations.
*
* @return array
* @since 02.01.39
*/
protected function checkConfigDrift()
{
$config = Factory::getConfig();
$debug = (bool) $config->get('debug', 0);
$errorReport = $config->get('error_reporting', 'default');
$gzip = (bool) $config->get('gzip', 0);
$sef = (bool) $config->get('sef', 0);
$sefRewrite = (bool) $config->get('sef_rewrite', 0);
$forceSSL = (int) $config->get('force_ssl', 0);
$caching = (bool) $config->get('caching', 0);
$lifetime = (int) $config->get('lifetime', 15);
$tmpPath = $config->get('tmp_path', '');
$logPath = $config->get('log_path', '');
// Flag potential issues
$issues = [];
if ($debug)
{
$issues[] = 'Debug mode is ON';
}
if ($errorReport === 'maximum'
|| $errorReport === 'development')
{
$issues[] = 'Error reporting: ' . $errorReport;
}
if ($forceSSL === 0)
{
$issues[] = 'Force SSL is OFF';
}
$status = empty($issues) ? 'ok' : 'degraded';
return [
'status' => $status,
'debug' => $debug,
'error_report' => $errorReport,
'gzip' => $gzip,
'sef' => $sef,
'sef_rewrite' => $sefRewrite,
'force_ssl' => $forceSSL,
'caching' => $caching,
'lifetime' => $lifetime,
'issues' => $issues ?: null,
];
}
/**
* Send a JSON health response and terminate execution.
*
+76 -4
View File
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 02.01.36-dev
VERSION: 02.01.37
-->
<updates>
@@ -10,15 +10,87 @@
<description>System - MokoWaaS update</description>
<element>mokowaas</element>
<type>plugin</type>
<version>02.01.36-dev</version>
<version>02.01.37-dev</version>
<client>site</client>
<folder>system</folder>
<tags><tag>development</tag></tags>
<infourl title="System - MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.36-dev.zip</downloadurl>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.37-dev.zip</downloadurl>
</downloads>
<sha256>9f7e0df50d0073ecb69ba3c13c22aec07b584e2f09127fc36bcf7eeeef534481</sha256>
<sha256>9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - MokoWaaS</name>
<description>System - MokoWaaS update</description>
<element>mokowaas</element>
<type>plugin</type>
<version>02.01.37-alpha</version>
<client>site</client>
<folder>system</folder>
<tags><tag>alpha</tag></tags>
<infourl title="System - MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/alpha</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/alpha/plg_system_mokowaas-02.01.37-alpha.zip</downloadurl>
</downloads>
<sha256>9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - MokoWaaS</name>
<description>System - MokoWaaS update</description>
<element>mokowaas</element>
<type>plugin</type>
<version>02.01.37-beta</version>
<client>site</client>
<folder>system</folder>
<tags><tag>beta</tag></tags>
<infourl title="System - MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/beta</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/beta/plg_system_mokowaas-02.01.37-beta.zip</downloadurl>
</downloads>
<sha256>9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - MokoWaaS</name>
<description>System - MokoWaaS update</description>
<element>mokowaas</element>
<type>plugin</type>
<version>02.01.37-rc</version>
<client>site</client>
<folder>system</folder>
<tags><tag>rc</tag></tags>
<infourl title="System - MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/rc</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/rc/plg_system_mokowaas-02.01.37-rc.zip</downloadurl>
</downloads>
<sha256>9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
</update>
<update>
<name>System - MokoWaaS</name>
<description>System - MokoWaaS update</description>
<element>mokowaas</element>
<type>plugin</type>
<version>02.01.37</version>
<client>site</client>
<folder>system</folder>
<tags><tag>stable</tag></tags>
<infourl title="System - MokoWaaS">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/plg_system_mokowaas-02.01.37.zip</downloadurl>
</downloads>
<sha256>9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd</sha256>
<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" />
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>