DbFactory.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. using System.Reflection;
  2. using FluentResults;
  3. using InABox.Clients;
  4. using InABox.Configuration;
  5. using InABox.Core;
  6. using InABox.Scripting;
  7. namespace InABox.Database;
  8. public class DatabaseMetadata : BaseObject, IGlobalConfigurationSettings
  9. {
  10. public Guid DatabaseID { get; set; } = Guid.NewGuid();
  11. }
  12. public static class DbFactory
  13. {
  14. public static Dictionary<string, ScriptDocument> LoadedScripts = new();
  15. private static DatabaseMetadata MetaData { get; set; } = new();
  16. public static Guid ID
  17. {
  18. get => MetaData.DatabaseID;
  19. set
  20. {
  21. MetaData.DatabaseID = value;
  22. SaveMetadata();
  23. }
  24. }
  25. private static IProvider? _provider;
  26. public static IProvider Provider
  27. {
  28. get => _provider ?? throw new Exception("Provider is not set");
  29. set => _provider = value;
  30. }
  31. public static bool IsProviderSet => _provider is not null;
  32. public static string? ColorScheme { get; set; }
  33. public static byte[]? Logo { get; set; }
  34. // See notes in Request.DatabaseInfo class
  35. // Once RPC transport is stable, these settings need
  36. // to be removed
  37. public static int RestPort { get; set; }
  38. public static int RPCPort { get; set; }
  39. //public static Type[] Entities { get { return entities; } set { SetEntityTypes(value); } }
  40. public static IEnumerable<Type> Entities
  41. {
  42. get { return CoreUtils.Entities.Where(x => x.GetInterfaces().Contains(typeof(IPersistent))); }
  43. }
  44. public static Type[] Stores
  45. {
  46. get => stores;
  47. set => SetStoreTypes(value);
  48. }
  49. public static DateTime Expiry { get; set; }
  50. public static void Start()
  51. {
  52. CoreUtils.CheckLicensing();
  53. var status = ValidateSchema();
  54. if (status.Equals(SchemaStatus.New))
  55. try
  56. {
  57. Provider.CreateSchema(ConsolidatedObjectModel().ToArray());
  58. SaveSchema();
  59. }
  60. catch (Exception err)
  61. {
  62. throw new Exception(string.Format("Unable to Create Schema\n\n{0}", err.Message));
  63. }
  64. else if (status.Equals(SchemaStatus.Changed))
  65. try
  66. {
  67. Provider.UpgradeSchema(ConsolidatedObjectModel().ToArray());
  68. SaveSchema();
  69. }
  70. catch (Exception err)
  71. {
  72. throw new Exception(string.Format("Unable to Update Schema\n\n{0}", err.Message));
  73. }
  74. // Start the provider
  75. Provider.Types = ConsolidatedObjectModel();
  76. Provider.OnLog += LogMessage;
  77. Provider.Start();
  78. CheckMetadata();
  79. if (!DataUpdater.MigrateDatabase())
  80. {
  81. throw new Exception("Database migration failed. Aborting startup");
  82. }
  83. //Load up your custom properties here!
  84. // Can't use clients (b/c we're inside the database layer already
  85. // but we can simply access the store directly :-)
  86. //CustomProperty[] props = FindStore<CustomProperty>("", "", "", "").Load(new Filter<CustomProperty>(x=>x.ID).IsNotEqualTo(Guid.Empty),null);
  87. var props = Provider.Query<CustomProperty>().Rows.Select(x => x.ToObject<CustomProperty>()).ToArray();
  88. DatabaseSchema.Load(props);
  89. AssertLicense();
  90. BeginLicenseCheckTimer();
  91. InitStores();
  92. LoadScripts();
  93. }
  94. #region MetaData
  95. private static void SaveMetadata()
  96. {
  97. var settings = new GlobalSettings
  98. {
  99. Section = nameof(DatabaseMetadata),
  100. Key = "",
  101. Contents = Serialization.Serialize(MetaData)
  102. };
  103. DbFactory.Provider.Save(settings);
  104. }
  105. private static void CheckMetadata()
  106. {
  107. var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseMetadata)))
  108. .Rows.FirstOrDefault()?.ToObject<GlobalSettings>();
  109. var data = result is not null ? Serialization.Deserialize<DatabaseMetadata>(result.Contents) : null;
  110. if (data is null)
  111. {
  112. MetaData = new DatabaseMetadata();
  113. SaveMetadata();
  114. }
  115. else
  116. {
  117. MetaData = data;
  118. }
  119. }
  120. #endregion
  121. #region License
  122. private enum LicenseValidation
  123. {
  124. Valid,
  125. Missing,
  126. Expired,
  127. Corrupt,
  128. Tampered
  129. }
  130. private static LicenseValidation CheckLicenseValidity(out DateTime expiry)
  131. {
  132. expiry = DateTime.MinValue;
  133. var license = Provider.Load<License>().FirstOrDefault();
  134. if (license is null)
  135. return LicenseValidation.Missing;
  136. if (!LicenseUtils.TryDecryptLicense(license.Data, out var licenseData, out var error))
  137. return LicenseValidation.Corrupt;
  138. if (!LicenseUtils.ValidateMacAddresses(licenseData.Addresses))
  139. return LicenseValidation.Tampered;
  140. var userTrackingItems = Provider.Query(
  141. new Filter<UserTracking>(x => x.ID).InList(licenseData.UserTrackingItems),
  142. new Columns<UserTracking>(x => x.ID)
  143. , log: false
  144. ).Rows
  145. .Select(r => r.Get<UserTracking, Guid>(c => c.ID))
  146. .ToArray();
  147. foreach(var item in licenseData.UserTrackingItems)
  148. {
  149. if (!userTrackingItems.Contains(item))
  150. return LicenseValidation.Tampered;
  151. }
  152. expiry = licenseData.Expiry;
  153. if (licenseData.Expiry < DateTime.Now)
  154. return LicenseValidation.Expired;
  155. return LicenseValidation.Valid;
  156. }
  157. private static int _expiredLicenseCounter = 0;
  158. private static TimeSpan LicenseCheckInterval = TimeSpan.FromMinutes(10);
  159. private static bool _readOnly;
  160. public static bool IsReadOnly { get => _readOnly; }
  161. private static System.Timers.Timer LicenseTimer = new System.Timers.Timer(LicenseCheckInterval.TotalMilliseconds) { AutoReset = true };
  162. private static void LogRenew(string message)
  163. {
  164. 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://prsdigital.com.au/wiki/index.php/License_Renewal.");
  165. }
  166. public static void LogReadOnly()
  167. {
  168. LogImportant($"Your database is in read-only mode; please renew your license to enable database updates.");
  169. }
  170. private static void LogLicenseExpiry(DateTime expiry)
  171. {
  172. if (expiry.Date == DateTime.Today)
  173. {
  174. LogRenew($"Your database license is expiring today at {expiry.TimeOfDay:HH:mm}!");
  175. return;
  176. }
  177. var diffInDays = (expiry - DateTime.Now).TotalDays;
  178. if(diffInDays < 1)
  179. {
  180. LogRenew($"Your database license will expire in less than a day, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
  181. }
  182. else if(diffInDays < 3 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 1)
  183. {
  184. LogRenew($"Your database license will expire in less than three days, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
  185. _expiredLicenseCounter = 0;
  186. }
  187. else if(diffInDays < 7 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 2)
  188. {
  189. LogRenew($"Your database license will expire in less than a week, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
  190. _expiredLicenseCounter = 0;
  191. }
  192. ++_expiredLicenseCounter;
  193. }
  194. private static void BeginReadOnly()
  195. {
  196. if (!IsReadOnly)
  197. {
  198. LogImportant(
  199. "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://prsdigital.com.au/wiki/index.php/License_Renewal.");
  200. _readOnly = true;
  201. }
  202. }
  203. private static void EndReadOnly()
  204. {
  205. if (IsReadOnly)
  206. {
  207. LogImportant("Valid license found; the database is no longer read-only.");
  208. _readOnly = false;
  209. }
  210. }
  211. private static void BeginLicenseCheckTimer()
  212. {
  213. LicenseTimer.Elapsed += LicenseTimer_Elapsed;
  214. LicenseTimer.Start();
  215. }
  216. private static void LicenseTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
  217. {
  218. AssertLicense();
  219. }
  220. public static void AssertLicense()
  221. {
  222. var result = CheckLicenseValidity(out DateTime expiry);
  223. switch (result)
  224. {
  225. case LicenseValidation.Valid:
  226. LogLicenseExpiry(expiry);
  227. EndReadOnly();
  228. break;
  229. case LicenseValidation.Missing:
  230. LogImportant("Database is unlicensed!");
  231. BeginReadOnly();
  232. break;
  233. case LicenseValidation.Expired:
  234. LogImportant("Database license has expired!");
  235. BeginReadOnly();
  236. break;
  237. case LicenseValidation.Corrupt:
  238. LogImportant("Database license is corrupt - you will need to renew your license.");
  239. BeginReadOnly();
  240. break;
  241. case LicenseValidation.Tampered:
  242. LogImportant("Database license has been tampered with - you will need to renew your license.");
  243. BeginReadOnly();
  244. break;
  245. }
  246. }
  247. #endregion
  248. #region Logging
  249. private static void LogMessage(LogType type, string message)
  250. {
  251. Logger.Send(type, "", message);
  252. }
  253. private static void LogInfo(string message)
  254. {
  255. Logger.Send(LogType.Information, "", message);
  256. }
  257. private static void LogImportant(string message)
  258. {
  259. Logger.Send(LogType.Important, "", message);
  260. }
  261. private static void LogError(string message)
  262. {
  263. Logger.Send(LogType.Error, "", message);
  264. }
  265. #endregion
  266. public static void InitStores()
  267. {
  268. foreach (var storetype in stores)
  269. {
  270. var store = (Activator.CreateInstance(storetype) as IStore)!;
  271. store.Provider = Provider;
  272. store.Init();
  273. }
  274. }
  275. public static IStore FindStore(Type type, Guid userguid, string userid, Platform platform, string version)
  276. {
  277. var defType = typeof(Store<>).MakeGenericType(type);
  278. Type? subType = Stores.Where(myType => myType.IsSubclassOf(defType)).FirstOrDefault();
  279. var store = (Activator.CreateInstance(subType ?? defType) as IStore)!;
  280. store.Provider = Provider;
  281. store.UserGuid = userguid;
  282. store.UserID = userid;
  283. store.Platform = platform;
  284. store.Version = version;
  285. return store;
  286. }
  287. public static IStore<TEntity> FindStore<TEntity>(Guid userguid, string userid, Platform platform, string version)
  288. where TEntity : Entity, new()
  289. {
  290. return (FindStore(typeof(TEntity), userguid, userid, platform, version) as IStore<TEntity>)!;
  291. }
  292. private static CoreTable DoQueryMultipleQuery<TEntity>(
  293. IQueryDef query,
  294. Guid userguid, string userid, Platform platform, string version)
  295. where TEntity : Entity, new()
  296. {
  297. var store = FindStore<TEntity>(userguid, userid, platform, version);
  298. return store.Query(query.Filter as Filter<TEntity>, query.Columns as Columns<TEntity>, query.SortOrder as SortOrder<TEntity>);
  299. }
  300. public static Dictionary<string, CoreTable> QueryMultiple(
  301. Dictionary<string, IQueryDef> queries,
  302. Guid userguid, string userid, Platform platform, string version)
  303. {
  304. var result = new Dictionary<string, CoreTable>();
  305. var queryMethod = typeof(DbFactory).GetMethod(nameof(DoQueryMultipleQuery), BindingFlags.NonPublic | BindingFlags.Static)!;
  306. var tasks = new List<Task>();
  307. foreach (var item in queries)
  308. tasks.Add(Task.Run(() =>
  309. {
  310. result[item.Key] = (queryMethod.MakeGenericMethod(item.Value.Type).Invoke(Provider, new object[]
  311. {
  312. item.Value,
  313. userguid, userid, platform, version
  314. }) as CoreTable)!;
  315. }));
  316. Task.WaitAll(tasks.ToArray());
  317. return result;
  318. }
  319. #region Supported Types
  320. private class ModuleConfiguration : Dictionary<string, bool>, ILocalConfigurationSettings
  321. {
  322. }
  323. private static Type[]? _dbtypes;
  324. public static IEnumerable<string> SupportedTypes()
  325. {
  326. _dbtypes ??= LoadSupportedTypes();
  327. return _dbtypes.Select(x => x.EntityName().Replace(".", "_"));
  328. }
  329. private static Type[] LoadSupportedTypes()
  330. {
  331. var result = new List<Type>();
  332. var path = Provider.URL.ToLower();
  333. var config = new LocalConfiguration<ModuleConfiguration>(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Load();
  334. var bChanged = false;
  335. foreach (var type in Entities)
  336. {
  337. var key = type.EntityName();
  338. if (config.ContainsKey(key))
  339. {
  340. if (config[key])
  341. //Logger.Send(LogType.Information, "", String.Format("{0} is enabled", key));
  342. result.Add(type);
  343. else
  344. Logger.Send(LogType.Information, "", string.Format("Entity [{0}] is disabled", key));
  345. }
  346. else
  347. {
  348. //Logger.Send(LogType.Information, "", String.Format("{0} does not exist - enabling", key));
  349. config[key] = true;
  350. result.Add(type);
  351. bChanged = true;
  352. }
  353. }
  354. if (bChanged)
  355. new LocalConfiguration<ModuleConfiguration>(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Save(config);
  356. return result.ToArray();
  357. }
  358. public static bool IsSupported<T>() where T : Entity
  359. {
  360. _dbtypes ??= LoadSupportedTypes();
  361. return _dbtypes.Contains(typeof(T));
  362. }
  363. #endregion
  364. //public static void OpenSession(bool write)
  365. //{
  366. // Provider.OpenSession(write);
  367. //}
  368. //public static void CloseSession()
  369. //{
  370. // Provider.CloseSession();
  371. //}
  372. #region Private Methods
  373. public static void LoadScripts()
  374. {
  375. Logger.Send(LogType.Information, "", "Loading Script Cache...");
  376. LoadedScripts.Clear();
  377. var scripts = Provider.Load(
  378. new Filter<Script>
  379. (x => x.ScriptType).IsEqualTo(ScriptType.BeforeQuery)
  380. .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterQuery)
  381. .Or(x => x.ScriptType).IsEqualTo(ScriptType.BeforeSave)
  382. .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterSave)
  383. .Or(x => x.ScriptType).IsEqualTo(ScriptType.BeforeDelete)
  384. .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterDelete)
  385. .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterLoad)
  386. );
  387. foreach (var script in scripts)
  388. {
  389. var key = string.Format("{0} {1}", script.Section, script.ScriptType.ToString());
  390. var doc = new ScriptDocument(script.Code);
  391. if (doc.Compile())
  392. {
  393. Logger.Send(LogType.Information, "",
  394. string.Format("- {0}.{1} Compiled Successfully", script.Section, script.ScriptType.ToString()));
  395. LoadedScripts[key] = doc;
  396. }
  397. else
  398. {
  399. Logger.Send(LogType.Error, "",
  400. string.Format("- {0}.{1} Compile Exception:\n{2}", script.Section, script.ScriptType.ToString(), doc.Result));
  401. }
  402. }
  403. Logger.Send(LogType.Information, "", "Loading Script Cache Complete");
  404. }
  405. //private static Type[] entities = null;
  406. //private static void SetEntityTypes(Type[] types)
  407. //{
  408. // foreach (Type type in types)
  409. // {
  410. // if (!type.IsSubclassOf(typeof(Entity)))
  411. // throw new Exception(String.Format("{0} is not a valid entity", type.Name));
  412. // }
  413. // entities = types;
  414. //}
  415. private static Type[] stores = { };
  416. private static void SetStoreTypes(Type[] types)
  417. {
  418. types = types.Where(
  419. myType => myType.IsClass
  420. && !myType.IsAbstract
  421. && !myType.IsGenericType).ToArray();
  422. foreach (var type in types)
  423. if (!type.GetInterfaces().Contains(typeof(IStore)))
  424. throw new Exception(string.Format("{0} is not a valid store", type.Name));
  425. stores = types;
  426. }
  427. private static Type[] ConsolidatedObjectModel()
  428. {
  429. // Add the core types from InABox.Core
  430. var types = new List<Type>();
  431. //var coreTypes = CoreUtils.TypeList(
  432. // new Assembly[] { typeof(Entity).Assembly },
  433. // myType =>
  434. // myType.IsClass
  435. // && !myType.IsAbstract
  436. // && !myType.IsGenericType
  437. // && myType.IsSubclassOf(typeof(Entity))
  438. // && myType.GetInterfaces().Contains(typeof(IRemotable))
  439. //);
  440. //types.AddRange(coreTypes);
  441. // Now add the end-user object model
  442. types.AddRange(Entities.Where(x =>
  443. x.GetTypeInfo().IsClass
  444. && !x.GetTypeInfo().IsGenericType
  445. && x.GetTypeInfo().IsSubclassOf(typeof(Entity))
  446. ));
  447. return types.ToArray();
  448. }
  449. private enum SchemaStatus
  450. {
  451. New,
  452. Changed,
  453. Validated
  454. }
  455. private static Dictionary<string, Type> GetSchema()
  456. {
  457. var model = new Dictionary<string, Type>();
  458. var objectmodel = ConsolidatedObjectModel();
  459. foreach (var type in objectmodel)
  460. {
  461. Dictionary<string, Type> thismodel = CoreUtils.PropertyList(type, x => true, true);
  462. foreach (var key in thismodel.Keys)
  463. model[type.Name + "." + key] = thismodel[key];
  464. }
  465. return model;
  466. //return Serialization.Serialize(model, Formatting.Indented);
  467. }
  468. private static SchemaStatus ValidateSchema()
  469. {
  470. var db_schema = Provider.GetSchema();
  471. if (db_schema.Count() == 0)
  472. return SchemaStatus.New;
  473. var mdl_json = Serialization.Serialize(GetSchema());
  474. var db_json = Serialization.Serialize(db_schema);
  475. return mdl_json.Equals(db_json) ? SchemaStatus.Validated : SchemaStatus.Changed;
  476. }
  477. private static void SaveSchema()
  478. {
  479. Provider.SaveSchema(GetSchema());
  480. }
  481. #endregion
  482. }