[ 'max_entries' => 100, 'max_users' => 2, 'features' => ['basic_crud', 'csv_export'], 'name' => 'Trial', 'duration_days' => 14 ], LICENSE_BASIC => [ 'max_entries' => 500, 'max_users' => 5, 'features' => ['basic_crud', 'csv_export', 'webhooks', 'audit_log'], 'name' => 'Basic' ], LICENSE_PROFESSIONAL => [ 'max_entries' => 2500, 'max_users' => 15, 'features' => ['basic_crud', 'csv_export', 'webhooks', 'audit_log', 'ip_enrichment', 'whitelabel', 'ptr_records'], 'name' => 'Professional' ], LICENSE_ENTERPRISE => [ 'max_entries' => -1, // Unlimited 'max_users' => -1, // Unlimited 'features' => ['basic_crud', 'csv_export', 'webhooks', 'audit_log', 'ip_enrichment', 'whitelabel', 'ptr_records', 'api_access', 'priority_support'], 'name' => 'Enterprise' ] ]; /** * Generate a new license key * Format: IPMAN-XXXX-XXXX-XXXX-XXXX */ function generateLicenseKey() { $segments = []; for ($i = 0; $i < 4; $i++) { $segments[] = strtoupper(bin2hex(random_bytes(2))); } return 'IPMAN-' . implode('-', $segments); } /** * Create a signed license key with embedded data */ function createSignedLicense($licenseeEmail, $licenseType, $expiresAt = null) { $data = [ 'e' => $licenseeEmail, 't' => $licenseType, 'i' => time(), 'x' => $expiresAt ? strtotime($expiresAt) : null ]; $payload = base64_encode(json_encode($data)); $signature = hash_hmac('sha256', $payload, getLicenseSecret()); return 'IPMAN-' . substr($signature, 0, 8) . '-' . chunk_split(strtoupper(bin2hex(random_bytes(6))), 4, '-'); } /** * Get license secret (from environment or generate) */ function getLicenseSecret() { $secret = getenv('LICENSE_SECRET'); if (empty($secret)) { $secret = 'purple-computing-isp-ip-manager-2025'; } return $secret; } /** * Validate a license key format */ function isValidLicenseFormat($key) { return preg_match('/^IPMAN-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$/i', $key); } /** * Get current license info from database */ function getLicenseInfo($db = null) { if (!$db && function_exists('getDB')) { $db = getDB(); } if (!$db) { return null; } try { $stmt = $db->query("SELECT * FROM license_info WHERE is_active = 1 ORDER BY id DESC LIMIT 1"); return $stmt->fetch(PDO::FETCH_ASSOC); } catch (Exception $e) { return null; } } /** * Check if license is valid and not expired */ function isLicenseValid($db = null) { $license = getLicenseInfo($db); if (!$license) { return false; } // Check if expired if ($license['expires_at'] && strtotime($license['expires_at']) < time()) { return false; } return $license['is_active'] == 1; } /** * Get license status with details */ function getLicenseStatus($db = null) { global $LICENSE_LIMITS; $license = getLicenseInfo($db); if (!$license) { return [ 'valid' => false, 'status' => 'unlicensed', 'message' => 'No license key configured', 'type' => null, 'limits' => $LICENSE_LIMITS[LICENSE_TRIAL] ]; } $isExpired = $license['expires_at'] && strtotime($license['expires_at']) < time(); if ($isExpired) { return [ 'valid' => false, 'status' => 'expired', 'message' => 'License expired on ' . date('Y-m-d', strtotime($license['expires_at'])), 'type' => $license['license_type'], 'limits' => $LICENSE_LIMITS[LICENSE_TRIAL], 'license' => $license ]; } if (!$license['is_active']) { return [ 'valid' => false, 'status' => 'inactive', 'message' => 'License has been deactivated', 'type' => $license['license_type'], 'limits' => $LICENSE_LIMITS[LICENSE_TRIAL], 'license' => $license ]; } $type = $license['license_type'] ?: LICENSE_TRIAL; $limits = $LICENSE_LIMITS[$type] ?? $LICENSE_LIMITS[LICENSE_TRIAL]; // Override with custom limits if set if ($license['max_entries']) { $limits['max_entries'] = $license['max_entries']; } if ($license['max_users']) { $limits['max_users'] = $license['max_users']; } if ($license['features']) { $customFeatures = json_decode($license['features'], true); if (is_array($customFeatures)) { $limits['features'] = $customFeatures; } } $daysRemaining = null; if ($license['expires_at']) { $daysRemaining = max(0, floor((strtotime($license['expires_at']) - time()) / 86400)); } return [ 'valid' => true, 'status' => 'active', 'message' => $limits['name'] . ' License', 'type' => $type, 'limits' => $limits, 'license' => $license, 'days_remaining' => $daysRemaining, 'expires_at' => $license['expires_at'] ]; } /** * Check if a specific feature is available */ function hasFeature($feature, $db = null) { $status = getLicenseStatus($db); // Always allow basic features for unlicensed/trial $basicFeatures = ['basic_crud', 'csv_export']; if (in_array($feature, $basicFeatures)) { return true; } if (!$status['valid']) { return false; } return in_array($feature, $status['limits']['features']); } /** * Check if entry limit has been reached */ function canAddEntry($db = null) { if (!$db && function_exists('getDB')) { $db = getDB(); } $status = getLicenseStatus($db); $maxEntries = $status['limits']['max_entries']; // -1 means unlimited if ($maxEntries == -1) { return ['allowed' => true, 'current' => 0, 'max' => -1]; } try { $stmt = $db->query("SELECT COUNT(*) FROM geofeed_entries"); $current = (int)$stmt->fetchColumn(); return [ 'allowed' => $current < $maxEntries, 'current' => $current, 'max' => $maxEntries ]; } catch (Exception $e) { return ['allowed' => true, 'current' => 0, 'max' => $maxEntries]; } } /** * Check if user limit has been reached */ function canAddUser($db = null) { if (!$db && function_exists('getDB')) { $db = getDB(); } $status = getLicenseStatus($db); $maxUsers = $status['limits']['max_users']; // -1 means unlimited if ($maxUsers == -1) { return ['allowed' => true, 'current' => 0, 'max' => -1]; } try { $stmt = $db->query("SELECT COUNT(*) FROM admin_users WHERE active = 1"); $current = (int)$stmt->fetchColumn(); return [ 'allowed' => $current < $maxUsers, 'current' => $current, 'max' => $maxUsers ]; } catch (Exception $e) { return ['allowed' => true, 'current' => 0, 'max' => $maxUsers]; } } /** * Activate a license key */ function activateLicense($db, $licenseKey, $licenseeName, $licenseeEmail) { if (!isValidLicenseFormat($licenseKey)) { return ['success' => false, 'error' => 'Invalid license key format']; } // For now, accept any valid format key as a trial // In production, you would validate against a license server $licenseType = LICENSE_TRIAL; $expiresAt = date('Y-m-d H:i:s', strtotime('+14 days')); // Check if key starts with specific prefixes for different tiers if (preg_match('/^IPMAN-[0-9A-F]{4}/', $licenseKey)) { $prefix = substr($licenseKey, 6, 1); switch ($prefix) { case 'B': $licenseType = LICENSE_BASIC; $expiresAt = date('Y-m-d H:i:s', strtotime('+1 year')); break; case 'P': $licenseType = LICENSE_PROFESSIONAL; $expiresAt = date('Y-m-d H:i:s', strtotime('+1 year')); break; case 'E': $licenseType = LICENSE_ENTERPRISE; $expiresAt = null; // No expiry break; } } global $LICENSE_LIMITS; $limits = $LICENSE_LIMITS[$licenseType]; try { // Deactivate any existing licenses $db->exec("UPDATE license_info SET is_active = 0"); // Insert new license $stmt = $db->prepare(" INSERT INTO license_info (license_key, licensee_name, licensee_email, license_type, max_entries, max_users, features, expires_at, is_active) VALUES (:key, :name, :email, :type, :max_entries, :max_users, :features, :expires, 1) "); $stmt->execute([ ':key' => $licenseKey, ':name' => $licenseeName, ':email' => $licenseeEmail, ':type' => $licenseType, ':max_entries' => $limits['max_entries'], ':max_users' => $limits['max_users'], ':features' => json_encode($limits['features']), ':expires' => $expiresAt ]); return [ 'success' => true, 'message' => 'License activated successfully', 'type' => $licenseType, 'expires_at' => $expiresAt ]; } catch (Exception $e) { return ['success' => false, 'error' => 'Failed to activate license: ' . $e->getMessage()]; } } /** * Deactivate current license */ function deactivateLicense($db) { try { $db->exec("UPDATE license_info SET is_active = 0"); return ['success' => true, 'message' => 'License deactivated']; } catch (Exception $e) { return ['success' => false, 'error' => 'Failed to deactivate license']; } } /** * Get usage statistics for license */ function getLicenseUsage($db = null) { if (!$db && function_exists('getDB')) { $db = getDB(); } $status = getLicenseStatus($db); $entryCount = 0; $userCount = 0; try { $stmt = $db->query("SELECT COUNT(*) FROM geofeed_entries"); $entryCount = (int)$stmt->fetchColumn(); $stmt = $db->query("SELECT COUNT(*) FROM admin_users WHERE active = 1"); $userCount = (int)$stmt->fetchColumn(); } catch (Exception $e) { // Ignore } $maxEntries = $status['limits']['max_entries']; $maxUsers = $status['limits']['max_users']; return [ 'entries' => [ 'current' => $entryCount, 'max' => $maxEntries, 'percentage' => $maxEntries > 0 ? round(($entryCount / $maxEntries) * 100) : 0, 'unlimited' => $maxEntries == -1 ], 'users' => [ 'current' => $userCount, 'max' => $maxUsers, 'percentage' => $maxUsers > 0 ? round(($userCount / $maxUsers) * 100) : 0, 'unlimited' => $maxUsers == -1 ], 'license' => $status ]; }