feat: MokoGitea LicenseValidator — core DRM enforcement, cache table, task scheduler
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 50s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 41s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 7s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 13s
Universal: Auto Version Bump / Version Bump (push) Successful in 16s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 25s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 42s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 46s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 50s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 41s
This commit is contained in:
@@ -215,3 +215,15 @@ INSERT IGNORE INTO `#__mokosuiteclient_retention_policies` (`id`, `content_type`
|
||||
(4, 'inactive_users', 730, 'anonymize', 0, 'Anonymize users inactive for 2 years (disabled by default)'),
|
||||
(5, 'closed_tickets', 365, 'anonymize', 0, 'Anonymize closed tickets older than 1 year (disabled by default)');
|
||||
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- License Cache — stores MokoGitea validation results
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `#__mokosuite_license_cache` (
|
||||
`dlid_hash` CHAR(64) NOT NULL COMMENT 'SHA-256 of DLID (never store raw DLID)',
|
||||
`response_data` TEXT NOT NULL COMMENT 'JSON validation response from MokoGitea',
|
||||
`checked_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`dlid_hash`),
|
||||
KEY `idx_checked` (`checked_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
/**
|
||||
* @package MokoSuiteClient
|
||||
* @subpackage plg_system_mokosuiteclient
|
||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||
* @license GNU General Public License version 3 or later; see LICENSE
|
||||
*/
|
||||
|
||||
namespace Moko\Plugin\System\MokoSuiteClient\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* MokoGitea License Validator — core DRM enforcement for the MokoSuite platform.
|
||||
*
|
||||
* Validates the site's DLID against MokoGitea, caches the result,
|
||||
* and provides entitlement checking for all suite modules.
|
||||
*
|
||||
* Default Gitea server: git.mokoconsulting.tech
|
||||
*
|
||||
* @since 02.45.00
|
||||
*/
|
||||
final class LicenseValidator
|
||||
{
|
||||
/** @var string Default MokoGitea server address */
|
||||
private const DEFAULT_GITEA_URL = 'https://git.mokoconsulting.tech';
|
||||
|
||||
/** @var int Cache TTL in seconds (24 hours) */
|
||||
private const CACHE_TTL = 86400;
|
||||
|
||||
/** @var int Grace period in days after expiry before deactivation */
|
||||
private const DEFAULT_GRACE_DAYS = 7;
|
||||
|
||||
/** @var object|null Cached license data for current request */
|
||||
private static ?object $cachedLicense = null;
|
||||
|
||||
/**
|
||||
* Validate the site's DLID against MokoGitea.
|
||||
* Returns cached result if still valid; calls API if expired.
|
||||
*/
|
||||
public static function validate(bool $forceRefresh = false): object
|
||||
{
|
||||
if (self::$cachedLicense && !$forceRefresh) {
|
||||
return self::$cachedLicense;
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$dlid = self::getDlid();
|
||||
|
||||
if (!$dlid) {
|
||||
return self::$cachedLicense = (object) [
|
||||
'valid' => false,
|
||||
'status' => 'no_dlid',
|
||||
'message' => 'No license key configured',
|
||||
'entitlements'=> [],
|
||||
];
|
||||
}
|
||||
|
||||
// Check DB cache first
|
||||
if (!$forceRefresh) {
|
||||
$cached = self::getCachedResult($db, $dlid);
|
||||
if ($cached) {
|
||||
return self::$cachedLicense = $cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Call MokoGitea API
|
||||
$result = self::callGiteaApi($dlid);
|
||||
|
||||
// Cache the result
|
||||
self::cacheResult($db, $dlid, $result);
|
||||
|
||||
return self::$cachedLicense = $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current license includes entitlement for a specific extension.
|
||||
*
|
||||
* @param string $extension Extension element name (e.g., 'com_mokosuite_crm', 'com_mokosuiterestaurant')
|
||||
* @return bool
|
||||
*/
|
||||
public static function isEntitled(string $extension): bool
|
||||
{
|
||||
$license = self::validate();
|
||||
|
||||
if (!$license->valid) return false;
|
||||
|
||||
// Map extension names to repo identifiers
|
||||
$repoMap = [
|
||||
'com_mokosuite' => 'MokoSuite',
|
||||
'com_mokosuite_crm' => 'MokoSuiteCRM',
|
||||
'com_mokosuite_erp' => 'MokoSuiteERP',
|
||||
'com_mokosuitechild' => 'MokoSuiteChild',
|
||||
'com_mokosuitecreate' => 'MokoSuiteCreate',
|
||||
'com_mokosuitenpo' => 'MokoSuiteNPO',
|
||||
'com_mokosuitefield' => 'MokoSuiteField',
|
||||
'com_mokosuitepos' => 'MokoSuitePOS',
|
||||
'com_mokoshop' => 'MokoSuiteShop',
|
||||
'com_mokosuitehrm' => 'MokoSuiteHRM',
|
||||
'com_mokosuitemrp' => 'MokoSuiteMRP',
|
||||
'com_mokosuiterestaurant' => 'MokoSuiteRestaurant',
|
||||
];
|
||||
|
||||
$repo = $repoMap[$extension] ?? $extension;
|
||||
$entitlements = $license->entitlements ?? [];
|
||||
|
||||
// Base is always entitled if license is valid
|
||||
if ($repo === 'MokoSuite') return true;
|
||||
|
||||
return in_array($repo, $entitlements, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full license status for admin display.
|
||||
*/
|
||||
public static function getStatus(): object
|
||||
{
|
||||
$license = self::validate();
|
||||
|
||||
return (object) [
|
||||
'valid' => $license->valid ?? false,
|
||||
'status' => $license->status ?? 'unknown',
|
||||
'tier' => $license->tier ?? 'none',
|
||||
'entitlements' => $license->entitlements ?? [],
|
||||
'expires_at' => $license->expires_at ?? null,
|
||||
'seats' => $license->seats ?? 0,
|
||||
'seats_used' => $license->seats_used ?? 0,
|
||||
'days_remaining'=> self::getDaysRemaining($license),
|
||||
'in_grace' => self::isInGracePeriod($license),
|
||||
'gitea_url' => self::getGiteaUrl(),
|
||||
'dlid_configured' => (bool) self::getDlid(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available seat count.
|
||||
*/
|
||||
public static function getAvailableSeats(): int
|
||||
{
|
||||
$license = self::validate();
|
||||
$total = (int) ($license->seats ?? 0);
|
||||
$used = (int) ($license->seats_used ?? 0);
|
||||
|
||||
if ($total === 0) return PHP_INT_MAX; // Unlimited seats
|
||||
|
||||
return max(0, $total - $used);
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a heartbeat to MokoGitea (active installation check).
|
||||
* Called by task scheduler daily.
|
||||
*/
|
||||
public static function heartbeat(): object
|
||||
{
|
||||
$dlid = self::getDlid();
|
||||
if (!$dlid) return (object) ['success' => false, 'error' => 'No DLID'];
|
||||
|
||||
$giteaUrl = self::getGiteaUrl();
|
||||
$siteUrl = \Joomla\CMS\Uri\Uri::root();
|
||||
$joomlaVersion = (new \Joomla\CMS\Version())->getShortVersion();
|
||||
|
||||
// Count installed suite modules
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('element')
|
||||
->from('#__extensions')
|
||||
->where($db->quoteName('element') . ' LIKE ' . $db->quote('com_mokosuite%'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component'))
|
||||
->where($db->quoteName('enabled') . ' = 1'));
|
||||
$installedModules = $db->loadColumn() ?: [];
|
||||
|
||||
$response = self::httpPost($giteaUrl . '/api/v1/licenses/heartbeat', [
|
||||
'dlid' => $dlid,
|
||||
'site_url' => $siteUrl,
|
||||
'joomla_version' => $joomlaVersion,
|
||||
'installed_modules' => $installedModules,
|
||||
'php_version' => PHP_VERSION,
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
// ── Private methods ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the configured DLID from component params.
|
||||
*/
|
||||
private static function getDlid(): string
|
||||
{
|
||||
try {
|
||||
$params = Factory::getApplication()->getParams('com_mokosuite');
|
||||
return trim($params->get('dlid', ''));
|
||||
} catch (\Throwable $e) {
|
||||
// Component not installed or params not available
|
||||
try {
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('params')
|
||||
->from('#__extensions')
|
||||
->where($db->quoteName('element') . ' = ' . $db->quote('com_mokosuite'))
|
||||
->where($db->quoteName('type') . ' = ' . $db->quote('component')));
|
||||
$paramsJson = $db->loadResult();
|
||||
$params = json_decode($paramsJson ?: '{}', false);
|
||||
return trim($params->dlid ?? '');
|
||||
} catch (\Throwable $e2) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MokoGitea server URL from config.
|
||||
*/
|
||||
private static function getGiteaUrl(): string
|
||||
{
|
||||
try {
|
||||
$params = Factory::getApplication()->getParams('com_mokosuite');
|
||||
return rtrim($params->get('gitea_url', self::DEFAULT_GITEA_URL), '/');
|
||||
} catch (\Throwable $e) {
|
||||
return self::DEFAULT_GITEA_URL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call MokoGitea license validation API.
|
||||
*/
|
||||
private static function callGiteaApi(string $dlid): object
|
||||
{
|
||||
$giteaUrl = self::getGiteaUrl();
|
||||
|
||||
$response = self::httpGet($giteaUrl . '/api/v1/licenses/validate?dlid=' . urlencode($dlid));
|
||||
|
||||
if (isset($response->valid)) {
|
||||
return (object) [
|
||||
'valid' => (bool) $response->valid,
|
||||
'status' => $response->status ?? 'unknown',
|
||||
'tier' => $response->tier ?? '',
|
||||
'entitlements' => $response->entitlements ?? $response->repo_scope ?? [],
|
||||
'expires_at' => $response->expires_at ?? null,
|
||||
'seats' => (int) ($response->seats ?? 0),
|
||||
'seats_used' => (int) ($response->seats_used ?? 0),
|
||||
'message' => $response->message ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// API error — use cached result if available, otherwise fail gracefully
|
||||
return (object) [
|
||||
'valid' => false,
|
||||
'status' => 'api_error',
|
||||
'message' => $response->error ?? 'Could not reach license server',
|
||||
'entitlements' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached validation result from database.
|
||||
*/
|
||||
private static function getCachedResult(DatabaseInterface $db, string $dlid): ?object
|
||||
{
|
||||
$dlidHash = hash('sha256', $dlid);
|
||||
|
||||
try {
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('response_data, checked_at')
|
||||
->from('#__mokosuite_license_cache')
|
||||
->where($db->quoteName('dlid_hash') . ' = ' . $db->quote($dlidHash))
|
||||
->where('checked_at > DATE_SUB(NOW(), INTERVAL ' . (int) self::CACHE_TTL . ' SECOND)'));
|
||||
$cached = $db->loadObject();
|
||||
|
||||
if ($cached && $cached->response_data) {
|
||||
$data = json_decode($cached->response_data, false);
|
||||
if ($data) return $data;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Table may not exist yet — that's fine
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache validation result in database.
|
||||
*/
|
||||
private static function cacheResult(DatabaseInterface $db, string $dlid, object $result): void
|
||||
{
|
||||
$dlidHash = hash('sha256', $dlid);
|
||||
|
||||
try {
|
||||
// Upsert
|
||||
$db->setQuery('REPLACE INTO #__mokosuite_license_cache (dlid_hash, response_data, checked_at) VALUES ('
|
||||
. $db->quote($dlidHash) . ', '
|
||||
. $db->quote(json_encode($result)) . ', '
|
||||
. $db->quote(Factory::getDate()->toSql()) . ')');
|
||||
$db->execute();
|
||||
} catch (\Throwable $e) {
|
||||
// Cache table may not exist — non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate days remaining on license.
|
||||
*/
|
||||
private static function getDaysRemaining(object $license): ?int
|
||||
{
|
||||
if (empty($license->expires_at)) return null;
|
||||
|
||||
$now = new \DateTime('today');
|
||||
$expiry = new \DateTime($license->expires_at);
|
||||
$diff = (int) $now->diff($expiry)->format('%r%a');
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license is in grace period (expired but within grace window).
|
||||
*/
|
||||
private static function isInGracePeriod(object $license): bool
|
||||
{
|
||||
$days = self::getDaysRemaining($license);
|
||||
if ($days === null || $days >= 0) return false;
|
||||
|
||||
$graceDays = self::DEFAULT_GRACE_DAYS;
|
||||
try {
|
||||
$graceDays = (int) Factory::getApplication()->getParams('com_mokosuite')->get('license_grace_days', self::DEFAULT_GRACE_DAYS);
|
||||
} catch (\Throwable $e) {}
|
||||
|
||||
return abs($days) <= $graceDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP GET helper.
|
||||
*/
|
||||
private static function httpGet(string $url): object
|
||||
{
|
||||
$response = file_get_contents($url, false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => 'Accept: application/json',
|
||||
'ignore_errors' => true,
|
||||
'timeout' => 10,
|
||||
],
|
||||
]));
|
||||
|
||||
return json_decode($response ?: '{}', false) ?: (object) ['error' => 'No response'];
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP POST helper.
|
||||
*/
|
||||
private static function httpPost(string $url, array $data): object
|
||||
{
|
||||
$response = file_get_contents($url, false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\nAccept: application/json",
|
||||
'ignore_errors' => true,
|
||||
'timeout' => 10,
|
||||
'content' => json_encode($data),
|
||||
],
|
||||
]));
|
||||
|
||||
return json_decode($response ?: '{}', false) ?: (object) ['error' => 'No response'];
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,14 @@ class TicketAutomation extends CMSPlugin implements SubscriberInterface
|
||||
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_TICKETS_AUTOCLOSE',
|
||||
'method' => 'runAutoClose',
|
||||
],
|
||||
'mokosuiteclient.license.validate' => [
|
||||
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_VALIDATE',
|
||||
'method' => 'runLicenseValidation',
|
||||
],
|
||||
'mokosuiteclient.license.heartbeat' => [
|
||||
'langConstPrefix' => 'PLG_TASK_MOKOSUITECLIENT_LICENSE_HEARTBEAT',
|
||||
'method' => 'runLicenseHeartbeat',
|
||||
],
|
||||
];
|
||||
|
||||
protected $autoloadLanguage = true;
|
||||
@@ -310,4 +318,50 @@ class TicketAutomation extends CMSPlugin implements SubscriberInterface
|
||||
|
||||
return trim($textBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Daily license revalidation — refresh cached license status from MokoGitea.
|
||||
* Recommended schedule: daily at 3:00 AM.
|
||||
*/
|
||||
private function runLicenseValidation(ExecuteTaskEvent $event): int
|
||||
{
|
||||
try {
|
||||
$result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::validate(true);
|
||||
|
||||
$status = $result->valid ? 'valid' : ($result->status ?? 'invalid');
|
||||
$tier = $result->tier ?? 'none';
|
||||
$entitlements = count($result->entitlements ?? []);
|
||||
|
||||
$this->logTask(sprintf(
|
||||
'License validation: status=%s tier=%s entitlements=%d',
|
||||
$status, $tier, $entitlements
|
||||
));
|
||||
|
||||
if (!$result->valid) {
|
||||
Log::add('License validation failed: ' . ($result->message ?? 'unknown'), Log::WARNING, 'mokosuite.license');
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logTask('License validation error: ' . $e->getMessage());
|
||||
return Status::KNOCKOUT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Daily heartbeat — report active installation to MokoGitea.
|
||||
* Recommended schedule: daily at 4:00 AM.
|
||||
*/
|
||||
private function runLicenseHeartbeat(ExecuteTaskEvent $event): int
|
||||
{
|
||||
try {
|
||||
$result = \Moko\Plugin\System\MokoSuiteClient\Helper\LicenseValidator::heartbeat();
|
||||
|
||||
$this->logTask('License heartbeat: ' . ($result->success ?? false ? 'sent' : ($result->error ?? 'failed')));
|
||||
return Status::OK;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logTask('License heartbeat error: ' . $e->getMessage());
|
||||
return Status::KNOCKOUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user