using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.Bus; using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Base.Email; using Sarand.Net.Standard.Base.Helpers; using Sarand.Net.Standard.Base.Utils; using Saxonite.Net.Core.Web.Base.App; using Saxonite.Net.Core.Web.Base.Consoles; using Saxonite.Net.Core.Web.Base.Models; using Saxonite.Net.Standard.Base.App; using Saxonite.Net.Standard.Base.Bus; using Saxonite.Net.Standard.Base.Consoles; using Saxonite.Net.Standard.Base.Mgr; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Web; namespace Saxonite.Net.Core.Web.Base.Controllers { //======================================================================= public abstract class SaxController_Base : Controller { //------------------------------------------------------------------- // Data members. private SaxInternals internals; //------------------------------------------------------------------- // Protected construction. protected SaxController_Base(SaxLocaliser localiser) { CreatedAt = DateTime.Now; Localiser = localiser; // Initialise console (debug) logging listener. //if (Directory.Exists(App.LoggingRootFolder) && SaxSystem.DebugMode) // SaxConsoleListener.ConfigureDebugListener(App.LoggingRootFolder); if (DebugMode) SaLogger.Trace(GetType().Name + ".CTR"); } protected override void Dispose(bool disposing) { if (disposing) { } base.Dispose(disposing); } //------------------------------------------------------------------- // Properties. public DateTime CreatedAt { get; } public bool DebugMode { get; } = SacsSystem.DebugMode; protected SaxApplication_Web App { get { return SaxApplication_Web.WebApp; } } public SaxAppConfig AppConfig { get { return App.AppConfig; } } public SaxSessionContext SessionContext { get { return App.SessionContext; } } public SaxLocaliser Localiser { get; } /// /// Indicate whether session data should be stored by action. /// Any action that might modify the session data must store it. /// Some actions such as scoring a password's complexity don't need to. /// public virtual bool StoreSessionData { get; protected set; } = true; //------------------------------------------------------------------- // Culture. private CultureInfo sessionCulture; private CultureInfo RequestCulture { get { return Request.HttpContext.Features.Get().RequestCulture.Culture ?? CultureInfo.InvariantCulture; } } public CultureInfo SessionCulture { get { if (sessionCulture == null) { sessionCulture = RequestCulture; // ToDo Adjust culture if needed. } return sessionCulture; } } private string LanguageIso2 { get { var feature = HttpContext.Features.Get(); return feature.RequestCulture.UICulture.TwoLetterISOLanguageName.ToLower(); } } //------------------------------------------------------------------- // Business components. public SaxInternals Internals { get { if (internals == null) internals = new SaxInternals(SessionContext); return internals; } } // Consoles. public SaxDataCacheConsole DataCacheConsole { get { return Internals.DataCacheConsole; } } public SaxSessionDataConsole SessionDataConsole { get { return Internals.SessionDataConsole; } } public SaxActivityConsole ActivityConsole { get { return Internals.ActivityConsole; } } // Managers. public SaxReferenceMgr ReferenceMgr { get { return Internals.ReferenceMgr; } } public SaxAccountMgr AccountMgr { get { return Internals.AccountMgr; } } public SaxActivityMgr ActivityMgr { get { return Internals.ActivityMgr; } } public SaxItemDataMgr ItemDataMgr { get { return Internals.ItemDataMgr; } } // System codes. public List DataItemTypeCodes { get { return Internals.DataItemTypeCodes; } } public List SportSpecialityLabelCodes { get { return Internals.SportSpecialityLabelCodes; } } public List SportParticipationCodes { get { return Internals.SportParticipationCodes; } } public List SportOfficiatingCodes { get { return Internals.SportOfficiatingCodes; } } public List RegionTypeCodes { get { return Internals.RegionTypeCodes; } } public List UserRoleCodes { get { return Internals.UserRoleCodes; } } public List OrganisationTypeCodes { get { return Internals.OrganisationTypeCodes; } } public List OrganisationCategoryCodes { get { return Internals.OrganisationCategoryCodes; } } public List OrganisationRoleCodes { get { return Internals.OrganisationRoleCodes; } } public List AddressTypeCodes { get { return Internals.AddressTypeCodes; } } public List EventTypeCodes { get { return Internals.EventTypeCodes; } } public List OpportunityTypeCodes { get { return Internals.OpportunityTypeCodes; } } public List ActivityReactionCodes { get { return Internals.ActivityReactionCodes; } } // Email. public SaEmailConfig EmailConfig { get { return App.EmailConfig; } } public SaxEmailConsole EmailConsole { get { return Internals.EmailConsole; } } //------------------------------------------------------------------- // Redirection locations. //------------------------------------------------------------------- public static readonly String ParamName_SessionExpired = "SessionExpired"; public enum RedirectLocation { WebAppBase, WebAppHome, WebAppHome_SessionExpired, } public String GetRedirectLocation(RedirectLocation redirectLocation) { string protocol = Request.Scheme; var parameters = new RouteValueDictionary(); String homeAction = Url.Action("Index", "Home", parameters, protocol); switch (redirectLocation) { case RedirectLocation.WebAppBase: return Regex.Replace(homeAction, "\\/$", "") + "/"; case RedirectLocation.WebAppHome: return homeAction; case RedirectLocation.WebAppHome_SessionExpired: // Set the session expired parameter so that the page can report it. // NB Paramater must have a value to be included in the URL query string. parameters.Add(ParamName_SessionExpired, "1"); return homeAction; } return ""; } //------------------------------------------------------------------- // Action execution events. //------------------------------------------------------------------- public override void OnActionExecuting(ActionExecutingContext filterContext) { //if (DebugMode) SaLogger.Debug(GetType().Name + ".OnActionExecuting" + (ViewData.Model != null ? " Model: " + ViewData.Model : "")); // Refreh model properties. SaxModel_Base baseModel = ViewData.Model as SaxModel_Base; RefreshModel(baseModel); base.OnActionExecuting(filterContext); } public override void OnActionExecuted(ActionExecutedContext filterContext) { //if (DebugMode) SaLogger.Debug(GetType().Name + ".OnActionExecuted" + (ViewData.Model != null ? " Model: " + ViewData.Model : "")); // Refreh model properties. SaxModel_Base baseModel = ViewData.Model as SaxModel_Base; RefreshModel(baseModel); // Store session data if present - as potentially modfied. if (SessionData != null) { // Most actions will store the session data in case it's been modified. // Some actions (eg. score password) don't need to. if (StoreSessionData) { String sessionDataJson = JsonConvert.SerializeObject(SessionData); HttpContext.Session.Set(SessionStorage.SessionKey_SessionData, SessionStorage.sessionDataEncoding.GetBytes(sessionDataJson)); } } else HttpContext.Session.Remove(SessionStorage.SessionKey_SessionData); // Response headers. if (baseModel != null && baseModel.ProhibitCache) { if (Response.Headers.ContainsKey(HeaderNames.CacheControl)) Response.Headers.Remove(HeaderNames.CacheControl); Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate"); Response.Headers.Add(HeaderNames.Pragma, "no-cache"); Response.Headers.Add(HeaderNames.Expires, "0"); } base.OnActionExecuted(filterContext); } //------------------------------------------------------------------- // Model. //------------------------------------------------------------------- protected void RefreshModel(SaxModel_Base model) { if (model == null) return; // Session data. model.Controller = this; model.SessionData = SessionData; // Redirect locations. model.RedirectLocation.Base = GetRedirectLocation(RedirectLocation.WebAppBase); model.RedirectLocation.Home = GetRedirectLocation(RedirectLocation.WebAppHome); model.RedirectLocation.Home_SessionExpired = GetRedirectLocation(RedirectLocation.WebAppHome_SessionExpired); } //------------------------------------------------------------------- // Parameters. //------------------------------------------------------------------- // Online Request Password encryption parameters (key & IV) are defined in // the base Controller so that they can be shared with the Adnin application. // NB IV value is generated from the key and fixed so that it doesn't change when the application restarts. // IV Must be regenerated if key is changed. protected static String Base64Key_RequestPassword_Online { get; } = SacsEncryptionHelper.EncodePlainKey("{~Saxonite@@Request^Password=}", SacsEncryptionHelper.KeyLength); //private static String Base64IV_RequestPassword_Generated_Online { get; } = SacsEncryptionHelper.InitSymmetricEncryptionIV(Base64Key_RequestPassword_Online); protected static String Base64IV_RequestPassword_Online { get; } = "1VAWYTW6d72n/6HDNhbqow=="; //------------------------------------------------------------------- public static String EncryptParameters(NameValueCollection parameters, String base64IV, String base64Key) { String fn = nameof(ControllerBase) + ".EncryptParameters"; try { // Copy the list of parameters and sort the keys. NameValueCollection parametersToEncrypt = new NameValueCollection(parameters); String[] paramKeys = parametersToEncrypt.AllKeys; Array.Sort(paramKeys); // Serialise parameters as if in a URL query string - so that they can be deserialised in DecryptParameters() below. // Start with a random length parameter name at start of list to obscure the encryption. string queryString = SacsRandomHelper.getRandomLengthNumber(4) + "&"; queryString += String.Join("&", paramKeys.Select(a => a + "=" + HttpUtility.UrlEncode(parametersToEncrypt[a]))); // Encrypt data. String encrypted = SacsEncryptionHelper.Encrypt(queryString, base64IV, base64Key); //if (SacsSystem.DebugMode) SaLogger.Debug(fn + "\n\tData: " + queryString + " \n\tEncrypted: " + encrypted); return encrypted; } catch (Exception exc) { SaLogger.Warn(fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); } return ""; } public static NameValueCollection DecryptParameters(String encrypted, String base64IV, String base64Key) { String fn = nameof(ControllerBase) + ".DecryptParameters"; try { String queryString = SacsEncryptionHelper.Decrypt(encrypted, base64IV, base64Key); //if (SacsSystem.DebugMode) SaLogger.Debug(fn + " \n\tEncrypted: " + encrypted + "\n\tData: " + queryString); return HttpUtility.ParseQueryString(queryString); } catch (Exception exc) { SaLogger.Warn(fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); } return new NameValueCollection(); } //------------------------------------------------------------------- // Cookies. //------------------------------------------------------------------- /// Retrieve the value from the named request cookie. public String GetRequestCookie(String key) { if (Request.Cookies.ContainsKey(key)) return Request.Cookies[key]; return null; } /// Set the value against the named response cookie. public void SetResponseCookie(string key, string value, DateTime? expiresAt = null, bool isEssential = false) { CookieOptions options = new CookieOptions(); options.HttpOnly = true; options.Secure = true; options.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict; if (expiresAt.HasValue) options.Expires = expiresAt; if (isEssential) options.IsEssential = true; Response.Cookies.Append(key, value, options); } //------------------------------------------------------------------- // Session. //------------------------------------------------------------------- public bool ValidUserSession { get { if (SessionData == null || !SessionData.ValidUserSession) return false; SaxSessionDataConsole sessionDataConsole = SessionDataConsole; if (sessionDataConsole != null) sessionDataConsole.RecordUserActive(UserAccountId, CreatedAt); return true; } } public SysId UserAccountId { get { return SessionData?.User?.UserAccountId ?? null; } } public DbUpdateInfo DbUpdateInfo { get { return SessionData?.DbUpdateInfo ?? new DbUpdateInfo(null, "", SaxSystem.ComputerName); } } // Referrer. public String Referer { get { return Request.Headers["Referer"].ToString(); } } public bool ValidReferer { get { return !StringUtil.IsEmpty(Referer); } } /// Store session data, eg. to start a new user session on sign-in. protected bool SetSessionData(SaxSessionData sessionData) { if (sessionStorage.SessionData != null) return false; sessionStorage.SessionData = sessionData; return true; } protected void ClearSessionData() { if (HttpContext.Session.IsAvailable) HttpContext.Session.Clear(); sessionStorage.SessionData = null; } //------------------------------------------------------------------- // Session storage. public class SessionStorage { public SaxSessionData SessionData { get; set; } public static String SessionKey_SessionData { get; } = "SaxSessionData"; public static Encoding sessionDataEncoding { get; } = Encoding.UTF8; //JSON Serialisation. public class SaxSessionDataContractResolver : DefaultContractResolver { // To allow private properties to be deserialised. // NB Only applies to top level properties in the serialised object. protected override List GetSerializableMembers(Type objectType) { var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; MemberInfo[] fields = objectType.GetFields(flags); return fields .Concat(objectType.GetProperties(flags).Where(propInfo => propInfo.CanWrite)) .ToList(); } protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { return base.CreateProperties(type, MemberSerialization.Fields); } }; public static JsonSerializerSettings JsonDeserialisationSettings = new JsonSerializerSettings { ContractResolver = new SaxSessionDataContractResolver(), // Force all deserialised objects to be replaced - to avoid proxy lists not populating. ObjectCreationHandling = ObjectCreationHandling.Replace }; } private readonly SessionStorage sessionStorage = new SessionStorage(); protected JsonSerializerSettings SaxSessionDataJsonSerializerSettings { get { return SessionStorage.JsonDeserialisationSettings; } } //------------------------------------------------------------------- public SaxSessionData SessionData { get { try { // Retrieve session data if not already in controller. if (sessionStorage.SessionData == null) { byte[] sessionDataBytes = null; HttpContext.Session.TryGetValue(SessionStorage.SessionKey_SessionData, out sessionDataBytes); if (sessionDataBytes != null) { String sessionDataJson = SessionStorage.sessionDataEncoding.GetString(sessionDataBytes); sessionStorage.SessionData = DeserializeSessionData(sessionDataJson); } } return sessionStorage.SessionData; } catch (Exception exc) { SaLogger.Warn(GetType().Name + ".SessionData " + exc.GetType().Name + " - " + exc.Message + exc.GetStackEntries().Concat("", "\n\t")); } return null; } } protected abstract SaxSessionData DeserializeSessionData(String sessionDataJson); //------------------------------------------------------------------- // Ajax actions. //------------------------------------------------------------------- protected static readonly Encoding ajaxResponseEncoding = Encoding.UTF8; protected IActionResult GetAjaxResult_GoHome(AjaxResponse_Base responseData) { responseData.SetGoHome(); return Json(responseData); } protected IActionResult GetAjaxResult_InvalidSession(AjaxResponse_Base responseData) { responseData.SetGoHome(); return Json(responseData); } //------------------------------------------------------------------- // Response data. // Null values are ignored by JSON serialisation. // - See Saxonite.Net.Core.Web.Base.App.SaxStartup.ConfigureServices() // - Ref. config.services.AddMvc(...).AddJsonOptions(options => { options.JsonSerializerOptions.IgnoreNullValues = true; }); //------------------------------------------------------------------- // Base response data - for simple responses. public class AjaxResponse_Base { // Response status. public bool? Success { get; set; } = null; public bool? Error { get; set; } = null; public bool? SessionExpired { get; protected set; } = null; public bool? GoHome { get; protected set; } = null; public AjaxResponse_Base SetSessionExpired(String title, String text) { SessionExpired = true; return SetMessage(title, text); } public AjaxResponse_Base SetGoHome() { GoHome = true; return this; } public AjaxResponse_Base SetGoHome(String title, String text) { GoHome = true; return SetMessage(title, text); } // Message - displays alert popup. public class MessageData { public String Title { get; set; } public String Text { get; set; } } public MessageData Message { get; private set; } public AjaxResponse_Base SetMessage(String title, String text) { if (!StringUtil.IsEmpty(text)) Message = new MessageData() { Title = title, Text = text }; return this; } } //------------------------------------------------------------------- public class AjaxResponse_Data : AjaxResponse_Base { //--------------------------------------------------------------- // Construction. protected AjaxResponse_Data(SaxModel_Base model) { } //--------------------------------------------------------------- /// Item data - Common properties for data items. public class ItemData_Base { // Common item properties. public String Id { get; set; } public String StatusCode { get; set; } public String StatusDesc { get; set; } // Flags for common status values. // Return true or null so that property is not serialised when false. public bool? Awaiting { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Awaiting)) ? true : (bool?)null; } } public bool? Active { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Active)) ? true : (bool?)null; } } public bool? Inactive { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Inactive)) ? true : (bool?)null; } } public bool? Deleted { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Deleted)) ? true : (bool?)null; } } /// Created on date - YYYYMMDD public String CreatedOn { get; set; } /// Created on date - formatted per locale public String CreatedOnFmt { get; set; } /// Updated on date - YYYYMMDD public String UpdatedOn { get; set; } /// Updated on date - formatted per locale public String UpdatedOnFmt { get; set; } } /// /// Item data with just Id and name. /// - Id is String to allow for obfuscated values. /// public class ItemIdName { public String Id { get; set; } public String Name { get; set; } } //--------------------------------------------------------------- // Form data. public class FormControlValue { public FormControlValue(String controlName, String value, String error) { ControlName = controlName; Value = value; Error = error; } public String ControlName { get; } public String Value { get; set; } public String Error { get; set; } } private readonly Dictionary controlValues = new Dictionary(); public List ControlValues { get { return controlValues.Count > 0 ? controlValues.Values.ToList() : null; } } public void SetControlValue(String controlName, String value, String error = null) { if (controlValues.ContainsKey(controlName)) { // Modify existing entry in the list. FormControlValue controlValue = controlValues[controlName]; if (value != null) controlValue.Value = value; if (error != null) controlValue.Error = error; } else { // Add new control to list. controlValues.Add(controlName, new FormControlValue(controlName, value, error)); } } /// Assign the error to the control by name and return this response data. public AjaxResponse_Data SetControlError(String controlName, String error) { return SetControlError(controlName, error, this); } /// /// Assign the error to the control by name and return the specified value. /// - Allows alternative return values to be specified (eg. 'false') to avoid separate return statement in calling routine. /// public T SetControlError(String controlName, String error, T returnValue) { SetControlValue(controlName, null, error); return returnValue; } public bool HasControlErrors() { foreach (FormControlValue controlValue in controlValues.Values) { if (!StringUtil.IsEmpty(controlValue.Error)) return true; } return false; } //--------------------------------------------------------------- // Notification - briefly displays after response received. public class NotificationData { public String Title { get; set; } public String Message { get; set; } } public NotificationData Notification { get; private set; } public AjaxResponse_Data SetNotification(String title, String message) { Notification = new NotificationData() { Title = title, Message = message }; return this; } //--------------------------------------------------------------- // Paged data. public class PagingData { public PagingData(SaxModel_Base model) : this(model.Paging.PageNo, model.Paging.PageLength, model.Paging.ItemCount, model.Paging.ItemTotal) { } public PagingData(uint pageNo, ushort pageLength, ushort itemCount, ulong itemTotal) { PageNo = pageNo; PageLength = pageLength; ItemCount = itemCount; ItemTotal = itemTotal; } public bool Paged { get { return (PageNo > 0 && PageLength > 0 && ItemTotal > PageLength); } } public uint PageNo { get; } = 0; public ushort PageLength { get; } = 0; public uint PageCount { get { return (PageLength > 0 ? (uint)Math.Ceiling((decimal)ItemTotal / PageLength) : 0); } } public ushort ItemCount { get; } = 0; public ulong ItemTotal { get; } = 0; } public PagingData Paging { get; private set; } public void SetPaging(SaxModel_Base model) { Paging = new PagingData(model); } //--------------------------------------------------------------- } //------------------------------------------------------------------- } //======================================================================= }