From ce208c44f29405f9a7f07ff49f359011d96caa35 Mon Sep 17 00:00:00 2001 From: Suha Can Date: Mon, 4 Oct 2010 12:27:59 -0700 Subject: [PATCH] - RuleEngine implementation for widget layer rules. - The ResultFilter uses the rule manager to filter out layers and widgets. - Added rules to default layers. - RuleManager with callback injection into the dlr scripting sandbox. - RuleProvider implementations for Url and Authenticated. Anonymous is just "not Authenticated". - RuleContext. - Renaming Layer.Rule to Layer.LayerRule. NHibernate bug for SqlCe most likely, couldn't escape "Rule" as expected. - Unit tests. --HG-- branch : dev --- .../Orchard.Tests.Modules.csproj | 20 +++++++ .../Widgets/WidgetsTests.cs | 56 ++++++++++++++++++ .../Orchard.Widgets/Filters/WidgetFilter.cs | 24 ++++++-- .../Modules/Orchard.Widgets/Migrations.cs | 11 ++-- .../Orchard.Widgets/Models/LayerPartRecord.cs | 2 +- .../Orchard.Widgets/Orchard.Widgets.csproj | 3 + .../RuleEngine/AuthenticatedRuleProvider.cs | 26 +++++++++ .../Orchard.Widgets/RuleEngine/RuleManager.cs | 58 +++++++++++++++++++ .../RuleEngine/UrlRuleProvider.cs | 21 +++++++ src/Orchard/Orchard.Framework.csproj | 3 + src/Orchard/UI/Widgets/IRuleManager.cs | 5 ++ src/Orchard/UI/Widgets/IRuleProvider.cs | 6 ++ src/Orchard/UI/Widgets/RuleContext.cs | 7 +++ 13 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 src/Orchard.Tests.Modules/Widgets/WidgetsTests.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/AuthenticatedRuleProvider.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/RuleManager.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/UrlRuleProvider.cs create mode 100644 src/Orchard/UI/Widgets/IRuleManager.cs create mode 100644 src/Orchard/UI/Widgets/IRuleProvider.cs create mode 100644 src/Orchard/UI/Widgets/RuleContext.cs diff --git a/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj b/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj index 010a7cda9..d4b49b24d 100644 --- a/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj +++ b/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj @@ -63,6 +63,21 @@ False ..\..\lib\fluentnhibernate\FluentNHibernate.dll + + False + ..\..\lib\dlr\IronRuby.dll + + + ..\..\lib\dlr\IronRuby.Libraries.dll + + + False + ..\..\lib\dlr\Microsoft.Dynamic.dll + + + False + ..\..\lib\dlr\Microsoft.Scripting.dll + False ..\..\lib\moq\Moq.dll @@ -115,6 +130,7 @@ + @@ -144,6 +160,10 @@ {79AED36E-ABD0-4747-93D3-8722B042454B} Orchard.Users + + {194D3CCC-1153-474D-8176-FDE8D7D0D0BD} + Orchard.Widgets + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} Orchard.Framework diff --git a/src/Orchard.Tests.Modules/Widgets/WidgetsTests.cs b/src/Orchard.Tests.Modules/Widgets/WidgetsTests.cs new file mode 100644 index 000000000..576148f1e --- /dev/null +++ b/src/Orchard.Tests.Modules/Widgets/WidgetsTests.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using Autofac; +using NUnit.Framework; +using Orchard.Scripting; +using Orchard.UI.Widgets; +using Orchard.Widgets.RuleEngine; + +namespace Orchard.Tests.Modules.Widgets { + [TestFixture] + public class WidgetsTests { + private IContainer _container; + private IRuleManager _ruleManager; + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + _container = builder.Build(); + _ruleManager = _container.Resolve(); + } + + [Test] + public void ProviderGetsCalledForExpression() { + bool result = _ruleManager.Matches("hello"); + Assert.IsTrue(result); + } + + [Test] + public void RubyExpressionIsEvaluated() { + bool result = _ruleManager.Matches("not hello"); + Assert.IsFalse(result); + } + + [Test] + public void ArgumentsArePassedCorrectly() { + bool result = _ruleManager.Matches("add(2, 3) == 5"); + Assert.IsTrue(result); + } + } + + public class AlwaysTrueRuleProvider : IRuleProvider { + public void Process(RuleContext ruleContext) { + if (ruleContext.FunctionName == "add") { + ruleContext.Result = Convert.ToInt32(ruleContext.Arguments[0]) + Convert.ToInt32(ruleContext.Arguments[1]); + return; + } + + ruleContext.Result = true; + } + } +} + diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/Filters/WidgetFilter.cs b/src/Orchard.Web/Modules/Orchard.Widgets/Filters/WidgetFilter.cs index 77dbf72b1..669a05903 100644 --- a/src/Orchard.Web/Modules/Orchard.Widgets/Filters/WidgetFilter.cs +++ b/src/Orchard.Web/Modules/Orchard.Widgets/Filters/WidgetFilter.cs @@ -1,20 +1,23 @@ -using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Web.Mvc; using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; using Orchard.Mvc.Filters; using Orchard.UI.Admin; +using Orchard.UI.Widgets; using Orchard.Widgets.Models; namespace Orchard.Widgets.Filters { public class WidgetFilter : FilterProvider, IResultFilter { private readonly IContentManager _contentManager; private readonly IWorkContextAccessor _workContextAccessor; + private readonly IRuleManager _ruleManager; - public WidgetFilter(IContentManager contentManager, IWorkContextAccessor workContextAccessor) { + public WidgetFilter(IContentManager contentManager, IWorkContextAccessor workContextAccessor, IRuleManager ruleManager) { _contentManager = contentManager; _workContextAccessor = workContextAccessor; + _ruleManager = ruleManager; } public void OnResultExecuting(ResultExecutingContext filterContext) { @@ -30,13 +33,22 @@ namespace Orchard.Widgets.Filters { // Once the Rule Engine is done: // Get Layers and filter by zone and rule IEnumerable widgetParts = _contentManager.Query().List(); + IEnumerable activeLayers = _contentManager.Query().List(); + + List activeLayerIds = new List(); + foreach (var activeLayer in activeLayers) { + if (_ruleManager.Matches(activeLayer.Record.LayerRule)) { + activeLayerIds.Add(activeLayer.ContentItem.Id); + } + } // Build and add shape to zone. var zones = workContext.Page.Zones; foreach (var widgetPart in widgetParts) { - var widgetShape = _contentManager.BuildDisplayModel(widgetPart); - - zones[widgetPart.Record.Zone].Add(widgetShape, widgetPart.Record.Position); + if (activeLayerIds.Contains(widgetPart.As().Container.ContentItem.Id)) { + var widgetShape = _contentManager.BuildDisplayModel(widgetPart); + zones[widgetPart.Record.Zone].Add(widgetShape, widgetPart.Record.Position); + } } } diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/Migrations.cs b/src/Orchard.Web/Modules/Orchard.Widgets/Migrations.cs index 0949ccdd7..9aab0855d 100644 --- a/src/Orchard.Web/Modules/Orchard.Widgets/Migrations.cs +++ b/src/Orchard.Web/Modules/Orchard.Widgets/Migrations.cs @@ -21,9 +21,12 @@ namespace Orchard.Widgets { } public void CreateDefaultLayers() { - _contentManager.Create("Layer", t => t.Record.Name = "Default"); - _contentManager.Create("Layer", t => t.Record.Name = "Authenticated"); - _contentManager.Create("Layer", t => t.Record.Name = "Anonymous"); + IContent defaultLayer = _contentManager.Create("Layer", t => { t.Record.Name = "Default"; t.Record.LayerRule = "true"; }); + _contentManager.Publish(defaultLayer.ContentItem); + IContent authenticatedLayer = _contentManager.Create("Layer", t => { t.Record.Name = "Authenticated"; t.Record.LayerRule = "Authenticated"; }); + _contentManager.Publish(authenticatedLayer.ContentItem); + IContent anonymousLayer = _contentManager.Create("Layer", t => { t.Record.Name = "Anonymous"; t.Record.LayerRule = "not Authenticated"; }); + _contentManager.Publish(anonymousLayer.ContentItem); } } @@ -43,7 +46,7 @@ namespace Orchard.Widgets { .ContentPartRecord() .Column("Name") .Column("Description") - .Column("Rule") + .Column("LayerRule") ); SchemaBuilder.CreateTable("WidgetPartRecord", table => table diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/Models/LayerPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Widgets/Models/LayerPartRecord.cs index b444f0cdb..66ec4506e 100644 --- a/src/Orchard.Web/Modules/Orchard.Widgets/Models/LayerPartRecord.cs +++ b/src/Orchard.Web/Modules/Orchard.Widgets/Models/LayerPartRecord.cs @@ -4,6 +4,6 @@ namespace Orchard.Widgets.Models { public class LayerPartRecord : ContentPartRecord { public virtual string Name { get; set; } public virtual string Description { get; set; } - public virtual string Rule { get; set; } + public virtual string LayerRule { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj b/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj index ea889dd91..bb4296e40 100644 --- a/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj +++ b/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj @@ -69,6 +69,9 @@ + + + diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/AuthenticatedRuleProvider.cs b/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/AuthenticatedRuleProvider.cs new file mode 100644 index 000000000..fbd59d9c2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/AuthenticatedRuleProvider.cs @@ -0,0 +1,26 @@ +using System; +using Orchard.Security; +using Orchard.UI.Widgets; + +namespace Orchard.Widgets.RuleEngine { + public class AuthenticatedRuleProvider : IRuleProvider { + private readonly IAuthenticationService _authenticationService; + + public AuthenticatedRuleProvider(IAuthenticationService authenticationService) { + _authenticationService = authenticationService; + } + + public void Process(RuleContext ruleContext) { + if (!String.Equals(ruleContext.FunctionName, "Authenticated", StringComparison.OrdinalIgnoreCase)) { + return; + } + + if (_authenticationService.GetAuthenticatedUser() != null) { + ruleContext.Result = true; + return; + } + + ruleContext.Result = false; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/RuleManager.cs b/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/RuleManager.cs new file mode 100644 index 000000000..5df76b16c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/RuleManager.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using Orchard.Scripting; +using Orchard.UI.Widgets; + +namespace Orchard.Widgets.RuleEngine { + public class RuleManager : IRuleManager { + private readonly IEnumerable _ruleProviders; + private readonly IScriptingManager _scriptingManager; + + public RuleManager(IEnumerable ruleProviders, IScriptingManager scriptingManager) { + _ruleProviders = ruleProviders; + _scriptingManager = scriptingManager; + } + + public bool Matches(string expression) { + _scriptingManager.SetVariable("callbacks", new CallbackApi(this)); + dynamic execContext = _scriptingManager.Eval(@" + class ExecContext + def initialize(callbacks) + @callbacks = callbacks; + end + + def execute(text) + instance_eval(text.to_s); + end + + def method_missing(name, *args, &block) + @callbacks.send(name, args, &block); + end + end + ExecContext.new(callbacks)"); + return execContext.execute(expression); + } + + public class CallbackApi { + private readonly RuleManager _ruleManager; + + public CallbackApi(RuleManager ruleManager) { + _ruleManager = ruleManager; + } + + public object send(string name, IList args) { + return _ruleManager.Evaluate(name, args); + } + } + + private object Evaluate(string name, IList args) { + RuleContext ruleContext = new RuleContext {FunctionName = name, Arguments = args.ToArray()}; + + foreach (var ruleProvider in _ruleProviders) { + ruleProvider.Process(ruleContext); + } + + return ruleContext.Result; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/UrlRuleProvider.cs b/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/UrlRuleProvider.cs new file mode 100644 index 000000000..6848dfe45 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/RuleEngine/UrlRuleProvider.cs @@ -0,0 +1,21 @@ +using System; +using Orchard.UI.Widgets; + +namespace Orchard.Widgets.RuleEngine { + public class UrlRuleProvider : IRuleProvider { + private readonly IWorkContextAccessor _workContextAccessor; + + public UrlRuleProvider(IWorkContextAccessor workContextAccessor) { + _workContextAccessor = workContextAccessor; + } + + public void Process(RuleContext ruleContext) { + if (!String.Equals(ruleContext.FunctionName, "Url", StringComparison.OrdinalIgnoreCase)) { + return; + } + + var context = _workContextAccessor.GetContext(); + ruleContext.Result = context.HttpContext.Request.RawUrl.StartsWith(Convert.ToString(ruleContext.Arguments[0])); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index c781cde7e..1dc640f51 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -179,6 +179,9 @@ + + + diff --git a/src/Orchard/UI/Widgets/IRuleManager.cs b/src/Orchard/UI/Widgets/IRuleManager.cs new file mode 100644 index 000000000..af3ae42a3 --- /dev/null +++ b/src/Orchard/UI/Widgets/IRuleManager.cs @@ -0,0 +1,5 @@ +namespace Orchard.UI.Widgets { + public interface IRuleManager : IDependency { + bool Matches(string expression); + } +} diff --git a/src/Orchard/UI/Widgets/IRuleProvider.cs b/src/Orchard/UI/Widgets/IRuleProvider.cs new file mode 100644 index 000000000..8a8b33dde --- /dev/null +++ b/src/Orchard/UI/Widgets/IRuleProvider.cs @@ -0,0 +1,6 @@ +namespace Orchard.UI.Widgets { + public interface IRuleProvider : IDependency { + void Process(RuleContext ruleContext); + } +} + diff --git a/src/Orchard/UI/Widgets/RuleContext.cs b/src/Orchard/UI/Widgets/RuleContext.cs new file mode 100644 index 000000000..5b744a5fd --- /dev/null +++ b/src/Orchard/UI/Widgets/RuleContext.cs @@ -0,0 +1,7 @@ +namespace Orchard.UI.Widgets { + public class RuleContext { + public string FunctionName { get; set; } + public object[] Arguments { get; set; } + public object Result { get; set; } + } +}