819 lines
30 KiB
PHP
819 lines
30 KiB
PHP
<?php
|
|
|
|
class LicenseService {
|
|
private static $remote_api_url = null;
|
|
private static $api_secret = null;
|
|
private static $app_slug = null;
|
|
private static $app_name = null;
|
|
private static $settings_cache = null;
|
|
private static $identity_cache = null;
|
|
private static $client_config_cache = null;
|
|
|
|
private static function loadClientConfig() {
|
|
if (self::$client_config_cache !== null) {
|
|
return self::$client_config_cache;
|
|
}
|
|
|
|
self::$client_config_cache = [];
|
|
$configPath = __DIR__ . '/../license_client_config.php';
|
|
if (!is_file($configPath)) {
|
|
return self::$client_config_cache;
|
|
}
|
|
|
|
try {
|
|
$config = include $configPath;
|
|
if (is_array($config)) {
|
|
self::$client_config_cache = $config;
|
|
}
|
|
} catch (Throwable $e) {
|
|
self::$client_config_cache = [];
|
|
}
|
|
|
|
return self::$client_config_cache;
|
|
}
|
|
|
|
private static function getClientConfigValue($key) {
|
|
$config = self::loadClientConfig();
|
|
return trim((string)($config[$key] ?? ''));
|
|
}
|
|
|
|
private static function normalizeApiBaseUrl($url) {
|
|
$url = trim((string)$url);
|
|
if ($url === '') {
|
|
return '';
|
|
}
|
|
|
|
$url = preg_replace('#/(?:index|manage|install)\.php(?:\?.*)?$#i', '', $url);
|
|
return rtrim((string)$url, '/');
|
|
}
|
|
|
|
private static function getCurrentRequestBaseParts() {
|
|
if (PHP_SAPI === 'cli') {
|
|
return ['', '', ''];
|
|
}
|
|
|
|
$host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
|
|
if ($host === '') {
|
|
return ['', '', ''];
|
|
}
|
|
|
|
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
|
|| (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string)$_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https');
|
|
$scheme = $isHttps ? 'https' : 'http';
|
|
|
|
$scriptName = str_replace('\\', '/', (string)($_SERVER['SCRIPT_NAME'] ?? '/index.php'));
|
|
$baseDir = rtrim(dirname($scriptName), '/');
|
|
if ($baseDir === '.' || $baseDir === '/') {
|
|
$baseDir = '';
|
|
}
|
|
|
|
return [$scheme, $host, $baseDir];
|
|
}
|
|
|
|
private static function buildLocalApiCandidates() {
|
|
[$scheme, $host, $baseDir] = self::getCurrentRequestBaseParts();
|
|
if ($host === '') {
|
|
return [];
|
|
}
|
|
|
|
$prefixes = [];
|
|
if ($baseDir !== '') {
|
|
$prefixes[] = $baseDir;
|
|
}
|
|
$prefixes[] = '';
|
|
|
|
$suffixes = ['/central_license_manager', '/key', '/keys'];
|
|
$candidates = [];
|
|
|
|
foreach ($prefixes as $prefix) {
|
|
foreach ($suffixes as $suffix) {
|
|
$candidate = self::normalizeApiBaseUrl($scheme . '://' . $host . $prefix . $suffix);
|
|
if ($candidate !== '') {
|
|
$candidates[$candidate] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_keys($candidates);
|
|
}
|
|
|
|
private static function isLocalHostUrl($url) {
|
|
$host = strtolower((string)(parse_url((string)$url, PHP_URL_HOST) ?: ''));
|
|
return in_array($host, ['localhost', '127.0.0.1', '::1'], true);
|
|
}
|
|
|
|
private static function urlLooksLikeHealthyLicenseApi($baseUrl) {
|
|
$baseUrl = self::normalizeApiBaseUrl($baseUrl);
|
|
if ($baseUrl === '') {
|
|
return false;
|
|
}
|
|
|
|
$url = rtrim($baseUrl, '/') . '/index.php?action=health';
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
$resp = curl_exec($ch);
|
|
$http_code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($resp === false || $http_code < 200 || $http_code >= 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(),
|
|
];
|
|
}
|
|
}
|