using System.Composition; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using InABox.Clients; using InABox.Configuration; using InABox.Core; using InABox.Scripting; using Microsoft.CodeAnalysis.CSharp; namespace InABox.Database { public static class DbFactory { public static Dictionary LoadedScripts = new(); private static string _deviceid = ""; private static IProvider? _provider; public static IProvider Provider { get => _provider ?? throw new Exception("Provider is not set"); set => _provider = value; } public static string? ColorScheme { get; set; } public static byte[]? Logo { get; set; } //public static Type[] Entities { get { return entities; } set { SetEntityTypes(value); } } public static IEnumerable Entities { get { return CoreUtils.Entities.Where(x => x.GetInterfaces().Contains(typeof(IPersistent))); } } public static Type[] Stores { get => stores; set => SetStoreTypes(value); } public static DateTime Expiry { get; set; } public static void Start(string deviceid) { CoreUtils.CheckLicensing(); _deviceid = deviceid; var status = ValidateSchema(); if (status.Equals(SchemaStatus.New)) try { Provider.CreateSchema(ConsolidatedObjectModel().ToArray()); SaveSchema(); } catch (Exception err) { throw new Exception(string.Format("Unable to Create Schema\n\n{0}", err.Message)); } else if (status.Equals(SchemaStatus.Changed)) try { Provider.UpgradeSchema(ConsolidatedObjectModel().ToArray()); SaveSchema(); } catch (Exception err) { throw new Exception(string.Format("Unable to Update Schema\n\n{0}", err.Message)); } // Start the provider Provider.Types = ConsolidatedObjectModel(); Provider.OnLog += LogMessage; Provider.Start(); if (!DataUpdater.MigrateDatabase()) { throw new Exception("Database migration failed. Aborting startup"); } //Load up your custom properties here! // Can't use clients (b/c were inside the database layer already // but we can simply access the store directly :-) //CustomProperty[] props = FindStore("", "", "", "").Load(new Filter(x=>x.ID).IsNotEqualTo(Guid.Empty),null); var props = Provider.Query().Rows.Select(x => x.ToObject()).ToArray(); DatabaseSchema.Load(props); AssertLicense(); BeginLicenseCheckTimer(); InitStores(); LoadScripts(); } #region License private enum LicenseValidation { Valid, Missing, Expired, Corrupt, Tampered } private static LicenseValidation CheckLicenseValidity(out License? license, out LicenseData? licenseData) { license = Provider.Load().FirstOrDefault(); if (license is null) { licenseData = null; return LicenseValidation.Missing; } if (!LicenseUtils.TryDecryptLicense(license.Data, out licenseData, out var error)) return LicenseValidation.Corrupt; if (licenseData.Expiry < DateTime.Now) return LicenseValidation.Expired; var userTrackingItems = Provider.Query( new Filter(x => x.ID).InList(licenseData.UserTrackingItems), new Columns(x => x.ID), log: false).Rows.Select(x => x.Get(x => x.ID)); foreach(var item in licenseData.UserTrackingItems) { if (!userTrackingItems.Contains(item)) { return LicenseValidation.Tampered; } } return LicenseValidation.Valid; } private static int _expiredLicenseCounter = 0; private static TimeSpan LicenseCheckInterval = TimeSpan.FromMinutes(10); private static bool _readOnly; public static bool IsReadOnly { get => _readOnly; } private static System.Timers.Timer LicenseTimer = new System.Timers.Timer(LicenseCheckInterval.TotalMilliseconds) { AutoReset = true }; private static void LogRenew(string message) { LogImportant($"{message} Please renew your license before then, or your database will go into read-only mode; it will be locked for saving anything until you renew your license. For help with renewing your license, please see the documentation at https://prs-software.com.au/wiki/index.php/License_Renewal."); } private static void LogLicenseExpiry(DateTime expiry) { if (expiry.Date == DateTime.Today) { LogRenew($"Your database license is expiring today at {expiry.TimeOfDay:HH:mm}!"); return; } var diffInDays = (expiry - DateTime.Now).TotalDays; if(diffInDays < 1) { LogRenew($"Your database license will expire in less than a day, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}."); } else if(diffInDays < 3 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 1) { LogRenew($"Your database license will expire in less than three days, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}."); _expiredLicenseCounter = 0; } else if(diffInDays < 7 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 2) { LogRenew($"Your database license will expire in less than a week, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}."); _expiredLicenseCounter = 0; } ++_expiredLicenseCounter; } public static void LogReadOnly() { LogError("Database is read-only because your license is invalid!"); } private static void BeginReadOnly() { LogImportant("Your database is now in read-only mode, since your license is invalid; you will be unable to save any records to the database until you renew your license. For help with renewing your license, please see the documentation at https://prs-software.com.au/wiki/index.php/License_Renewal."); _readOnly = true; } private static void EndReadOnly() { LogImportant("Valid license found; the database is no longer read-only."); _readOnly = false; } private static void BeginLicenseCheckTimer() { LicenseTimer.Elapsed += LicenseTimer_Elapsed; LicenseTimer.Start(); } private static void LicenseTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { AssertLicense(); } private static Random LicenseIDGenerate = new Random(); private static void UpdateValidLicense(License license, LicenseData licenseData) { var ids = Provider.Query( new Filter(x => x.Created).IsGreaterThanOrEqualTo(licenseData.LastRenewal), new Columns(x => x.ID), log: false); var newIDList = new List(); if(ids.Rows.Count > 0) { for (int i = 0; i < 10; i++) { newIDList.Add(ids.Rows[LicenseIDGenerate.Next(0, ids.Rows.Count)].Get(x => x.ID)); } } licenseData.UserTrackingItems = newIDList.ToArray(); if(LicenseUtils.TryEncryptLicense(licenseData, out var newData, out var error)) { license.Data = newData; Provider.Save(license); } } private static void AssertLicense() { var result = CheckLicenseValidity(out var license, out var licenseData); if (IsReadOnly) { if(result == LicenseValidation.Valid) { EndReadOnly(); } return; } // TODO: Switch to real system if(result != LicenseValidation.Valid) { var newLicense = LicenseUtils.GenerateNewLicense(); if (LicenseUtils.TryEncryptLicense(newLicense, out var newData, out var error)) { license.Data = newData; Provider.Save(license); } else { Logger.Send(LogType.Error, "", $"Error updating license: {error}"); } return; } else { return; } switch (result) { case LicenseValidation.Valid: LogLicenseExpiry(licenseData!.Expiry); UpdateValidLicense(license, licenseData); break; case LicenseValidation.Missing: LogImportant("Database is unlicensed!"); BeginReadOnly(); break; case LicenseValidation.Expired: LogImportant("Database license has expired!"); BeginReadOnly(); break; case LicenseValidation.Corrupt: LogImportant("Database license is corrupt - you will need to renew your license."); BeginReadOnly(); break; case LicenseValidation.Tampered: LogImportant("Database license has been tampered with - you will need to renew your license."); BeginReadOnly(); break; } } #endregion #region Logging private static void LogMessage(LogType type, string message) { Logger.Send(type, "", message); } private static void LogInfo(string message) { Logger.Send(LogType.Information, "", message); } private static void LogImportant(string message) { Logger.Send(LogType.Important, "", message); } private static void LogError(string message) { Logger.Send(LogType.Error, "", message); } #endregion public static void InitStores() { foreach (var storetype in stores) { var store = Activator.CreateInstance(storetype) as IStore; store.Provider = Provider; store.Init(); } } public static IStore FindStore(Guid userguid, string userid, string platform, string version) where TEntity : Entity, new() { var defType = typeof(Store<>).MakeGenericType(typeof(TEntity)); Type? subType = Stores.Where(myType => myType.IsSubclassOf(defType)).FirstOrDefault(); var store = (Store)Activator.CreateInstance(subType ?? defType)!; store.Provider = Provider; store.UserGuid = userguid; store.UserID = userid; store.Platform = platform; store.Version = version; return store; } private static CoreTable DoQueryMultipleQuery( IQueryDef query, Guid userguid, string userid, string platform, string version) where TEntity : Entity, new() { var store = FindStore(userguid, userid, platform, version); return store.Query(query.Filter as Filter, query.Columns as Columns, query.SortOrder as SortOrder); } public static Dictionary QueryMultiple( Dictionary queries, Guid userguid, string userid, string platform, string version) { var result = new Dictionary(); var queryMethod = typeof(DbFactory).GetMethod(nameof(DoQueryMultipleQuery), BindingFlags.NonPublic | BindingFlags.Static)!; var tasks = new List(); foreach (var item in queries) tasks.Add(Task.Run(() => { result[item.Key] = (queryMethod.MakeGenericMethod(item.Value.Type).Invoke(Provider, new object[] { item.Value, userguid, userid, platform, version }) as CoreTable)!; })); Task.WaitAll(tasks.ToArray()); return result; } #region Supported Types private class ModuleConfiguration : Dictionary, LocalConfigurationSettings { } private static Type[]? _dbtypes; public static IEnumerable SupportedTypes() { _dbtypes ??= LoadSupportedTypes(); return _dbtypes.Select(x => x.EntityName().Replace(".", "_")); } private static Type[] LoadSupportedTypes() { var result = new List(); var path = Provider.URL.ToLower(); var config = new LocalConfiguration(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Load(); var bChanged = false; foreach (var type in Entities) { var key = type.EntityName(); if (config.ContainsKey(key)) { if (config[key]) //Logger.Send(LogType.Information, "", String.Format("{0} is enabled", key)); result.Add(type); else Logger.Send(LogType.Information, "", string.Format("Entity [{0}] is disabled", key)); } else { //Logger.Send(LogType.Information, "", String.Format("{0} does not exist - enabling", key)); config[key] = true; result.Add(type); bChanged = true; } } if (bChanged) new LocalConfiguration(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Save(config); return result.ToArray(); } public static bool IsSupported() where T : Entity { _dbtypes ??= LoadSupportedTypes(); return _dbtypes.Contains(typeof(T)); } #endregion //public static void OpenSession(bool write) //{ // Provider.OpenSession(write); //} //public static void CloseSession() //{ // Provider.CloseSession(); //} #region Private Methods public static void LoadScripts() { Logger.Send(LogType.Information, "", "Loading Script Cache..."); LoadedScripts.Clear(); var scripts = Provider.Load( new Filter