diff --git a/Disco.Services/Plugins/PluginManifest.cs b/Disco.Services/Plugins/PluginManifest.cs index b3b4dc37..4e4136c7 100644 --- a/Disco.Services/Plugins/PluginManifest.cs +++ b/Disco.Services/Plugins/PluginManifest.cs @@ -450,6 +450,7 @@ namespace Disco.Services.Plugins handler.Manifest = this; handler.HostController = HostController; + handler.Url = HostController.Url; return handler; } diff --git a/Disco.Services/Plugins/PluginWebHandler.cs b/Disco.Services/Plugins/PluginWebHandler.cs index 0db0146a..127477b7 100644 --- a/Disco.Services/Plugins/PluginWebHandler.cs +++ b/Disco.Services/Plugins/PluginWebHandler.cs @@ -16,6 +16,8 @@ namespace Disco.Services.Plugins { public PluginManifest Manifest { get; set; } public Controller HostController { get; set; } + public UrlHelper Url { get; set; } + protected DiscoDataContext Database; private Lazy plugin; protected WebHelper Plugin @@ -29,10 +31,7 @@ namespace Disco.Services.Plugins public PluginWebHandler() { plugin = new Lazy(new Func(() => new WebHelper(HostController.HttpContext, Manifest))); - } - public void OnActionExecuting() - { Database = new DiscoDataContext(); Database.Configuration.LazyLoadingEnabled = false; } @@ -169,6 +168,19 @@ namespace Disco.Services.Plugins } #endregion + #region Status Codes + + public ActionResult Ok() + { + return new HttpStatusCodeResult(System.Net.HttpStatusCode.OK); + } + public ActionResult Ok(string description) + { + return new HttpStatusCodeResult(System.Net.HttpStatusCode.OK, description); + } + + #endregion + #region Content public ActionResult Content(string content, string contentType, Encoding contentEncoding) { diff --git a/Disco.Services/Plugins/PluginWebHandlerController.cs b/Disco.Services/Plugins/PluginWebHandlerController.cs index e06d7fd3..c973489e 100644 --- a/Disco.Services/Plugins/PluginWebHandlerController.cs +++ b/Disco.Services/Plugins/PluginWebHandlerController.cs @@ -9,115 +9,332 @@ namespace Disco.Services.Plugins { public abstract class PluginWebHandlerController : PluginWebHandler { + public ControllerContext ControllerContext => HostController.ControllerContext; - public override ActionResult ExecuteAction(string ActionName) + public void OnAuthorization(AuthorizationContext authorizationContext, string actionName) + { + var actionDescriptor = (PluginActionDescriptor)authorizationContext.ActionDescriptor; + + var authorizationFilters = actionDescriptor.GetAuthorizationFilters; + foreach (var filter in authorizationFilters) + { + filter.OnAuthorization(authorizationContext); + if (authorizationContext.Result != null) + return; + } + } + + public override ActionResult ExecuteAction(string actionName) { var handlerType = GetType(); - var methodDescriptor = FindControllerMethod(Manifest, handlerType, ActionName); + var methodDescriptor = FindControllerMethod(ControllerContext, Manifest, handlerType, actionName); if (methodDescriptor == null) - return HttpNotFound("Unknown Plugin Method"); - - // Authorize Method - if (methodDescriptor.Authorizers.Length > 0) + return HttpNotFound("No such plugin method"); + + var authorizationContext = new AuthorizationContext(ControllerContext, methodDescriptor); + OnAuthorization(authorizationContext, actionName); + if (authorizationContext.Result != null) + return authorizationContext.Result; + + return methodDescriptor.Execute(this, ControllerContext); + } + + private static PluginActionDescriptor FindControllerMethod(ControllerContext context, PluginManifest manifest, Type handler, string actionName) + { + var descriptors = GetWebHandler(manifest, handler); + return (PluginActionDescriptor)descriptors.FindAction(context, actionName); + } + + #region Method Cache + private static readonly Dictionary pluginControllerCache = new Dictionary(); + private static PluginControllerDescription GetWebHandler(PluginManifest manifest, Type handler) + { + if (!pluginControllerCache.TryGetValue(handler, out var result)) { - var attributeDenied = methodDescriptor.Authorizers.FirstOrDefault(a => !a.IsAuthorized(HostController.HttpContext)); - if (attributeDenied != null) - { - var message = attributeDenied.HandleUnauthorizedMessage(); - var resource = string.Format("{0} [{1}]", attributeDenied.AuthorizeResource, HostController.Request.RawUrl); - - if (CurrentUser != null) - AuthorizationLog.LogAccessDenied(CurrentUser.UserId, resource, message); - - return new HttpUnauthorizedResult(); - } + // Cache Miss + result = new PluginControllerDescription(manifest, handler); + pluginControllerCache[handler] = result; } - var methodParams = BuildMethodParameters(Manifest, handlerType, methodDescriptor.MethodInfo, ActionName, HostController); - - return (ActionResult)methodDescriptor.MethodInfo.Invoke(this, methodParams); + return result; } - private static WebHandlerCachedItem FindControllerMethod(PluginManifest Manifest, Type Handler, string ActionName) + private class PluginParameterDescriptor : ParameterDescriptor { - var descriptors = CacheWebHandler(Manifest, Handler); - WebHandlerCachedItem method; - if (descriptors.TryGetValue(ActionName.ToLower(), out method)) - return method; // Not Found - else - return null; // Not Found - } - private static object[] BuildMethodParameters(PluginManifest Manifest, Type Handler, MethodInfo methodInfo, string ActionName, Controller HostController) - { - var methodParams = methodInfo.GetParameters(); - var result = new object[methodParams.Length]; + private static readonly object[] emptyObjectArray = new object[0]; - for (int i = 0; i < methodParams.Length; i++) + private readonly PluginActionDescriptor actionDescriptor; + public override ActionDescriptor ActionDescriptor => actionDescriptor; + public override string ParameterName { get; } + public override Type ParameterType { get; } + public override ParameterBindingInfo BindingInfo { get; } + public override object DefaultValue { get; } + + public PluginParameterDescriptor(PluginActionDescriptor actionDescriptor, string parameterName, Type parameterType, IModelBinder modelBinder, object defaultValue) { - var methodParam = methodParams[i]; + this.actionDescriptor = actionDescriptor; + ParameterName = parameterName; + ParameterType = parameterType; + DefaultValue = defaultValue; + BindingInfo = new PluginParameterBindingInfo(modelBinder); + } - Type parameterType = methodParam.ParameterType; - IModelBinder modelBinder = ModelBinders.Binders.GetBinder(parameterType); - IValueProvider valueProvider = HostController.ValueProvider; - string parameterName = methodParam.Name; + public object BindParameter(ControllerContext controllerContext) + { + IValueProvider valueProvider = controllerContext.Controller.ValueProvider; ModelBindingContext bindingContext = new ModelBindingContext() { FallbackToEmptyPrefix = true, - ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, parameterType), - ModelName = parameterName, - ModelState = HostController.ViewData.ModelState, + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, ParameterType), + ModelName = ParameterName, + ModelState = controllerContext.Controller.ViewData.ModelState, PropertyFilter = (p) => true, ValueProvider = valueProvider }; - var parameterValue = modelBinder.BindModel(HostController.ControllerContext, bindingContext); + var parameterValue = BindingInfo.Binder.BindModel(controllerContext.Controller.ControllerContext, bindingContext); - if (parameterValue == null && methodParam.HasDefaultValue) - parameterValue = methodParam.DefaultValue; + if (parameterValue == null) + parameterValue = DefaultValue; - result[i] = parameterValue; + return parameterValue; } - return result; + public override object[] GetCustomAttributes(bool inherit) + { + return emptyObjectArray; + } + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return emptyObjectArray; + } + public override bool IsDefined(Type attributeType, bool inherit) + { + return false; + } + + private class PluginParameterBindingInfo : ParameterBindingInfo + { + private static readonly string[] emptyStringArray = new string[0]; + public override IModelBinder Binder { get; } + public override ICollection Exclude { get; } = emptyStringArray; + public override ICollection Include { get; } = emptyStringArray; + + public PluginParameterBindingInfo(IModelBinder modelBinder) + { + Binder = modelBinder; + } + } } - #region Method Cache - private static Dictionary> WebHandlerCachedItems = new Dictionary>(); - private static Dictionary CacheWebHandler(PluginManifest Manifest, Type Handler) + private class PluginActionDescriptor : ActionDescriptor { - Dictionary result; + private readonly PluginControllerDescription controllerDescription; + private readonly MethodInfo methodInfo; + private readonly IAuthorizationFilter[] authorizationFilters; + private readonly PluginParameterDescriptor[] parameterDescriptors; + public override string UniqueId { get; } + public override string ActionName { get; } + public override ControllerDescriptor ControllerDescriptor => controllerDescription; - if (!WebHandlerCachedItems.TryGetValue(Handler, out result)) + public PluginActionDescriptor(PluginControllerDescription controllerDescription, string methodName, MethodInfo methodInfo) { - // Cache Miss - result = new Dictionary(); - var methods = Array.FindAll(Handler.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), mi => { return !mi.IsSpecialName && typeof(ActionResult).IsAssignableFrom(mi.ReturnType); }); + this.controllerDescription = controllerDescription; + this.methodInfo = methodInfo; + UniqueId = $"{ControllerDescriptor.UniqueId}_{methodName}"; + ActionName = methodName; + authorizationFilters = DiscoverAuthorizationFilters(); + parameterDescriptors = DiscoverParameters(); + } + + private IAuthorizationFilter[] DiscoverAuthorizationFilters() + { + var filters = methodInfo.GetCustomAttributes(true).OfType().ToList(); + + foreach (var authorizer in filters.OfType()) + authorizer.AuthorizeResource = string.Format("[Plugin]::{0}::{1}", controllerDescription.Manifest.Id, methodInfo.Name); + + return filters.ToArray(); + } + + private PluginParameterDescriptor[] DiscoverParameters() + { + var methodParams = methodInfo.GetParameters(); + var result = new List(methodParams.Length); + + for (int i = 0; i < methodParams.Length; i++) + { + var methodParam = methodParams[i]; + + IModelBinder modelBinder = ModelBinders.Binders.GetBinder(methodParam.ParameterType); + + var defaultValue = (object)null; + if (methodParam.DefaultValue != DBNull.Value) + defaultValue = methodParam.DefaultValue; + + result.Add(new PluginParameterDescriptor(this, methodParam.Name, methodParam.ParameterType, modelBinder, defaultValue)); + } + + return result.ToArray(); + } + + public override object Execute(ControllerContext controllerContext, IDictionary parameters) + { + throw new NotSupportedException(); + } + + public ActionResult Execute(PluginWebHandlerController pluginController, ControllerContext controllerContext) + { + var methodParameters = BuildMethodParameters(methodInfo, controllerContext.Controller); + + return (ActionResult)methodInfo.Invoke(pluginController, methodParameters); + } + + private static object[] BuildMethodParameters(MethodInfo methodInfo, ControllerBase HostController) + { + var methodParams = methodInfo.GetParameters(); + var result = new object[methodParams.Length]; + + for (int i = 0; i < methodParams.Length; i++) + { + var methodParam = methodParams[i]; + + Type parameterType = methodParam.ParameterType; + IModelBinder modelBinder = ModelBinders.Binders.GetBinder(parameterType); + IValueProvider valueProvider = HostController.ValueProvider; + string parameterName = methodParam.Name; + + ModelBindingContext bindingContext = new ModelBindingContext() + { + FallbackToEmptyPrefix = true, + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, parameterType), + ModelName = parameterName, + ModelState = HostController.ViewData.ModelState, + PropertyFilter = (p) => true, + ValueProvider = valueProvider + }; + + var parameterValue = modelBinder.BindModel(HostController.ControllerContext, bindingContext); + + if (parameterValue == null && methodParam.HasDefaultValue) + parameterValue = methodParam.DefaultValue; + + result[i] = parameterValue; + } + + return result; + } + + private object[] BuildMethodParameters(ControllerContext controllerContext) + { + var parameters = new object[parameterDescriptors.Length]; + for (int i = 0; i < parameterDescriptors.Length; i++) + { + parameters[i] = parameterDescriptors[i].BindParameter(controllerContext); + } + return parameters; + } + + public override ParameterDescriptor[] GetParameters() + { + return parameterDescriptors; + } + + public IEnumerable GetAuthorizationFilters => authorizationFilters; + + public override object[] GetCustomAttributes(bool inherit) + { + return authorizationFilters; + } + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + var result = new List(authorizationFilters.Length); + foreach (var filter in authorizationFilters) + { + if (attributeType.IsAssignableFrom(filter.GetType())) + result.Add(filter); + } + return result.ToArray(); + } + public override bool IsDefined(Type attributeType, bool inherit) + { + foreach (var filter in authorizationFilters) + { + if (attributeType.IsAssignableFrom(filter.GetType())) + { + return true; + } + } + return false; + } + + public override IEnumerable GetFilterAttributes(bool useCache) + { + return authorizationFilters.OfType(); + } + } + + private class PluginControllerDescription : ControllerDescriptor + { + private static readonly object[] emptyObjectArray = new object[0]; + private readonly Dictionary actions; + public override string ControllerName { get; } + + public PluginManifest Manifest { get; } + public override Type ControllerType { get; } + public override string UniqueId { get; } + + public PluginControllerDescription(PluginManifest manifest, Type controllerType) + { + Manifest = manifest; + ControllerType = controllerType; + actions = DiscoverActions(); + ControllerName = controllerType.Name; + UniqueId = $"DiscoPlugin_{manifest.Id}_{controllerType.Name}"; + } + + private Dictionary DiscoverActions() + { + var actions = new Dictionary(StringComparer.OrdinalIgnoreCase); + var methods = Array.FindAll(ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), mi => { return !mi.IsSpecialName && typeof(ActionResult).IsAssignableFrom(mi.ReturnType); }); foreach (var method in methods) { - var authorizers = method.GetCustomAttributes().ToArray(); - foreach (var authorizer in authorizers) - authorizer.AuthorizeResource = string.Format("[Plugin]::{0}::{1}", Manifest.Id, method.Name); - - var item = new WebHandlerCachedItem() - { - Method = method.Name, - MethodInfo = method, - Authorizers = authorizers - }; - result.Add(item.Method.ToLower(), item); + actions.Add(method.Name, new PluginActionDescriptor(this, method.Name, method)); } - WebHandlerCachedItems[Handler] = result; + return actions; } - return result; - } - private class WebHandlerCachedItem - { - public string Method { get; set; } - public MethodInfo MethodInfo { get; set; } - public DiscoAuthorizeBaseAttribute[] Authorizers { get; set; } + public override ActionDescriptor FindAction(ControllerContext controllerContext, string actionName) + { + if (actions.TryGetValue(actionName, out var action)) + return action; + else + return null; + } + + public override ActionDescriptor[] GetCanonicalActions() + { + return actions.Values.ToArray(); + } + + public override object[] GetCustomAttributes(bool inherit) + { + return emptyObjectArray; + } + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return emptyObjectArray; + } + public override IEnumerable GetFilterAttributes(bool useCache) + { + return Enumerable.Empty(); + } + public override bool IsDefined(Type attributeType, bool inherit) + { + return false; + } } #endregion } diff --git a/Disco.Services/Web/Bundles/BundleExtensions.cs b/Disco.Services/Web/Bundles/BundleExtensions.cs index 3044a642..35cc9240 100644 --- a/Disco.Services/Web/Bundles/BundleExtensions.cs +++ b/Disco.Services/Web/Bundles/BundleExtensions.cs @@ -8,20 +8,24 @@ namespace Disco.Services.Web { public static class BundleExtensions { - public static void BundleDeferred(this HtmlHelper htmlHelper, string BundleUrl) + public static void BundleDeferred(this HttpContextBase httpContext, string BundleUrl) { // Ensure 'App-Relative' Url: BundleUrl = BundleUrl.StartsWith("~/") ? BundleUrl : (BundleUrl.StartsWith("/") ? string.Concat("~", BundleUrl) : string.Concat("~/", BundleUrl)); - var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] as List; + var deferredBundles = httpContext.Items[BundleTable.DeferredKey] as List; if (deferredBundles == null) { deferredBundles = new List(); - htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] = deferredBundles; + httpContext.Items[BundleTable.DeferredKey] = deferredBundles; } if (!deferredBundles.Contains(BundleUrl)) deferredBundles.Add(BundleUrl); } + + public static void BundleDeferred(this HtmlHelper htmlHelper, string BundleUrl) + => htmlHelper.ViewContext.HttpContext.BundleDeferred(BundleUrl); + public static HtmlString BundleRenderDeferred(this HtmlHelper htmlHelper) { var deferredBundles = htmlHelper.ViewContext.HttpContext.Items[BundleTable.DeferredKey] as List; diff --git a/Disco.Web/Areas/Config/Controllers/PluginsController.cs b/Disco.Web/Areas/Config/Controllers/PluginsController.cs index 77fc9638..7de21f9f 100644 --- a/Disco.Web/Areas/Config/Controllers/PluginsController.cs +++ b/Disco.Web/Areas/Config/Controllers/PluginsController.cs @@ -23,7 +23,7 @@ namespace Disco.Web.Areas.Config.Controllers } #region Plugin Configuration - [DiscoAuthorize(Claims.Config.Plugin.Configure), HttpPost] + [DiscoAuthorize(Claims.Config.Plugin.Configure), HttpPost, ValidateInput(false)] public virtual ActionResult Configure(string PluginId, FormCollection form) { if (string.IsNullOrEmpty(PluginId)) diff --git a/Disco.Web/ClientSource/Scripts/Modules/Disco-ExpressionEditor.js b/Disco.Web/ClientSource/Scripts/Modules/Disco-ExpressionEditor.js index f748c097..2c7e7bde 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/Disco-ExpressionEditor.js +++ b/Disco.Web/ClientSource/Scripts/Modules/Disco-ExpressionEditor.js @@ -1,4 +1,4 @@ -/// +/// function DiscoExpressionEditor(host, validateUrl, expression) { this.host = host; this.hostDocument = null; diff --git a/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.js b/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.js index db5a4a19..3fc62809 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.js +++ b/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.js @@ -2,63 +2,71 @@ if (!document.DiscoFunctions) { document.DiscoFunctions = {}; } if (!document.DiscoFunctions.PropertyChangeHelper) { - document.DiscoFunctions.PropertyValue = function (PropertyField) { - if (PropertyField[0].nodeName.toLowerCase() == 'input' && PropertyField.attr('type') == 'checkbox') { - return PropertyField.is(':checked'); + document.DiscoFunctions.PropertyValue = function (propertyField) { + if (propertyField[0].nodeName.toLowerCase() == 'input' && propertyField.attr('type') == 'checkbox') { + return propertyField.is(':checked'); } - return PropertyField.val(); + return propertyField.val(); }; - document.DiscoFunctions.PropertyChangeHelper = function (PropertyField, FieldWatermark, UpdateUrl, UpdatePropertyName) { - var fieldValue = document.DiscoFunctions.PropertyValue(PropertyField); + document.DiscoFunctions.PropertyChangeHelper = function (propertyField, fieldWatermark, updateUrl, updatePropertyName, data) { + var fieldValue = document.DiscoFunctions.PropertyValue(propertyField); var fieldChangeToken = null; - var $ajaxSave = PropertyField.nextAll('.ajaxSave').first(); - var $ajaxLoading = PropertyField.nextAll('.ajaxLoading').first(); + var $ajaxSave = propertyField.nextAll('.ajaxSave').first(); + var $ajaxLoading = propertyField.nextAll('.ajaxLoading').first(); var fieldChangeFunction = function () { $ajaxSave.hide(); - var changedValue = document.DiscoFunctions.PropertyValue(PropertyField); + var changedValue = document.DiscoFunctions.PropertyValue(propertyField); if (fieldValue != changedValue) { fieldValue = changedValue; if (fieldChangeToken) window.clearTimeout(fieldChangeToken); fieldChangeToken = window.setTimeout(function () { $ajaxLoading.show(); - var data = {}; - data[UpdatePropertyName] = fieldValue; - $.getJSON(UpdateUrl, data, function (response, result) { + if (!data) { + data = {}; + } + data[updatePropertyName] = fieldValue; + + $.getJSON(updateUrl, data, function (response, result) { if (result != 'success' || response != 'OK') { - alert('Unable to change property "' + UpdatePropertyName + '":\n' + response); + alert('Unable to change property "' + updatePropertyName + '":\n' + response); $ajaxLoading.hide(); } else { $ajaxLoading.hide().next('.ajaxOk').show().delay('fast').fadeOut('slow'); } + }).fail(function (jqXHR, textStatus, errorThrown) { + alert('Unable to change property "' + updatePropertyName + '":\n' + errorThrown); + $ajaxLoading.hide(); }) fieldChangeToken = null; }, 500); }; } - if (PropertyField[0].nodeName.toLowerCase() == 'input' && PropertyField.attr('type') == 'checkbox') { - PropertyField.click(fieldChangeFunction); + if (propertyField[0].nodeName.toLowerCase() == 'input' && propertyField.attr('type') == 'checkbox') { + propertyField.click(fieldChangeFunction); } else { - PropertyField.change(fieldChangeFunction); + propertyField.change(fieldChangeFunction); } // For Input Text Boxes - if (PropertyField[0].nodeName.toLowerCase() == 'input' && PropertyField.attr('type') == 'text') { - PropertyField.keydown(function (e) { + if (propertyField[0].nodeName.toLowerCase() == 'input' && propertyField.attr('type') == 'text') { + propertyField.keydown(function (e) { $ajaxSave.show(); if (e.which == 13) { $(this).blur(); } }) - .watermark(FieldWatermark) .blur(function () { $ajaxSave.hide(); }).focus(function () { $(this).select(); }); + if (fieldWatermark) { + propertyField.watermark(fieldWatermark); + } } // For TextAreas - if (PropertyField[0].nodeName.toLowerCase() == 'textarea') { - PropertyField.keydown(function () { + if (propertyField[0].nodeName.toLowerCase() == 'textarea') { + propertyField.keydown(function () { $ajaxSave.show(); }).blur(function () { $ajaxSave.hide(); diff --git a/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.min.js b/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.min.js index 2d4484bd..37f1c4b5 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.min.js +++ b/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers.min.js @@ -1 +1 @@ -if(document.DiscoFunctions||(document.DiscoFunctions={}),document.DiscoFunctions.PropertyChangeHelper||(document.DiscoFunctions.PropertyValue=function(n){return n[0].nodeName.toLowerCase()=="input"&&n.attr("type")=="checkbox"?n.is(":checked"):n.val()},document.DiscoFunctions.PropertyChangeHelper=function(n,t,i,r){var e=document.DiscoFunctions.PropertyValue(n),f=null,u=n.nextAll(".ajaxSave").first(),o=n.nextAll(".ajaxLoading").first(),s=function(){u.hide();var t=document.DiscoFunctions.PropertyValue(n);e!=t&&(e=t,f&&window.clearTimeout(f),f=window.setTimeout(function(){o.show();var n={};n[r]=e;$.getJSON(i,n,function(n,t){t!="success"||n!="OK"?(alert('Unable to change property "'+r+'":\n'+n),o.hide()):o.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow")});f=null},500))};n[0].nodeName.toLowerCase()=="input"&&n.attr("type")=="checkbox"?n.click(s):n.change(s);n[0].nodeName.toLowerCase()=="input"&&n.attr("type")=="text"&&n.keydown(function(n){u.show();n.which==13&&$(this).blur()}).watermark(t).blur(function(){u.hide()}).focus(function(){$(this).select()});n[0].nodeName.toLowerCase()=="textarea"&&n.keydown(function(){u.show()}).blur(function(){u.hide()})}),document.DiscoFunctions.DateChangeUserHelper||(document.DiscoFunctions.DateChangeUserHelper=function(n,t,i,r,u,f,e){var s=n.val(),o=null,h=t.next(".ajaxLoading");n.watermark(i).change(function(){var i=n.val();s.toLowerCase()!=i.toLowerCase()&&(s=i,o&&window.clearTimeout(o),o=window.setTimeout(function(){h.show();var n={};n[u]=s;$.getJSON(r,n,function(n,i){i!="success"||n.Result!="OK"?(alert("Unable to change Date:\n"+n),h.hide()):(t.text("by "+n.UserDescription),h.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow"))});o=null},500))}).focus(function(){$(this).select()});e?n.datepicker({defaultDate:new Date,minDate:moment(f).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datepicker("setDate",new Date)}}):n.datetimepicker({defaultDate:new Date,ampm:!0,minDate:moment(f).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datetimepicker("setDate",new Date)}})}),document.DiscoFunctions.DateChangeHelper||(document.DiscoFunctions.DateChangeHelper=function(n,t,i,r,u,f){var o=n.val(),e=null,s=n.next(".ajaxLoading");n.watermark(t).change(function(){var t=n.val();o.toLowerCase()!=t.toLowerCase()&&(o=t,e&&window.clearTimeout(e),e=window.setTimeout(function(){s.show();var n={};n[r]=o;$.getJSON(i,n,function(n,t){t!="success"||n!="OK"?(alert("Unable to change Date:\n"+n),s.hide()):s.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow")});e=null},500))}).focus(function(){$(this).select()});f?n.datepicker({defaultDate:new Date,minDate:moment(u).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datepicker("setDate",new Date)}}):n.datetimepicker({defaultDate:new Date,ampm:!0,minDate:moment(u).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datetimepicker("setDate",new Date)}})}),!document.DiscoFunctions.DateDialogCreateUpdater){var dialog,dialogForm,dialogHeader,dialogDateBox,dialogDatePropertyNameBox,updateUrl,friendlyName,dateField,userField,updatePropertyName,notSetDisplay,minDate,useAjax;function dateDialogGet(){if(!dialog){dialog=$("
").attr({"class":"dialog"});dialogForm=$("
").attr({action:"/",method:"post"}).appendTo(dialog);var n=$("

").appendTo(dialogForm);dialogHeader=$("

").attr("autofocus","autofocus").appendTo(n);dialogDatePropertyNameBox=$("").attr({type:"hidden",name:"key"}).appendTo(n);dialogDateBox=$("").attr({type:"datetime",name:"value"}).css({display:"block","margin-top":15,"margin-left":"auto","margin-right":"auto"}).appendTo(n);$("").attr({type:"hidden",name:"redirect"}).val("true").appendTo(n);dialog.dialog({resizable:!1,modal:!0,autoOpen:!1,buttons:{Update:dateDialogUpdate,Cancel:function(){$(this).dialog("close")}},open:function(){dialog.dialog("widget").find(".ui-dialog-buttonpane :tabbable:first").focus()}});dialogDateBox.datetimepicker({defaultDate:new Date,ampm:!0,changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd"})}return dialog}function dateDialogUpdate(){var u=dialogDateBox.val(),t,n,i,r;useAjax?(t=$("#"+dateField),userField&&(n=$("#"+userField)),dialog.dialog("close"),i=(n?n.next(".ajaxLoading"):t.next(".ajaxLoading")).show(),r={key:updatePropertyName,value:u},$.getJSON(updateUrl,r,function(r,u){u!="success"||r.Result!="OK"?(alert("Unable to change "+friendlyName+" Date:\n"+r),i.hide()):(r.DateTimeFull?t.attr("data-isodate",r.DateTimeISO8601).attr("data-livestamp",r.DateTimeUnixEpoc).attr("title",r.DateTimeFull).text(r.DateTimeFriendly):t.attr("data-isodate","").attr("data-livestamp","-1").attr("title",notSetDisplay).text(notSetDisplay),n&&n.text("by "+r.UserDescription),i.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow"))})):(dialog.dialog("disable"),dialog.dialog("option","buttons",null),dialogDatePropertyNameBox.val(updatePropertyName),dialogForm.attr("action",updateUrl),dialogForm.submit())}function dateDialogOpen(n,t,i,r,u,f,e,o){var s,h;updateUrl=n;friendlyName=t;dateField=i;userField=r;updatePropertyName=u;notSetDisplay=f;minDate=e;useAjax=o;s=dateDialogGet();s.dialog("option","title",friendlyName);dialogHeader.text(friendlyName+" Date");h=$("#"+i).attr("data-isodate");h?dialogDateBox.datetimepicker("setDate",new Date(h)):dialogDateBox.datetimepicker("setDate",new Date);e?dialogDateBox.datetimepicker("option","minDate",moment(minDate).toDate()):dialogDateBox.datetimepicker("option","minDate",null);s.dialog("open")}function dateDialogCreateUpdater(n,t,i,r,u,f,e,o){$("").attr({href:"#","class":"button small",style:"margin-right: 5px;"}).text("Update").click(function(s){s.preventDefault();dateDialogOpen(n,t,i,r,u,f,e,o)}).insertBefore("#"+i)}document.DiscoFunctions.DateDialogCreateUpdater=dateDialogCreateUpdater} \ No newline at end of file +if(document.DiscoFunctions||(document.DiscoFunctions={}),document.DiscoFunctions.PropertyChangeHelper||(document.DiscoFunctions.PropertyValue=function(n){return n[0].nodeName.toLowerCase()=="input"&&n.attr("type")=="checkbox"?n.is(":checked"):n.val()},document.DiscoFunctions.PropertyChangeHelper=function(n,t,i,r,u){var s=document.DiscoFunctions.PropertyValue(n),e=null,f=n.nextAll(".ajaxSave").first(),o=n.nextAll(".ajaxLoading").first(),h=function(){f.hide();var t=document.DiscoFunctions.PropertyValue(n);s!=t&&(s=t,e&&window.clearTimeout(e),e=window.setTimeout(function(){o.show();u||(u={});u[r]=s;$.getJSON(i,u,function(n,t){t!="success"||n!="OK"?(alert('Unable to change property "'+r+'":\n'+n),o.hide()):o.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow")}).fail(function(n,t,i){alert('Unable to change property "'+r+'":\n'+i);o.hide()});e=null},500))};n[0].nodeName.toLowerCase()=="input"&&n.attr("type")=="checkbox"?n.click(h):n.change(h);n[0].nodeName.toLowerCase()=="input"&&n.attr("type")=="text"&&(n.keydown(function(n){f.show();n.which==13&&$(this).blur()}).blur(function(){f.hide()}).focus(function(){$(this).select()}),t&&n.watermark(t));n[0].nodeName.toLowerCase()=="textarea"&&n.keydown(function(){f.show()}).blur(function(){f.hide()})}),document.DiscoFunctions.DateChangeUserHelper||(document.DiscoFunctions.DateChangeUserHelper=function(n,t,i,r,u,f,e){var s=n.val(),o=null,h=t.next(".ajaxLoading");n.watermark(i).change(function(){var i=n.val();s.toLowerCase()!=i.toLowerCase()&&(s=i,o&&window.clearTimeout(o),o=window.setTimeout(function(){h.show();var n={};n[u]=s;$.getJSON(r,n,function(n,i){i!="success"||n.Result!="OK"?(alert("Unable to change Date:\n"+n),h.hide()):(t.text("by "+n.UserDescription),h.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow"))});o=null},500))}).focus(function(){$(this).select()});e?n.datepicker({defaultDate:new Date,minDate:moment(f).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datepicker("setDate",new Date)}}):n.datetimepicker({defaultDate:new Date,ampm:!0,minDate:moment(f).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datetimepicker("setDate",new Date)}})}),document.DiscoFunctions.DateChangeHelper||(document.DiscoFunctions.DateChangeHelper=function(n,t,i,r,u,f){var o=n.val(),e=null,s=n.next(".ajaxLoading");n.watermark(t).change(function(){var t=n.val();o.toLowerCase()!=t.toLowerCase()&&(o=t,e&&window.clearTimeout(e),e=window.setTimeout(function(){s.show();var n={};n[r]=o;$.getJSON(i,n,function(n,t){t!="success"||n!="OK"?(alert("Unable to change Date:\n"+n),s.hide()):s.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow")});e=null},500))}).focus(function(){$(this).select()});f?n.datepicker({defaultDate:new Date,minDate:moment(u).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datepicker("setDate",new Date)}}):n.datetimepicker({defaultDate:new Date,ampm:!0,minDate:moment(u).toDate(),changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd",beforeShow:function(n){$input=$(n);$input.val()||$input.datetimepicker("setDate",new Date)}})}),!document.DiscoFunctions.DateDialogCreateUpdater){var dialog,dialogForm,dialogHeader,dialogDateBox,dialogDatePropertyNameBox,updateUrl,friendlyName,dateField,userField,updatePropertyName,notSetDisplay,minDate,useAjax;function dateDialogGet(){if(!dialog){dialog=$("
").attr({"class":"dialog"});dialogForm=$("").attr({action:"/",method:"post"}).appendTo(dialog);var n=$("

").appendTo(dialogForm);dialogHeader=$("

").attr("autofocus","autofocus").appendTo(n);dialogDatePropertyNameBox=$("").attr({type:"hidden",name:"key"}).appendTo(n);dialogDateBox=$("").attr({type:"datetime",name:"value"}).css({display:"block","margin-top":15,"margin-left":"auto","margin-right":"auto"}).appendTo(n);$("").attr({type:"hidden",name:"redirect"}).val("true").appendTo(n);dialog.dialog({resizable:!1,modal:!0,autoOpen:!1,buttons:{Update:dateDialogUpdate,Cancel:function(){$(this).dialog("close")}},open:function(){dialog.dialog("widget").find(".ui-dialog-buttonpane :tabbable:first").focus()}});dialogDateBox.datetimepicker({defaultDate:new Date,ampm:!0,changeYear:!0,changeMonth:!0,dateFormat:"yy/mm/dd"})}return dialog}function dateDialogUpdate(){var u=dialogDateBox.val(),t,n,i,r;useAjax?(t=$("#"+dateField),userField&&(n=$("#"+userField)),dialog.dialog("close"),i=(n?n.next(".ajaxLoading"):t.next(".ajaxLoading")).show(),r={key:updatePropertyName,value:u},$.getJSON(updateUrl,r,function(r,u){u!="success"||r.Result!="OK"?(alert("Unable to change "+friendlyName+" Date:\n"+r),i.hide()):(r.DateTimeFull?t.attr("data-isodate",r.DateTimeISO8601).attr("data-livestamp",r.DateTimeUnixEpoc).attr("title",r.DateTimeFull).text(r.DateTimeFriendly):t.attr("data-isodate","").attr("data-livestamp","-1").attr("title",notSetDisplay).text(notSetDisplay),n&&n.text("by "+r.UserDescription),i.hide().next(".ajaxOk").show().delay("fast").fadeOut("slow"))})):(dialog.dialog("disable"),dialog.dialog("option","buttons",null),dialogDatePropertyNameBox.val(updatePropertyName),dialogForm.attr("action",updateUrl),dialogForm.submit())}function dateDialogOpen(n,t,i,r,u,f,e,o){var s,h;updateUrl=n;friendlyName=t;dateField=i;userField=r;updatePropertyName=u;notSetDisplay=f;minDate=e;useAjax=o;s=dateDialogGet();s.dialog("option","title",friendlyName);dialogHeader.text(friendlyName+" Date");h=$("#"+i).attr("data-isodate");h?dialogDateBox.datetimepicker("setDate",new Date(h)):dialogDateBox.datetimepicker("setDate",new Date);e?dialogDateBox.datetimepicker("option","minDate",moment(minDate).toDate()):dialogDateBox.datetimepicker("option","minDate",null);s.dialog("open")}function dateDialogCreateUpdater(n,t,i,r,u,f,e,o){$("").attr({href:"#","class":"button small",style:"margin-right: 5px;"}).text("Update").click(function(s){s.preventDefault();dateDialogOpen(n,t,i,r,u,f,e,o)}).insertBefore("#"+i)}document.DiscoFunctions.DateDialogCreateUpdater=dateDialogCreateUpdater} \ No newline at end of file diff --git a/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers/disco.propertychangehelpers.js b/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers/disco.propertychangehelpers.js index f044c779..30fd030a 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers/disco.propertychangehelpers.js +++ b/Disco.Web/ClientSource/Scripts/Modules/Disco-PropertyChangeHelpers/disco.propertychangehelpers.js @@ -2,63 +2,71 @@ document.DiscoFunctions = {}; } if (!document.DiscoFunctions.PropertyChangeHelper) { - document.DiscoFunctions.PropertyValue = function (PropertyField) { - if (PropertyField[0].nodeName.toLowerCase() == 'input' && PropertyField.attr('type') == 'checkbox') { - return PropertyField.is(':checked'); + document.DiscoFunctions.PropertyValue = function (propertyField) { + if (propertyField[0].nodeName.toLowerCase() == 'input' && propertyField.attr('type') == 'checkbox') { + return propertyField.is(':checked'); } - return PropertyField.val(); + return propertyField.val(); }; - document.DiscoFunctions.PropertyChangeHelper = function (PropertyField, FieldWatermark, UpdateUrl, UpdatePropertyName) { - var fieldValue = document.DiscoFunctions.PropertyValue(PropertyField); + document.DiscoFunctions.PropertyChangeHelper = function (propertyField, fieldWatermark, updateUrl, updatePropertyName, data) { + var fieldValue = document.DiscoFunctions.PropertyValue(propertyField); var fieldChangeToken = null; - var $ajaxSave = PropertyField.nextAll('.ajaxSave').first(); - var $ajaxLoading = PropertyField.nextAll('.ajaxLoading').first(); + var $ajaxSave = propertyField.nextAll('.ajaxSave').first(); + var $ajaxLoading = propertyField.nextAll('.ajaxLoading').first(); var fieldChangeFunction = function () { $ajaxSave.hide(); - var changedValue = document.DiscoFunctions.PropertyValue(PropertyField); + var changedValue = document.DiscoFunctions.PropertyValue(propertyField); if (fieldValue != changedValue) { fieldValue = changedValue; if (fieldChangeToken) window.clearTimeout(fieldChangeToken); fieldChangeToken = window.setTimeout(function () { $ajaxLoading.show(); - var data = {}; - data[UpdatePropertyName] = fieldValue; - $.getJSON(UpdateUrl, data, function (response, result) { + if (!data) { + data = {}; + } + data[updatePropertyName] = fieldValue; + + $.getJSON(updateUrl, data, function (response, result) { if (result != 'success' || response != 'OK') { - alert('Unable to change property "' + UpdatePropertyName + '":\n' + response); + alert('Unable to change property "' + updatePropertyName + '":\n' + response); $ajaxLoading.hide(); } else { $ajaxLoading.hide().next('.ajaxOk').show().delay('fast').fadeOut('slow'); } + }).fail(function (jqXHR, textStatus, errorThrown) { + alert('Unable to change property "' + updatePropertyName + '":\n' + errorThrown); + $ajaxLoading.hide(); }) fieldChangeToken = null; }, 500); }; } - if (PropertyField[0].nodeName.toLowerCase() == 'input' && PropertyField.attr('type') == 'checkbox') { - PropertyField.click(fieldChangeFunction); + if (propertyField[0].nodeName.toLowerCase() == 'input' && propertyField.attr('type') == 'checkbox') { + propertyField.click(fieldChangeFunction); } else { - PropertyField.change(fieldChangeFunction); + propertyField.change(fieldChangeFunction); } // For Input Text Boxes - if (PropertyField[0].nodeName.toLowerCase() == 'input' && PropertyField.attr('type') == 'text') { - PropertyField.keydown(function (e) { + if (propertyField[0].nodeName.toLowerCase() == 'input' && propertyField.attr('type') == 'text') { + propertyField.keydown(function (e) { $ajaxSave.show(); if (e.which == 13) { $(this).blur(); } }) - .watermark(FieldWatermark) .blur(function () { $ajaxSave.hide(); }).focus(function () { $(this).select(); }); + if (fieldWatermark) { + propertyField.watermark(fieldWatermark); + } } // For TextAreas - if (PropertyField[0].nodeName.toLowerCase() == 'textarea') { - PropertyField.keydown(function () { + if (propertyField[0].nodeName.toLowerCase() == 'textarea') { + propertyField.keydown(function () { $ajaxSave.show(); }).blur(function () { $ajaxSave.hide(); diff --git a/Disco.Web/ClientSource/Scripts/Modules/Disco-jQueryExtensions.js b/Disco.Web/ClientSource/Scripts/Modules/Disco-jQueryExtensions.js index b15e98cd..40baefd4 100644 --- a/Disco.Web/ClientSource/Scripts/Modules/Disco-jQueryExtensions.js +++ b/Disco.Web/ClientSource/Scripts/Modules/Disco-jQueryExtensions.js @@ -1,4 +1,4 @@ -/// +/// (function ($) { var checkboxBulkSelectMethods = { diff --git a/Disco.Web/Controllers/PluginWebHandlerController.cs b/Disco.Web/Controllers/PluginWebHandlerController.cs index 9762a793..c46065cc 100644 --- a/Disco.Web/Controllers/PluginWebHandlerController.cs +++ b/Disco.Web/Controllers/PluginWebHandlerController.cs @@ -13,6 +13,11 @@ namespace Disco.Web.Controllers { public partial class PluginWebHandlerController : Controller { + protected override void OnAuthorization(AuthorizationContext filterContext) + { + base.OnAuthorization(filterContext); + } + [OutputCache(Duration = 0, Location = System.Web.UI.OutputCacheLocation.None)] public virtual ActionResult Index(string PluginId, string PluginAction) { @@ -24,7 +29,6 @@ namespace Disco.Web.Controllers { using (var pluginWebHandler = manifest.CreateWebHandler(this)) { - pluginWebHandler.OnActionExecuting(); return pluginWebHandler.ExecuteAction(PluginAction); } } diff --git a/Disco.Web/Extensions/ControllerExtensions.cs b/Disco.Web/Extensions/ControllerExtensions.cs index 491babc3..636bb4c5 100644 --- a/Disco.Web/Extensions/ControllerExtensions.cs +++ b/Disco.Web/Extensions/ControllerExtensions.cs @@ -1,11 +1,12 @@ -using System.Web.Mvc; +using RazorGenerator.Mvc; +using System; +using System.Web.Mvc; using System.Web.Routing; -namespace Disco.Web.Extensions +namespace Disco.Web { public static class ControllerExtensions { - public static ActionResult RedirectToAction(this Controller controller, ActionResult result, string urlFragment) { var callInfo = result.GetT4MVCResult(); @@ -24,5 +25,23 @@ namespace Disco.Web.Extensions } } + private static string[] _viewFileNames = new string[] { "cshtml" }; + public static ActionResult PrecompiledPartialView(this Controller controller, object model) where ViewType : WebViewPage + { + return PrecompiledPartialView(controller, typeof(ViewType), model); + } + + public static ActionResult PrecompiledPartialView(this Controller controller, Type viewType, object model) + { + if (!typeof(WebViewPage).IsAssignableFrom(viewType)) + throw new ArgumentException("ViewType must be assignable from WebViewPage", "viewType"); + + IView v = new PrecompiledMvcView(controller.Request.Path, viewType, false, _viewFileNames); + + if (model != null) + controller.ViewData.Model = model; + + return new PartialViewResult { View = v, ViewData = controller.ViewData, TempData = controller.TempData }; + } } } \ No newline at end of file