501 lines
19 KiB
C#
501 lines
19 KiB
C#
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.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)
|
|
{
|
|
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 bool DebugMode { get; } = SacsSystem.DebugMode;
|
|
protected SaxApplication_Web App { get { return SaxApplication_Web.WebApp; } }
|
|
public SaxSessionConfig SessionConfig { get { return App.SessionConfig; } }
|
|
public SaxSessionContext SessionContext { get { return App.SessionContext; } }
|
|
protected SaxLocaliser Localiser { get; }
|
|
/// <summary>
|
|
/// Indicate whether session data should be stored by action.
|
|
/// <para>Any action that might modify the session data must store it.</para>
|
|
/// <para>Some actions such as scoring a password's complexity don't need to.</para>
|
|
/// </summary>
|
|
public virtual bool StoreSessionData { get; protected set; } = true;
|
|
// Culture.
|
|
private string LanguageIso2
|
|
{
|
|
get
|
|
{
|
|
var feature = HttpContext.Features.Get<IRequestCultureFeature>();
|
|
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 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<TypeCodeDesc> RegionTypes { get { return Internals.RegionTypes; } }
|
|
public List<TypeCodeDesc> UserRoles { get { return Internals.UserRoles; } }
|
|
public List<TypeCodeDesc> OrganisationTypes { get { return Internals.OrganisationTypes; } }
|
|
public List<TypeCodeDesc> OrganisationCategories { get { return Internals.OrganisationCategories; } }
|
|
public List<TypeCodeDesc> OrganisationRoles { get { return Internals.OrganisationRoles; } }
|
|
public List<TypeCodeDesc> OpportunityTypes { get { return Internals.OpportunityTypes; } }
|
|
public List<TypeCodeDesc> DataItemTypes { get { return Internals.DataItemTypes; } }
|
|
// Email.
|
|
public SaEmailConfig EmailConfig { get { return App.EmailConfig; } }
|
|
public SaxEmailConsole 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.
|
|
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_SessionConfig, SessionStorage.sessionDataEncoding.GetBytes(sessionDataJson));
|
|
}
|
|
}
|
|
else HttpContext.Session.Remove(SessionStorage.SessionKey_SessionConfig);
|
|
|
|
// 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.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.
|
|
//-------------------------------------------------------------------
|
|
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.
|
|
//-------------------------------------------------------------------
|
|
/// <summary>Retrieve the value from the named request cookie.</summary>
|
|
public String GetRequestCookie(String key)
|
|
{
|
|
if (Request.Cookies.ContainsKey(key))
|
|
return Request.Cookies[key];
|
|
return null;
|
|
}
|
|
/// <summary>Set the value against the named response cookie.</summary>
|
|
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, "", SaxSystem.ComputerName); } }
|
|
// Referrer.
|
|
public String Referer { get { return Request.Headers["Referer"].ToString(); } }
|
|
public bool ValidReferer { get { return !StringUtil.IsEmpty(Referer); } }
|
|
/// <summary>Store session data, eg. to start a new user session on sign-in.</summary>
|
|
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_SessionConfig { 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<MemberInfo> 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<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
|
|
{
|
|
return base.CreateProperties(type, MemberSerialization.Fields);
|
|
}
|
|
};
|
|
public JsonSerializerSettings SaxSessionDataJsonSerializerSettings = new JsonSerializerSettings
|
|
{
|
|
ContractResolver = new SaxSessionDataContractResolver()
|
|
};
|
|
}
|
|
private readonly SessionStorage sessionStorage = new SessionStorage();
|
|
protected JsonSerializerSettings SaxSessionDataJsonSerializerSettings { get { return sessionStorage.SaxSessionDataJsonSerializerSettings; } }
|
|
|
|
//-------------------------------------------------------------------
|
|
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_SessionConfig, 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);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
public class AjaxResponse_Base
|
|
{
|
|
// Response status.
|
|
public bool Success { get; set; } = false;
|
|
public bool Error { get; set; } = false;
|
|
public bool SessionExpired { get; protected set; } = false;
|
|
public bool GoHome { get; protected set; } = false;
|
|
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
|
|
{
|
|
// 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<String, FormControlValue> controlValues = new Dictionary<String, FormControlValue>();
|
|
public List<FormControlValue> ControlValues { get { return controlValues.Values.ToList(); } }
|
|
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));
|
|
}
|
|
}
|
|
/// <summary>Assign the error to the control by name and return this response data.</summary>
|
|
public AjaxResponse_Data SetControlError(String controlName, String error)
|
|
{
|
|
return SetControlError(controlName, error, this);
|
|
}
|
|
/// <summary>
|
|
/// Assign the error to the control by name and return the specified value.
|
|
/// <para>- Allows alternative return values to be specified (eg. 'false') to avoid separate return statement in calling routine.</para>
|
|
/// </summary>
|
|
public T SetControlError<T>(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 bool HasNotification { get { return Notification != null; } }
|
|
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.DataPageNo, model.DataPageLength, model.DataItemCount, model.DataItemTotal)
|
|
{ }
|
|
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);
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
}
|
|
//=======================================================================
|
|
}
|