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.BusinessMgr.Net.Core.Web.App; using Sarand.BusinessMgr.Net.Core.Web.Models; using Sarand.BusinessMgr.Net.Standard.Base.App; using Sarand.BusinessMgr.Net.Standard.Base.Bus; using Sarand.BusinessMgr.Net.Standard.Base.Consoles; using Sarand.BusinessMgr.Net.Standard.Base.Core; using Sarand.BusinessMgr.Net.Standard.Base.Mgr; using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.CodeTypes; using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Base.Diary; using Sarand.Net.Standard.Base.Email; using Sarand.Net.Standard.Base.Helpers; using Sarand.Net.Standard.Base.Interfaces; using Sarand.Net.Standard.Base.ItemData; using Sarand.Net.Standard.Base.ItemDocs; using Sarand.Net.Standard.Base.ItemOptions; using Sarand.Net.Standard.Base.ReferenceData; using Sarand.Net.Standard.Base.Security; using Sarand.Net.Standard.Base.UpdateLog; using Sarand.Net.Standard.Base.Utils; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Web; namespace Sarand.BusinessMgr.Net.Core.Web.Controllers { //======================================================================= public abstract class SbmController_Base : Controller { //------------------------------------------------------------------- // Data members. private SbmInternals internals; //------------------------------------------------------------------- // Protected construction. protected SbmController_Base(SbmLocaliser localiser) { Localiser = localiser; // Initialise console (debug) logging listener. //if (Directory.Exists(App.LoggingRootFolder) && SbmSystem.DebugMode) // SbmConsoleListener.ConfigureDebugListener(App.LoggingRootFolder); if (DebugMode) SaLogger.Trace(GetType().Name + ".CTR"); } protected override void Dispose(bool disposing) { if (disposing) { } base.Dispose(disposing); } //------------------------------------------------------------------- // Properties. public bool DebugMode { get; } = SacsSystem.DebugMode; protected SbmApplication_Web App { get { return SbmGlobals_Web.Application; } } public SbmAppConfig AppConfig { get { return App.AppConfig; } } public SbmSessionContext SessionContext { get { return SessionData?.SessionContext ?? null; } } public ISbmUserContext UserContext { get { return SessionData; } } protected SbmLocaliser 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 string LanguageIso2 { get { var feature = HttpContext.Features.Get(); return feature.RequestCulture.UICulture.TwoLetterISOLanguageName.ToLower(); } } // Business components. public SbmInternals Internals { get { if (internals == null) internals = new SbmInternals(UserContext); return internals; } } // Managers. public SbmAccountMgr AccountMgr { get { return Internals.AccountMgr; } } public SbmAssetMgr AssetMgr { get { return Internals.AssetMgr; } } public SbmCommodityMgr CommodityMgr { get { return Internals.CommodityMgr; } } public SbmPriceMgr PriceMgr { get { return Internals.PriceMgr; } } public SbmTransactionMgr TransactionMgr { get { return Internals.TransactionMgr; } } public SbmStockMgr StockMgr { get { return Internals.StockMgr; } } public SbmProcessMgr ProcessMgr { get { return Internals.ProcessMgr; } } public SbmDataItemMgr DataItemMgr { get { return Internals.DataItemMgr; } } // Consoles. public SbmConfigConsole ConfigConsole { get { return Internals.ConfigConsole; } } public SaDiaryConsole DiaryConsole { get { return Internals.DiaryConsole; } } public SaSecurityConsole SecurityConsole { get { return Internals.SecurityConsole; } } public SaCodeTypeConsole CodeTypeConsole { get { return Internals.CodeTypeConsole; } } public SaReferenceDataConsole ReferenceDataConsole { get { return Internals.ReferenceDataConsole; } } public SaUpdateLogConsole UpdateLogConsole { get { return Internals.UpdateLogConsole; } } public SaInterfaceConsole InterfaceConsole { get { return Internals.InterfaceConsole; } } public SaItemDocConsole ItemDocConsole { get { return Internals.ItemDocConsole; } } public SaItemOptionConsole ItemOptionConsole { get { return Internals.ItemOptionConsole; } } public SaItemDataConsole ItemDataConsole { get { return Internals.ItemDataConsole; } } // Configurations. public SbmStockControlConfig StockControlConfig { get { return Internals.StockControlConfig; } } public SbmRequirementCostingConfig RequirementCostingConfig { get { return Internals.RequirementCostingConfig; } } // System codes. public List GetCodesOfType(Sbm.SystemCode systemCode) { return Internals.GetCodesOfType(systemCode); } // Email. public SaEmailConfig EmailConfig { get { return Internals.EmailConfig; } } public SbmEmailConsole EmailConsole { get { return Internals.EmailConsole; } } //------------------------------------------------------------------- // Redirection locations. //------------------------------------------------------------------- public static readonly String ParamName_SessionExpired = "SessionExpired"; protected enum RedirectLocation { WebAppBase, WebAppHome, WebAppHome_SessionExpired, } protected 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. SbmModel_Base baseModel = ViewData.Model as SbmModel_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. SbmModel_Base baseModel = ViewData.Model as SbmModel_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(SbmModel_Base model) { if (model == null) return; // Session data. model.SessionData_Web = SessionData; // Redirect locations. model.RedirectLocation.Base = GetRedirectLocation(RedirectLocation.WebAppBase); model.RedirectLocation.Home = GetRedirectLocation(RedirectLocation.WebAppHome); model.RedirectLocation.Home_SessionExpired = GetRedirectLocation(RedirectLocation.WebAppHome_SessionExpired); } //------------------------------------------------------------------- // Parameters. //------------------------------------------------------------------- public String EncryptParameters(NameValueCollection parameters, String base64IV, String base64Key) { String fn = nameof(ControllerBase) + ".EncryptParameters"; try { // Copy the list of parameters, adding a random name/value pair to randomise the encryption. NameValueCollection parametersToEncrypt = new NameValueCollection(parameters); // NB Range 10-189 intended to have equal chance of 2 or 3 digit number. parametersToEncrypt.Add("!" + SacsRandomHelper.GetInt32(10, 190).ToString(), SacsRandomHelper.GetInt32(10, 190).ToString()); // Sort the keys to ensure that the random '!' param is first in the query string. 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. string queryString = String.Join("&", paramKeys.Select(a => a + "=" + HttpUtility.UrlEncode(parametersToEncrypt[a]))); String encrypted = SacsEncryptionHelper.Encrypt(queryString, base64IV, base64Key); if (DebugMode) { SaLogger.Debug(fn + " Encrypted: " + encrypted); NameValueCollection decryptedParams = DecryptParameters(encrypted, base64IV, base64Key); } return encrypted; } catch (Exception exc) { SaLogger.Warn(fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); } return ""; } public NameValueCollection DecryptParameters(String encrypted, String base64IV, String base64Key) { String fn = nameof(ControllerBase) + ".DecryptParameters"; if (DebugMode) SaLogger.Debug(fn + " \n\tEncrypted: " + encrypted + "\n\tIV: " + base64IV + "\n\tKey: " + base64Key); try { String queryString = SacsEncryptionHelper.Decrypt(encrypted, base64IV, base64Key); 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 { return SessionData?.ValidUserSession ?? false; } } public DbUpdateInfo DbUpdateInfo { get { return SessionData?.DbUpdateInfo ?? new DbUpdateInfo(null, "", SbmSystem.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(SbmSessionData_Web 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 SbmSessionData_Web SessionData { get; set; } public static String SessionKey_SessionData { get; } = "SbmSessionData"; public static Encoding sessionDataEncoding { get; } = Encoding.UTF8; //JSON Serialisation. public class SbmSessionDataContractResolver : 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 SbmSessionDataContractResolver(), // Force all deserialised objects to be replaced - to avoid proxy lists not populating. ObjectCreationHandling = ObjectCreationHandling.Replace }; } private readonly SessionStorage sessionStorage = new SessionStorage(); public static JsonSerializerSettings SbmSessionDataJsonSerializerSettings { get { return SessionStorage.JsonDeserialisationSettings; } } //------------------------------------------------------------------- public SbmSessionData_Web 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 SbmSessionData_Web 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); } //------------------------------------------------------------------- // Base Ajax Response Data. //------------------------------------------------------------------- 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; } } //------------------------------------------------------------------- // Ajax Response Data. //------------------------------------------------------------------- public class AjaxResponse_Data : AjaxResponse_Base { //--------------------------------------------------------------- // Construction. protected AjaxResponse_Data(SbmModel_Base model) { } //--------------------------------------------------------------- /// /// Common item response data properties. /// - Null values are ignored by the JSON serialiser. /// - Return null for false boolean values where possible. /// - Return null for blank string values where possible. /// public class ItemData { public String Id { get; set; } private String notes; public String Notes { get { return StringUtil.OnEmpty(notes, null); } set { notes = StringUtil.EmptyOnNull(value).Trim(); } } public String StatusCode { get; set; } public String StatusDesc { get; set; } 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? Deleted { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Deleted)) ? true : (bool?)null; } } public String CreatedOn { get; set; } public String CreatedOnFmt { get; set; } public String UpdatedOn { get; set; } public String UpdatedOnFmt { 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(SbmModel_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(SbmModel_Base model) { Paging = new PagingData(model); } //--------------------------------------------------------------- } //------------------------------------------------------------------- } //======================================================================= }