= 300) { return false; } $data = json_decode($resp, true); return is_array($data) && !empty($data['success']) && stripos((string)($data['manager'] ?? ''), 'license') !== false; } private static function detectLocalApiUrl() { foreach (self::buildLocalApiCandidates() as $candidate) { if (self::urlLooksLikeHealthyLicenseApi($candidate)) { return $candidate; } } [$scheme, $host, $baseDir] = self::getCurrentRequestBaseParts(); if ($host === '') { return ''; } return self::normalizeApiBaseUrl($scheme . '://' . $host . $baseDir . '/central_license_manager'); } private static function getApiUrl() { if (self::$remote_api_url !== null) { return self::$remote_api_url; } $configOverride = self::normalizeApiBaseUrl(self::getClientConfigValue('license_api_url')); if ($configOverride !== '') { self::$remote_api_url = $configOverride; return self::$remote_api_url; } $configured = self::normalizeApiBaseUrl(getenv('LICENSE_API_URL') ?: ''); if ($configured !== '') { if (self::isLocalHostUrl($configured) && !self::urlLooksLikeHealthyLicenseApi($configured)) { $detectedLocalUrl = self::detectLocalApiUrl(); self::$remote_api_url = $detectedLocalUrl !== '' ? $detectedLocalUrl : $configured; } else { self::$remote_api_url = $configured; } return self::$remote_api_url; } // Do not auto-switch to a bundled local license manager just because the module exists. // Customer activation keys for this project are issued centrally from omanapp.cloud, so // local/offline copies should keep using the central service unless an explicit project // config or environment variable points somewhere else. self::$remote_api_url = 'https://omanapp.cloud/central_license_manager'; return self::$remote_api_url; } private static function getApiSecret() { if (self::$api_secret === null) { $secret = trim((string)(self::getClientConfigValue('license_api_secret') ?: getenv('LICENSE_API_SECRET') ?: getenv('CLM_API_SECRET') ?: '1485-5215-2578')); self::$api_secret = $secret !== '' ? $secret : '1485-5215-2578'; } return self::$api_secret; } private static function buildActivationDebugContext() { $identity = self::getClientIdentity(); $apiUrl = self::getApiUrl(); $slug = (string)($identity['app_slug'] ?? ''); $slugSource = (string)($identity['app_slug_source'] ?? 'unknown'); $name = (string)($identity['app_name'] ?? ''); $parts = []; if ($apiUrl !== '') { $parts[] = 'Activation server: ' . $apiUrl . '.'; } if ($slug !== '') { $parts[] = 'App slug sent: ' . $slug . ' (' . $slugSource . ').'; } if ($name !== '') { $parts[] = 'App name sent: ' . $name . '.'; } return implode(' ', $parts); } public static function sanitizeAppSlug($value, $allowEmpty = false) { $value = strtolower(trim((string)$value)); $value = preg_replace('/[^a-z0-9]+/', '-', $value); $value = trim((string)$value, '-'); if ($value === '') { return $allowEmpty ? '' : 'bilingual-accounting'; } return substr($value, 0, 120); } private static function loadIdentitySettings() { if (self::$settings_cache !== null) { return self::$settings_cache; } self::$settings_cache = [ 'license_app_name' => null, 'license_app_slug' => null, ]; try { require_once __DIR__ . '/../db/config.php'; $db = db(); $tableExists = $db->query("SHOW TABLES LIKE 'settings'")->fetch(); if ($tableExists) { $stmt = $db->prepare("SELECT `key`, `value` FROM settings WHERE `key` IN ('license_app_name', 'license_app_slug')"); $stmt->execute(); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { $key = (string)($row['key'] ?? ''); $value = trim((string)($row['value'] ?? '')); if (array_key_exists($key, self::$settings_cache) && $value !== '') { self::$settings_cache[$key] = $value; } } } } catch (Throwable $e) { // Ignore local settings lookup errors and fall back to env/default values. } return self::$settings_cache; } private static function resolveClientIdentity() { if (self::$identity_cache !== null) { return self::$identity_cache; } $settings = self::loadIdentitySettings(); $storedName = trim((string)($settings['license_app_name'] ?? '')); $configName = self::getClientConfigValue('license_app_name'); $envName = trim((string)(getenv('LICENSE_APP_NAME') ?: '')); if ($storedName !== '') { $name = $storedName; $nameSource = 'settings'; } elseif ($configName !== '') { $name = $configName; $nameSource = 'project_config'; } elseif ($envName !== '') { $name = $envName; $nameSource = 'environment'; } else { $name = 'Bilingual Accounting'; $nameSource = 'default'; } $storedSlug = trim((string)($settings['license_app_slug'] ?? '')); $configSlug = self::getClientConfigValue('license_app_slug'); $envSlug = trim((string)(getenv('LICENSE_APP_SLUG') ?: '')); if ($storedSlug !== '') { $slug = self::sanitizeAppSlug($storedSlug); $slugSource = 'settings'; } elseif ($configSlug !== '') { $slug = self::sanitizeAppSlug($configSlug); $slugSource = 'project_config'; } elseif ($envSlug !== '') { $slug = self::sanitizeAppSlug($envSlug); $slugSource = 'environment'; } else { $slug = self::sanitizeAppSlug($name); $slugSource = $nameSource === 'default' ? 'default' : 'derived'; } self::$app_name = $name; self::$app_slug = $slug; self::$identity_cache = [ 'app_name' => $name, 'app_slug' => $slug, 'app_name_source' => $nameSource, 'app_slug_source' => $slugSource, ]; return self::$identity_cache; } public static function getClientIdentity() { return self::resolveClientIdentity(); } private static function slugify($value) { return self::sanitizeAppSlug($value); } private static function getAppName() { return (string)(self::resolveClientIdentity()['app_name'] ?? 'Bilingual Accounting'); } private static function getAppSlug() { return (string)(self::resolveClientIdentity()['app_slug'] ?? 'bilingual-accounting'); } private static function getProductName() { $name = trim((string)(getenv('LICENSE_PRODUCT_NAME') ?: self::getAppName())); return $name !== '' ? $name : self::getAppName(); } private static function getProductVersion() { return trim((string)(getenv('LICENSE_PRODUCT_VERSION') ?: '')); } private static function normalizeAllowedActivations(array $response) { return max(1, (int)($response['allowed_activations'] ?? $response['max_activations'] ?? 1)); } private static function normalizeMaxCounters(array $response) { return max(1, (int)($response['max_counters'] ?? self::normalizeAllowedActivations($response))); } /** * Returns the number of days remaining in the trial. */ public static function getTrialRemainingDays() { require_once __DIR__ . '/../db/config.php'; self::ensureTrialSchema(); $stmt = db()->prepare("SELECT trial_started_at FROM system_license LIMIT 1"); $stmt->execute(); $res = $stmt->fetch(); if (!$res || !$res['trial_started_at']) { return 0; } $started = strtotime((string)$res['trial_started_at']); $days_elapsed = floor((time() - $started) / (24 * 60 * 60)); return (int) max(0, 15 - $days_elapsed); } /** * Ensures the database schema for the trial period exists. */ private static function ensureTrialSchema() { require_once __DIR__ . '/../db/config.php'; try { $db = db(); $tableExists = $db->query("SHOW TABLES LIKE 'system_license'")->fetch(); if (!$tableExists) { $db->exec("CREATE TABLE system_license ( id INT AUTO_INCREMENT PRIMARY KEY, license_key VARCHAR(255) DEFAULT '', activation_token TEXT DEFAULT NULL, fingerprint VARCHAR(255) DEFAULT NULL, status ENUM('pending', 'active', 'expired', 'suspended', 'trial') DEFAULT 'pending', allowed_activations INT DEFAULT 1, max_counters INT DEFAULT 1, app_slug VARCHAR(120) DEFAULT NULL, app_name VARCHAR(190) DEFAULT NULL, license_source_url VARCHAR(255) DEFAULT NULL, activated_at DATETIME DEFAULT NULL, last_checked_at DATETIME DEFAULT NULL, trial_started_at DATETIME DEFAULT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); return; } $columnChecks = [ 'trial_started_at' => "ALTER TABLE system_license ADD COLUMN trial_started_at DATETIME DEFAULT NULL", 'allowed_activations' => "ALTER TABLE system_license ADD COLUMN allowed_activations INT DEFAULT 1", 'max_counters' => "ALTER TABLE system_license ADD COLUMN max_counters INT DEFAULT 1", 'app_slug' => "ALTER TABLE system_license ADD COLUMN app_slug VARCHAR(120) DEFAULT NULL", 'app_name' => "ALTER TABLE system_license ADD COLUMN app_name VARCHAR(190) DEFAULT NULL", 'license_source_url' => "ALTER TABLE system_license ADD COLUMN license_source_url VARCHAR(255) DEFAULT NULL", 'last_checked_at' => "ALTER TABLE system_license ADD COLUMN last_checked_at DATETIME DEFAULT NULL", ]; foreach ($columnChecks as $column => $sql) { $stmt = $db->query("SHOW COLUMNS FROM system_license LIKE '" . addslashes($column) . "'"); if (!$stmt->fetch()) { $db->exec($sql); } } $stmt = $db->query("SHOW COLUMNS FROM system_license LIKE 'status'"); $statusCol = $stmt->fetch(); if ($statusCol && strpos((string)$statusCol['Type'], "'trial'") === false) { $db->exec("ALTER TABLE system_license MODIFY COLUMN status ENUM('pending', 'active', 'expired', 'suspended', 'trial') DEFAULT 'pending'"); } } catch (Exception $e) { // Keep licensing checks fail-safe; UI will show remote/local activation errors separately. } } /** * Public wrapper to initialize the trial period. */ public static function initTrial() { try { self::startTrial(); return ['success' => true]; } catch (Exception $e) { return ['success' => false, 'error' => $e->getMessage()]; } } /** * Initializes the trial period. */ private static function startTrial() { require_once __DIR__ . '/../db/config.php'; self::ensureTrialSchema(); $stmt = db()->prepare("SELECT COUNT(*) FROM system_license"); $stmt->execute(); if ((int)$stmt->fetchColumn() === 0) { db()->exec("INSERT INTO system_license (license_key, status, trial_started_at) VALUES ('TRIAL', 'trial', NOW())"); } else { db()->exec("UPDATE system_license SET trial_started_at = NOW() WHERE trial_started_at IS NULL"); } } /** * Generates a unique fingerprint for this server environment. */ public static function getFingerprint() { $data = [ php_uname('n'), php_uname('m'), PHP_OS, ]; return hash('sha256', implode('|', $data)); } /** * Returns the number of allowed activations/counters. */ public static function getAllowedActivations() { require_once __DIR__ . '/../db/config.php'; self::ensureTrialSchema(); $stmt = db()->prepare("SELECT allowed_activations FROM system_license WHERE status = 'active' ORDER BY id DESC LIMIT 1"); $stmt->execute(); $res = $stmt->fetch(); if ($res) { return (int)$res['allowed_activations']; } return 1; } /** * Checks if the system is currently activated or within trial period. */ public static function canAccess() { if (self::isActivated()) { return true; } $daysLeft = self::getTrialRemainingDays(); return $daysLeft > 0; } /** * Checks if the system is currently activated with a valid license key. */ public static function isActivated() { require_once __DIR__ . '/../db/config.php'; self::ensureTrialSchema(); $fingerprint = self::getFingerprint(); $stmt = db()->prepare("SELECT * FROM system_license WHERE status = 'active' AND fingerprint = ? ORDER BY id DESC LIMIT 1"); $stmt->execute([$fingerprint]); $license = $stmt->fetch(); if (!$license) { return false; } if (($license['fingerprint'] ?? '') !== $fingerprint) { return false; } $lastCheckRaw = $license['last_checked_at'] ?? ($license['activated_at'] ?? null); $lastCheck = $lastCheckRaw ? strtotime((string)$lastCheckRaw) : false; if ($lastCheck === false || time() - $lastCheck > (7 * 24 * 60 * 60)) { $res = self::callRemoteApi('/verify', [ 'license_key' => (string)$license['license_key'], 'fingerprint' => (string)$license['fingerprint'], 'token' => (string)($license['activation_token'] ?? ''), 'app_slug' => (string)($license['app_slug'] ?: self::getAppSlug()), ]); if (empty($res['success'])) { db()->prepare("UPDATE system_license SET status = 'suspended', last_checked_at = NOW() WHERE id = ?")->execute([(int)$license['id']]); return false; } $allowed = self::normalizeAllowedActivations($res); $maxCounters = self::normalizeMaxCounters($res); $appSlug = (string)($res['app_slug'] ?? ($license['app_slug'] ?: self::getAppSlug())); $appName = (string)($res['app_name'] ?? ($license['app_name'] ?: self::getAppName())); db()->prepare("UPDATE system_license SET status = 'active', allowed_activations = ?, max_counters = ?, app_slug = ?, app_name = ?, license_source_url = ?, last_checked_at = NOW() WHERE id = ?") ->execute([$allowed, $maxCounters, $appSlug, $appName, self::getApiUrl(), (int)$license['id']]); } return true; } /** * Attempts to activate the product online. */ public static function activate($license_key) { $license_key = strtoupper(trim((string)$license_key)); if ($license_key === '') { return ['success' => false, 'error' => 'License key is required.']; } $fingerprint = self::getFingerprint(); $response = self::callRemoteApi('/activate', [ 'license_key' => $license_key, 'fingerprint' => $fingerprint, 'domain' => $_SERVER['HTTP_HOST'] ?? 'unknown', 'product' => self::getProductName(), 'product_version' => self::getProductVersion(), 'app_slug' => self::getAppSlug(), 'app_name' => self::getAppName(), ]); if (empty($response['success'])) { $error = trim((string)($response['error'] ?? 'Remote activation failed.')); $needsContext = $error === '' || stripos($error, 'Remote request failed') !== false || stripos($error, 'Invalid response from remote server') !== false || stripos($error, 'License does not belong to this app') !== false || stripos($error, 'Invalid license key') !== false; if ($needsContext) { $error = rtrim($error !== '' ? $error : 'Remote activation failed.', '.'); $error .= '. ' . self::buildActivationDebugContext(); } return ['success' => false, 'error' => $error !== '' ? $error : 'Remote activation failed.']; } require_once __DIR__ . '/../db/config.php'; self::ensureTrialSchema(); $allowed = self::normalizeAllowedActivations($response); $maxCounters = self::normalizeMaxCounters($response); $appSlug = (string)($response['app_slug'] ?? self::getAppSlug()); $appName = (string)($response['app_name'] ?? self::getAppName()); $token = (string)($response['activation_token'] ?? bin2hex(random_bytes(32))); $stmt = db()->prepare("DELETE FROM system_license WHERE fingerprint = ? OR license_key = ? OR status = 'trial'"); $stmt->execute([$fingerprint, $license_key]); $stmt = db()->prepare("INSERT INTO system_license (license_key, fingerprint, status, activated_at, last_checked_at, activation_token, allowed_activations, max_counters, app_slug, app_name, license_source_url, trial_started_at) VALUES (?, ?, 'active', NOW(), NOW(), ?, ?, ?, ?, ?, ?, NULL)"); $stmt->execute([ $license_key, $fingerprint, $token, $allowed, $maxCounters, $appSlug, $appName, self::getApiUrl(), ]); return [ 'success' => true, 'app_slug' => $appSlug, 'app_name' => $appName, 'allowed_activations' => $allowed, 'max_counters' => $maxCounters, ]; } /** * Fetches all licenses from the remote server. */ public static function listLicenses() { return self::callRemoteApi('/list', [ 'secret' => self::getApiSecret(), 'app_slug' => self::getAppSlug(), ]); } /** * Updates an existing license. */ public static function updateLicense($id, $data) { $params = array_merge([ 'id' => $id, 'secret' => self::getApiSecret(), 'app_slug' => self::getAppSlug(), ], is_array($data) ? $data : []); return self::callRemoteApi('/update', $params); } /** * Issues a new license. */ public static function issueLicense($max_activations, $prefix = 'FLAT', $owner = null, $address = null) { $payload = [ 'secret' => self::getApiSecret(), 'max_activations' => max(1, (int)$max_activations), 'max_counters' => max(1, (int)$max_activations), 'prefix' => $prefix, 'owner' => $owner, 'address' => $address, 'app_slug' => self::getAppSlug(), 'app_name' => self::getAppName(), ]; return self::callRemoteApi('/issue', $payload); } /** * Remote API Caller. */ private static function callRemoteApi($endpoint, $params) { $action = ltrim((string)$endpoint, '/'); $baseUrl = self::getApiUrl(); if ($baseUrl === '' || strpos($baseUrl, 'your-domain.com') !== false) { return self::simulateApi($endpoint, $params); } if (strpos($baseUrl, 'index.php') !== false) { $separator = strpos($baseUrl, '?') === false ? '?' : '&'; $url = $baseUrl . $separator . 'action=' . urlencode($action); } else { $url = rtrim($baseUrl, '/') . '/index.php?action=' . urlencode($action); } $payload = json_encode($params); if ($payload === false) { return ['success' => false, 'error' => 'Failed to encode remote activation payload.']; } $resp = false; $http_code = 0; $content_type = ''; $curl_error = ''; if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $resp = curl_exec($ch); $http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $content_type = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE); $curl_error = curl_error($ch); curl_close($ch); } else { $headers = [ 'Content-Type: application/json', 'Content-Length: ' . strlen($payload), ]; $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => implode("\r\n", $headers) . "\r\n", 'content' => $payload, 'timeout' => 10, 'ignore_errors' => true, ], ]); $resp = @file_get_contents($url, false, $context); $streamHeaders = isset($http_response_header) && is_array($http_response_header) ? $http_response_header : []; foreach ($streamHeaders as $headerLine) { if (preg_match('#^HTTP/\S+\s+(\d{3})#i', (string)$headerLine, $matches)) { $http_code = (int)$matches[1]; } if (stripos((string)$headerLine, 'Content-Type:') === 0) { $content_type = trim((string)substr((string)$headerLine, strlen('Content-Type:'))); } } if ($resp === false) { $lastError = error_get_last(); $curl_error = trim((string)($lastError['message'] ?? 'HTTP stream transport failed.')); } } if ($resp === false) { $networkError = $curl_error !== '' ? $curl_error : 'Unknown network error.'; return ['success' => false, 'error' => 'Remote request failed: ' . $networkError . ' URL: ' . $url]; } $data = json_decode($resp, true); if (!is_array($data)) { $preview = trim((string)preg_replace('/\s+/', ' ', strip_tags((string)$resp))); if ($preview !== '' && strlen($preview) > 180) { $preview = substr($preview, 0, 177) . '...'; } $error = 'Invalid response from remote server.'; if ($http_code > 0) { $error .= ' HTTP ' . $http_code . '.'; } if ($content_type !== '') { $error .= ' Content-Type: ' . $content_type . '.'; } $error .= ' URL: ' . $url . '.'; if ($preview !== '') { $error .= ' Preview: ' . $preview; } return ['success' => false, 'error' => $error]; } if ($http_code < 200 || $http_code >= 300) { return [ 'success' => false, 'error' => (string)($data['error'] ?? ('Remote server returned error code ' . $http_code . '.')), 'http_code' => $http_code, ]; } return $data; } /** * Local simulation for development purposes. */ private static function simulateApi($endpoint, $params) { $clean_key = strtoupper(trim((string)($params['license_key'] ?? ''))); $pattern = '/^FLAT-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}$/'; if ($endpoint === '/list') { return ['success' => true, 'data' => []]; } if ($endpoint === '/apps') { return ['success' => true, 'data' => [['slug' => self::getAppSlug(), 'name' => self::getAppName()]]]; } if ($endpoint === '/update') { return ['success' => true, 'data' => $params]; } if ($endpoint === '/issue') { return [ 'success' => true, 'license_key' => 'FLAT-' . strtoupper(bin2hex(random_bytes(2))) . '-' . strtoupper(bin2hex(random_bytes(2))) . '-' . strtoupper(bin2hex(random_bytes(2))), 'max_activations' => max(1, (int)($params['max_activations'] ?? 1)), 'allowed_activations' => max(1, (int)($params['max_activations'] ?? 1)), 'max_counters' => max(1, (int)($params['max_counters'] ?? $params['max_activations'] ?? 1)), 'app_slug' => self::getAppSlug(), 'app_name' => self::getAppName(), ]; } if (!preg_match($pattern, $clean_key)) { return [ 'success' => false, 'error' => 'Invalid License Format. Expected FLAT-XXXX-XXXX-XXXX (Simulation Mode).', ]; } if ($clean_key === 'FLAT-0000-0000-0000') { return [ 'success' => false, 'error' => 'Activation Failed: This license key is already active on another machine.', ]; } if ($endpoint === '/verify') { return [ 'success' => true, 'max_activations' => 2, 'allowed_activations' => 2, 'max_counters' => 2, 'app_slug' => self::getAppSlug(), 'app_name' => self::getAppName(), ]; } return [ 'success' => true, 'allowed_activations' => 2, 'max_activations' => 2, 'max_counters' => 2, 'activation_token' => hash('sha256', $clean_key . (string)($params['fingerprint'] ?? '') . 'DEBUG_SALT'), 'app_slug' => self::getAppSlug(), 'app_name' => self::getAppName(), ]; } }