diff --git a/README.md b/README.md index 400a694..e3ec224 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Extension/MokoWaaS.php b/src/Extension/MokoWaaS.php index 0a42c69..88cc520 100644 --- a/src/Extension/MokoWaaS.php +++ b/src/Extension/MokoWaaS.php @@ -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. * diff --git a/updates.xml b/updates.xml index b978c47..9857be8 100644 --- a/updates.xml +++ b/updates.xml @@ -1,7 +1,7 @@ @@ -10,15 +10,87 @@ System - MokoWaaS update mokowaas plugin - 02.01.36-dev + 02.01.37-dev site system development https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.36-dev.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/development/plg_system_mokowaas-02.01.37-dev.zip - 9f7e0df50d0073ecb69ba3c13c22aec07b584e2f09127fc36bcf7eeeef534481 + 9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd + + Moko Consulting + https://mokoconsulting.tech + + + System - MokoWaaS + System - MokoWaaS update + mokowaas + plugin + 02.01.37-alpha + site + system + alpha + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/alpha + + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/alpha/plg_system_mokowaas-02.01.37-alpha.zip + + 9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd + + Moko Consulting + https://mokoconsulting.tech + + + System - MokoWaaS + System - MokoWaaS update + mokowaas + plugin + 02.01.37-beta + site + system + beta + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/beta + + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/beta/plg_system_mokowaas-02.01.37-beta.zip + + 9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd + + Moko Consulting + https://mokoconsulting.tech + + + System - MokoWaaS + System - MokoWaaS update + mokowaas + plugin + 02.01.37-rc + site + system + rc + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/rc + + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/rc/plg_system_mokowaas-02.01.37-rc.zip + + 9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd + + Moko Consulting + https://mokoconsulting.tech + + + System - MokoWaaS + System - MokoWaaS update + mokowaas + plugin + 02.01.37 + site + system + stable + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/tag/stable + + https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/releases/download/stable/plg_system_mokowaas-02.01.37.zip + + 9d71440040a957fb155c9951ea76ec9e684dca99565be8ff2c0acf5d1a3a5edd Moko Consulting https://mokoconsulting.tech