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 Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.Bus; using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Helpers; using Sarand.Net.Standard.Base.Utils; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; namespace Sacs.Web.Components { //======================================================================= public class SaController : Controller { //------------------------------------------------------------------- /// /// Protected construction - use derived objects. /// protected SaController() { } protected override void Dispose(bool disposing) { if (disposing) { } base.Dispose(disposing); } //------------------------------------------------------------------- // Properties. public DateTime CreatedAt { get; } = DateTime.Now; public bool DebugMode { get; } = SacsSystem.DebugMode; /// /// 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(); } } //------------------------------------------------------------------- // Redirection locations. //------------------------------------------------------------------- public static readonly String ParamName_SessionExpired = "SessionExpired"; public enum RedirectLocation { WebAppBase, WebAppHome, WebAppHome_SessionExpired, } [NonAction] 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 Parameter 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($"{SysUtil.Fn}" + (ViewData.Model != null ? " Model: " + ViewData.Model : "")); base.OnActionExecuting(filterContext); } public override void OnActionExecuted(ActionExecutedContext filterContext) { //if (DebugMode) SaLogger.Debug($"{SysUtil.Fn}" + (ViewData.Model != null ? " Model: " + ViewData.Model : "")); base.OnActionExecuted(filterContext); } //------------------------------------------------------------------- public static String EncryptParameters(NameValueCollection parameters, String base64IV, String base64Key) { 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(SysUtil.Fn + "\n\tData: " + queryString + " \n\tEncrypted: " + encrypted); return encrypted; } catch (Exception exc) { SaLogger.Warn($"{SysUtil.Fn} {ExceptionUtil.GetErrorStateMessage(exc)}"); } return ""; } public static NameValueCollection DecryptParameters(String encrypted, String base64IV, String base64Key) { try { String queryString = SacsEncryptionHelper.Decrypt(encrypted, base64IV, base64Key); //if (SacsSystem.DebugMode) SaLogger.Debug(SysUtil.Fn + " \n\tEncrypted: " + encrypted + "\n\tData: " + queryString); return HttpUtility.ParseQueryString(queryString); } catch (Exception exc) { SaLogger.Warn(SysUtil.Fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); } return new NameValueCollection(); } //------------------------------------------------------------------- // Cookies. //------------------------------------------------------------------- [NonAction] /// Retrieve the value from the named request cookie. public String GetRequestCookie(String key) { if (Request.Cookies.ContainsKey(key)) return Request.Cookies[key]; return null; } [NonAction] /// 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); } //------------------------------------------------------------------- // Error State //------------------------------------------------------------------- private SacsErrorState errorState = null; public bool HasErrorState() { return errorState != null; } public SacsErrorState ErrorState { get { return errorState; } } public String ErrorStateMessage { get { return (errorState != null ? errorState.Message : String.Empty); } } /// /// Record the error state message. /// Blank message does not set the error state - leaves previous state intact. /// Return value is fale unless the message is blank. /// public bool SetErrorState(String message) { if (StringUtil.IsEmpty(message)) return true; this.errorState = new SacsErrorState(message); return false; } /// Clear the error state. public void ClearErrorState() { errorState = null; } //------------------------------------------------------------------- // 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.IsBlank(text)) Message = new MessageData() { Title = title, Text = text }; return this; } // Confirmation message - to query user. public class ConfirmMessageData : MessageData { public SaConfirmActions Actions { get; set; } } public ConfirmMessageData ConfirmMessage { get; private set; } public AjaxResponse_Base SetConfirmMessage(String title, String text, SaConfirmActions actions = SaConfirmActions.YesNo) { if (!StringUtil.IsBlank(text)) ConfirmMessage = new ConfirmMessageData() { Title = title, Text = text, Actions = actions }; return this; } public bool HasConfirmMessage { get { return ConfirmMessage != null; } } } //------------------------------------------------------------------- public class AjaxResponse_Data : AjaxResponse_Base { //--------------------------------------------------------------- // Construction. protected AjaxResponse_Data(SaModel model) { } //--------------------------------------------------------------- /// Item data - Common properties for data items. public class ItemData_Base { // Common item properties. public String Id { get; set; } = null; private String notes; public String Notes { get { return StringUtil.IsBlank(notes) ? null : notes; } set { notes = StringUtil.EmptyOnNull(value).Trim(); } } public String StatusCode { get; set; } = null; public String StatusDesc { get; set; } = null; // 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; } } /// Contact Method properties. public class ContactMethodData { public String Method { get; set; } public String MethodCaption { get; set; } public String Value { get; set; } } /// Address properties. public class AddressData : ItemData_Base { public String Line1 { get; set; } public String Line2 { get; set; } public String Line3 { get; set; } public String Town { get; set; } public String RegionIdProvince { get; set; } public String RegionNameProvince { get; set; } public String RegionIdCountry { get; set; } public String RegionNameCountry { get; set; } public String PostalCode { get; set; } } /// /// Item data with label and value. /// public class ItemLabelValue { public String Label { get; set; } public String Value { 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; } } /// Item code and desc values. public class ItemCodeDesc { public ItemCodeDesc() { } public ItemCodeDesc(String code, String desc) { this.Code = code; this.Desc = desc; } public String Code { get; set; } public String Desc { get; set; } } /// Statistics Caption/Value pair. public class StatData : AjaxResponse_Base { public String Caption { get; set; } public String Value { get; set; } } /// Statistics Group. public class StatsGroupData : AjaxResponse_Base { public String Caption { get; set; } public List Stats { get; } = new List(); } //--------------------------------------------------------------- // 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.IsBlank(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(SaModel 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(SaModel model) { Paging = new PagingData(model); } //--------------------------------------------------------------- } //------------------------------------------------------------------- } //======================================================================= }