diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 00000000000..0c5ddee146d --- /dev/null +++ b/.cursorignore @@ -0,0 +1,30 @@ +node_modules/ +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +.DS_Store +.idea/ +.vscode/ +.gitignore +.git/ +.gitignore + +.vs/ + +build/ +build.* +.yarn/ +Artifacts/ +DotNetNuke.Internal.SourceGenerators/ +Install/ +tools/ +Website/ +.nuget/ +.github/ +Packages/ +Refs/ +tools/ +Dnn Platform/node_modules/ +Dnn Platform/Tests \ No newline at end of file diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000000..216c4271161 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,72 @@ + +# .NET Development Rules + +You are a senior .NET full stack developer and an expert in C#, ASP.NET Framework, ASP.NET Core, and ReactJS + +## This project + +This is a CMS that runs on classic ASP.NET Framework (version 4.8). The default entry point is /default.aspx which can be found under /DNN Platform/Website. +We are building a new pipeline based on ASP.NET MVC with the goal of moving away from webforms and towards .NET Core. Right now we will create a hybrid solution +with two rendering mechanisms: the old webforms pipeline through default.aspx and a new pipeline through /DesktopModules/Default/Page/{tabId}/{locale}. This +should be picked up and handled in the DotNetNuke.Web.MvcPipeline library. The skin that is being loaded should decide which pipeline is being used to render a +page to the client. + +## Code Organization + +We want to minimize any code changes to the existing project and concentrate as much as we can any code changes in the DotNetNuke.Web.MvcPipeline project. +If need be we can create more projects to house new code. + +## Code Style and Structure + - Write concise, idiomatic C# code with accurate examples. + - Follow .NET and ASP.NET Core conventions and best practices. + - Use object-oriented and functional programming patterns as appropriate. + - Prefer LINQ and lambda expressions for collection operations. + - Use descriptive variable and method names (e.g., 'IsUserSignedIn', 'CalculateTotal'). + - Structure files according to .NET conventions (Controllers, Models, Services, etc.). + +## Naming Conventions + - Follow guidelines from the stylecop.json file + +## C# and .NET Usage + - + +## Syntax and Formatting + - Use 'var' for implicit typing when the type is obvious. + +## Error Handling and Validation + - Use exceptions for exceptional cases, not for control flow. + - Implement proper error logging using built-in .NET logging or a third-party logger. + - Use Data Annotations or Fluent Validation for model validation. + - Implement global exception handling middleware. + - Return appropriate HTTP status codes and consistent error responses. + +## API Design + - Follow RESTful API design principles. + - Use attribute routing in controllers. + - Implement versioning for your API. + - Use action filters for cross-cutting concerns. + +## Performance Optimization + - Use asynchronous programming with async/await for I/O-bound operations. + - Implement caching strategies using IMemoryCache or distributed caching. + - Use efficient LINQ queries and avoid N+1 query problems. + - Implement pagination for large data sets. + +## Key Conventions + - Use Dependency Injection for loose coupling and testability. + - Implement repository pattern. + - Use AutoMapper for object-to-object mapping if needed. + - Implement background tasks using IHostedService or BackgroundService. + +## Testing + - Write unit tests using xUnit, NUnit, or MSTest. + - Use Moq or NSubstitute for mocking dependencies. + - Implement integration tests for API endpoints. + +## Security + - Implement proper CORS policies. + +## API Documentation + - Provide XML comments for controllers and models. + +Follow the official Microsoft documentation and ASP.NET MVC guides for best practices in routing, controllers, models, and other API components. diff --git a/Build/Tasks/packaging.json b/Build/Tasks/packaging.json index 1cc83889d95..d69c69a73dc 100644 --- a/Build/Tasks/packaging.json +++ b/Build/Tasks/packaging.json @@ -36,6 +36,10 @@ "/bin/System.Web.Http.dll", "/bin/System.Web.Http.WebHost.dll", "/bin/DotNetNuke.Web.Mvc.dll", + + "/bin/DotNetNuke.Web.MvcPipeline.dll", + "/bin/DotNetNuke.Web.MvcWebsite.dll", + "/bin/System.Web.Mvc.dll", "/bin/System.Web.Helpers.dll", "/bin/Microsoft.Web.Helpers.dll", diff --git a/DNN Platform/Components/Microsoft.AspNetMvc/Microsoft.AspNetMvc.dnn b/DNN Platform/Components/Microsoft.AspNetMvc/Microsoft.AspNetMvc.dnn index 51666a51f5c..a1800fee000 100644 --- a/DNN Platform/Components/Microsoft.AspNetMvc/Microsoft.AspNetMvc.dnn +++ b/DNN Platform/Components/Microsoft.AspNetMvc/Microsoft.AspNetMvc.dnn @@ -17,6 +17,16 @@ bin + DotNetNuke.Web.MvcPipeline.dll + 0.0.1.0 + + + bin + DotNetNuke.Web.MvcWebsite.dll + 0.0.1.0 + + + bin DotNetNuke.Web.Mvc.dll 9.10.0 diff --git a/DNN Platform/DotNetNuke.Abstractions/Portals/IPortalSettings.cs b/DNN Platform/DotNetNuke.Abstractions/Portals/IPortalSettings.cs index ddd390191a8..25fd5f97a5d 100644 --- a/DNN Platform/DotNetNuke.Abstractions/Portals/IPortalSettings.cs +++ b/DNN Platform/DotNetNuke.Abstractions/Portals/IPortalSettings.cs @@ -365,5 +365,8 @@ public interface IPortalSettings /// Gets a value indicating whether to display the dropdowns to quickly add a moduel to the page in the edit bar. bool ShowQuickModuleAddMenu { get; } + + /// Gets the pipeline type for the portal. + string PagePipeline { get; } } } diff --git a/DNN Platform/DotNetNuke.Abstractions/Portals/PagePipelineConstants.cs b/DNN Platform/DotNetNuke.Abstractions/Portals/PagePipelineConstants.cs new file mode 100644 index 00000000000..85f3d4008ea --- /dev/null +++ b/DNN Platform/DotNetNuke.Abstractions/Portals/PagePipelineConstants.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Abstractions.Portals +{ + /// + /// Provides constants for the page pipelines used in the DNN platform, + /// such as WebForms and MVC. + /// + public static class PagePipelineConstants + { + /// WebForms. + public const string WebForms = "webforms"; + + /// MVC. + public const string Mvc = "mvc"; + } +} diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnRedirecttoRouteResult.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnRedirecttoRouteResult.cs index c74cb2d09ff..91c60d30025 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnRedirecttoRouteResult.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/ActionResults/DnnRedirecttoRouteResult.cs @@ -42,7 +42,10 @@ public override void ExecuteResult(ControllerContext context) { Requires.NotNull("context", context); - Guard.Against(context.IsChildAction, "Cannot Redirect In Child Action"); + if (!context.RouteData.Values.ContainsKey("mvcpage")) + { + Guard.Against(context.IsChildAction, "Cannot Redirect In Child Action"); + } string url; if (this.Url != null && context.Controller is IDnnController) diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs index 0219b802562..2eda490d429 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs @@ -2,62 +2,62 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information -namespace DotNetNuke.Web.Mvc.Framework.Controllers -{ - using System; - using System.Diagnostics.CodeAnalysis; - using System.Text; - using System.Web.Mvc; - using System.Web.Routing; - using System.Web.UI; - - using DotNetNuke.Entities.Modules; - using DotNetNuke.Entities.Modules.Actions; - using DotNetNuke.Entities.Portals; - using DotNetNuke.Entities.Tabs; - using DotNetNuke.Entities.Users; - using DotNetNuke.Services.Localization; - using DotNetNuke.UI.Modules; - using DotNetNuke.Web.Mvc.Framework.ActionResults; - using DotNetNuke.Web.Mvc.Framework.Modules; - using DotNetNuke.Web.Mvc.Helpers; - - public abstract class DnnController : Controller, IDnnController +namespace DotNetNuke.Web.Mvc.Framework.Controllers +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Text; + using System.Web.Mvc; + using System.Web.Routing; + using System.Web.UI; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Entities.Users; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.Mvc.Framework.ActionResults; + using DotNetNuke.Web.Mvc.Framework.Modules; + using DotNetNuke.Web.Mvc.Helpers; + + public abstract class DnnController : Controller, IDnnController { /// Initializes a new instance of the class. - protected DnnController() - { - this.ActionInvoker = new ResultCapturingActionInvoker(); - } - - public ModuleInfo ActiveModule - { - get { return (this.ModuleContext == null) ? null : this.ModuleContext.Configuration; } - } - - public TabInfo ActivePage - { - get { return (this.PortalSettings == null) ? null : this.PortalSettings.ActiveTab; } - } - - public PortalSettings PortalSettings - { - get { return (this.ModuleContext == null) ? null : this.ModuleContext.PortalSettings; } + protected DnnController() + { + this.ActionInvoker = new ResultCapturingActionInvoker(); + } + + public ModuleInfo ActiveModule + { + get { return (this.ModuleContext == null) ? null : this.ModuleContext.Configuration; } + } + + public TabInfo ActivePage + { + get { return (this.PortalSettings == null) ? null : this.PortalSettings.ActiveTab; } + } + + public PortalSettings PortalSettings + { + get { return (this.ModuleContext == null) ? null : this.ModuleContext.PortalSettings; } } /// - public ActionResult ResultOfLastExecute - { - get - { - var actionInvoker = this.ActionInvoker as ResultCapturingActionInvoker; - return (actionInvoker != null) ? actionInvoker.ResultOfLastInvoke : null; - } - } - - public new UserInfo User - { - get { return (this.PortalSettings == null) ? null : this.PortalSettings.UserInfo; } + public ActionResult ResultOfLastExecute + { + get + { + var actionInvoker = this.ActionInvoker as ResultCapturingActionInvoker; + return (actionInvoker != null) ? actionInvoker.ResultOfLastInvoke : null; + } + } + + public new UserInfo User + { + get { return (this.PortalSettings == null) ? null : this.PortalSettings.UserInfo; } } /// @@ -79,79 +79,115 @@ public ActionResult ResultOfLastExecute public ViewEngineCollection ViewEngineCollectionEx { get; set; } /// - public string LocalizeString(string key) - { - return Localization.GetString(key, this.LocalResourceFile); - } - - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Breaking change")] - protected internal RedirectToRouteResult RedirectToDefaultRoute() - { - return new DnnRedirecttoRouteResult(string.Empty, string.Empty, string.Empty, null, false); + public string LocalizeString(string key) + { + return Localization.GetString(key, this.LocalResourceFile); + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Breaking change")] + protected internal RedirectToRouteResult RedirectToDefaultRoute() + { + return new DnnRedirecttoRouteResult(string.Empty, string.Empty, string.Empty, null, false); } /// - protected override RedirectToRouteResult RedirectToAction(string actionName, string controllerName, RouteValueDictionary routeValues) - { - return new DnnRedirecttoRouteResult(actionName, controllerName, string.Empty, routeValues, false, this.Url); + protected override RedirectToRouteResult RedirectToAction(string actionName, string controllerName, RouteValueDictionary routeValues) + { + return new DnnRedirecttoRouteResult(actionName, controllerName, string.Empty, routeValues, false, this.Url); } /// - protected override ViewResult View(IView view, object model) - { - if (model != null) - { - this.ViewData.Model = model; - } - - return new DnnViewResult - { - View = view, - ViewData = this.ViewData, - TempData = this.TempData, - }; + protected override ViewResult View(IView view, object model) + { + if (model != null) + { + this.ViewData.Model = model; + } + + return new DnnViewResult + { + View = view, + ViewData = this.ViewData, + TempData = this.TempData, + }; } /// - protected override ViewResult View(string viewName, string masterName, object model) - { - if (model != null) - { - this.ViewData.Model = model; - } - - return new DnnViewResult - { - ViewName = viewName, - MasterName = masterName, - ViewData = this.ViewData, - TempData = this.TempData, - ViewEngineCollection = this.ViewEngineCollection, - }; + protected override ViewResult View(string viewName, string masterName, object model) + { + if (model != null) + { + this.ViewData.Model = model; + } + + return new DnnViewResult + { + ViewName = viewName, + MasterName = masterName, + ViewData = this.ViewData, + TempData = this.TempData, + ViewEngineCollection = this.ViewEngineCollection, + }; } /// - protected override PartialViewResult PartialView(string viewName, object model) - { - if (model != null) - { - this.ViewData.Model = model; - } - - return new DnnPartialViewResult - { - ViewName = viewName, - ViewData = this.ViewData, - TempData = this.TempData, - ViewEngineCollection = this.ViewEngineCollection, - }; + protected override PartialViewResult PartialView(string viewName, object model) + { + if (model != null) + { + this.ViewData.Model = model; + } + + return new DnnPartialViewResult + { + ViewName = viewName, + ViewData = this.ViewData, + TempData = this.TempData, + ViewEngineCollection = this.ViewEngineCollection, + }; } /// - protected override void Initialize(RequestContext requestContext) - { - base.Initialize(requestContext); - this.Url = new DnnUrlHelper(requestContext, this); - } - } -} + protected override void Initialize(RequestContext requestContext) + { + base.Initialize(requestContext); + + if (requestContext.RouteData != null && requestContext.RouteData.Values.ContainsKey("mvcpage")) + { + var values = requestContext.RouteData.Values; + var moduleContext = new ModuleInstanceContext(); + var moduleInfo = ModuleController.Instance.GetModule((int)values["ModuleId"], (int)values["TabId"], false); + + if (moduleInfo.ModuleControlId != (int)values["ModuleControlId"]) + { + moduleInfo = moduleInfo.Clone(); + moduleInfo.ContainerPath = (string)values["ContainerPath"]; + moduleInfo.ContainerSrc = (string)values["ContainerSrc"]; + moduleInfo.ModuleControlId = (int)values["ModuleControlId"]; + moduleInfo.PaneName = (string)values["PanaName"]; + moduleInfo.IconFile = (string)values["IconFile"]; + } + + moduleContext.Configuration = moduleInfo; + + this.ModuleContext = new ModuleInstanceContext() { Configuration = moduleInfo }; + this.LocalResourceFile = string.Format( + "~/DesktopModules/MVC/{0}/{1}/{2}.resx", + moduleInfo.DesktopModule.FolderName, + Localization.LocalResourceDirectory, + this.RouteData.Values["controller"]); + + var moduleApplication = new ModuleApplication(requestContext, true) + { + ModuleName = moduleInfo.DesktopModule.ModuleName, + FolderPath = moduleInfo.DesktopModule.FolderName, + }; + moduleApplication.Init(); + + this.ViewEngineCollectionEx = moduleApplication.ViewEngines; + } + + this.Url = new DnnUrlHelper(requestContext, this); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs index 807a79dd99f..547f5f9af5b 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Modules/ResultCapturingActionInvoker.cs @@ -6,7 +6,13 @@ namespace DotNetNuke.Web.Mvc.Framework.Modules { using System; using System.Collections.Generic; - using System.Web.Mvc; + using System.Web.Mvc; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.Mvc.Framework.Controllers; + using DotNetNuke.Web.Mvc.Routing; public class ResultCapturingActionInvoker : ControllerActionInvoker { @@ -14,7 +20,7 @@ public class ResultCapturingActionInvoker : ControllerActionInvoker /// protected override ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList filters, ActionDescriptor actionDescriptor, IDictionary parameters) - { + { var context = base.InvokeActionMethodWithFilters(controllerContext, filters, actionDescriptor, parameters); this.ResultOfLastInvoke = context.Result; return context; @@ -35,6 +41,11 @@ protected override void InvokeActionResult(ControllerContext controllerContext, if (this.ResultOfLastInvoke == null) { this.ResultOfLastInvoke = actionResult; + } + + if (controllerContext.RouteData.Values.ContainsKey("mvcpage")) + { + base.InvokeActionResult(controllerContext, actionResult); } } } diff --git a/DNN Platform/DotNetNuke.Web.Mvc/Routing/MvcRoutingManager.cs b/DNN Platform/DotNetNuke.Web.Mvc/Routing/MvcRoutingManager.cs index b45c843bc6b..901945e35ed 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Routing/MvcRoutingManager.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Routing/MvcRoutingManager.cs @@ -79,6 +79,11 @@ public Route MapRoute(string moduleFolderName, string routeName, string url, obj route = MapRouteWithNamespace(fullRouteName, routeUrl, defaults, constraints, namespaces); this.routes.Add(route); Logger.Trace("Mapping route: " + fullRouteName + " @ " + routeUrl); + fullRouteName = "mvcpipeline" + fullRouteName; + routeUrl = routeUrl.Replace("/MVC/", "/"); + route = MapRouteWithNamespaceAndArea(fullRouteName, moduleFolderName, routeUrl, defaults, constraints, namespaces); + this.routes.Add(route); + Logger.Trace("Mapping route for mvcpipeline: " + fullRouteName + " Area=" + moduleFolderName + " @ " + routeUrl); } return route; @@ -136,6 +141,29 @@ private static Route MapRouteWithNamespace(string name, string url, object defau return route; } + private static Route MapRouteWithNamespaceAndArea(string name, string area, string url, object defaults, object constraints, string[] namespaces) + { + var route = new Route(url, new DnnMvcRouteHandler()) + { + Defaults = CreateRouteValueDictionaryUncached(defaults), + Constraints = CreateRouteValueDictionaryUncached(constraints), + }; + if (route.DataTokens == null) + { + route.DataTokens = new RouteValueDictionary(); + } + + route.DataTokens.Add("area", area); + ConstraintValidation.Validate(route); + if ((namespaces != null) && (namespaces.Length > 0)) + { + route.SetNameSpaces(namespaces); + } + + route.SetName(name); + return route; + } + private static RouteValueDictionary CreateRouteValueDictionaryUncached(object values) { var dictionary = values as IDictionary; diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/PropertyHelper.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/PropertyHelper.cs new file mode 100644 index 00000000000..2733dbd76f2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/PropertyHelper.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Commons +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + using DotNetNuke.Common; + + /// + /// Provides fast reflection-based access to property values using cached delegates. + /// + internal class PropertyHelper + { + private static readonly MethodInfo CallPropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetterByReference", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CallPropertyGetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetter", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly ConcurrentDictionary ReflectionCache = new ConcurrentDictionary(); + private readonly Func valueGetter; + + /// Initializes a new instance of the class. + /// The property. + public PropertyHelper(PropertyInfo property) + { + Requires.NotNull("property", property); + + this.Name = property.Name; + this.valueGetter = MakeFastPropertyGetter(property); + } + + // Implementation of the fast getter. + private delegate TValue ByRefFunc(ref TDeclaringType arg); + + /// + /// Gets or sets the property name. + /// + public virtual string Name { get; protected set; } + + /// Creates and caches fast property helpers that expose getters for every public get property on the underlying type. + /// The instance to extract property accessors for. + /// A cached array of all public property getters from the underlying type of this instance. + public static PropertyHelper[] GetProperties(object instance) + { + return GetProperties(instance, CreateInstance, ReflectionCache); + } + + /// Creates a single fast property getter. The result is not cached. + /// Property information to extract the getter for. + /// A fast property getter delegate. + /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. + public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) + { + Requires.NotNull("property", propertyInfo); + + var getMethod = propertyInfo.GetGetMethod(); + Guard.Against(getMethod == null, "Property must have a Get Method"); + Guard.Against(getMethod.IsStatic, "Property's Get method must not be static"); + Guard.Against(getMethod.GetParameters().Length != 0, "Property's Get method must not have parameters"); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "this". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + var typeInput = getMethod.ReflectedType; + var typeOutput = getMethod.ReturnType; + + Delegate callPropertyGetterDelegate; + if (typeInput.IsValueType) + { + // Create a delegate (ref TDeclaringType) -> TValue + var propertyGetterAsFunc = getMethod.CreateDelegate(typeof(ByRefFunc<,>).MakeGenericType(typeInput, typeOutput)); + var callPropertyGetterClosedGenericMethod = CallPropertyGetterByReferenceOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = Delegate.CreateDelegate(typeof(Func), propertyGetterAsFunc, callPropertyGetterClosedGenericMethod); + } + else + { + // Create a delegate TDeclaringType -> TValue + var propertyGetterAsFunc = getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(typeInput, typeOutput)); + var callPropertyGetterClosedGenericMethod = CallPropertyGetterOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = Delegate.CreateDelegate(typeof(Func), propertyGetterAsFunc, callPropertyGetterClosedGenericMethod); + } + + return (Func)callPropertyGetterDelegate; + } + + /// + /// Gets the value of the property for the specified instance. + /// + /// The instance to read the property value from. + /// The property value. + public object GetValue(object instance) + { + // Contract.Assert(valueGetter != null, "Must call Initialize before using this object"); + return this.valueGetter(instance); + } + + /// + /// Gets or creates cached instances for the specified object's type. + /// + /// The instance whose type is used to discover properties. + /// A factory used to create new instances. + /// The cache used to store helpers per type. + /// An array of objects describing the type's public properties. + protected static PropertyHelper[] GetProperties( + object instance, + Func createPropertyHelper, + ConcurrentDictionary cache) + { + // Using an array rather than IEnumerable, as this will be called on the hot path numerous times. + PropertyHelper[] helpers; + + var type = instance.GetType(); + + if (!cache.TryGetValue(type, out helpers)) + { + // We avoid loading indexed properties using the where statement. + // Indexed properties are not useful (or valid) for grabbing properties off an anonymous object. + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(prop => prop.GetIndexParameters().Length == 0 && + prop.GetMethod != null); + + var newHelpers = new List(); + + foreach (var property in properties) + { + var propertyHelper = createPropertyHelper(property); + + newHelpers.Add(propertyHelper); + } + + helpers = newHelpers.ToArray(); + cache.TryAdd(type, helpers); + } + + return helpers; + } + + private static object CallPropertyGetter(Func getter, object @this) + { + return getter((TDeclaringType)@this); + } + + private static object CallPropertyGetterByReference(ByRefFunc getter, object @this) + { + var unboxed = (TDeclaringType)@this; + return getter(ref unboxed); + } + + private static PropertyHelper CreateInstance(PropertyInfo property) + { + return new PropertyHelper(property); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/TypeHelper.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/TypeHelper.cs new file mode 100644 index 00000000000..7ad7f69d38d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/TypeHelper.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Commons +{ + using System.Web.Routing; + + /// + /// Helper methods for working with anonymous or POCO types in the MVC pipeline. + /// + public static class TypeHelper + { + /// + /// Given an object of anonymous type, add each property as a key and associated with its value to a dictionary. + /// + /// This helper will cache accessors and types, and is intended when the anonymous object is accessed multiple + /// times throughout the lifetime of the web application. + /// + /// The source object whose public properties will be projected. + /// A route value dictionary containing the object's property names and values. + public static RouteValueDictionary ObjectToDictionary(object value) + { + var dictionary = new RouteValueDictionary(); + + if (value != null) + { + foreach (var helper in PropertyHelper.GetProperties(value)) + { + dictionary.Add(helper.Name, helper.GetValue(value)); + } + } + + return dictionary; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.Content.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.Content.cs new file mode 100644 index 00000000000..451d4bf59d1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.Content.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Containers +{ + using System; + using System.IO; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Internal; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.Models; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.Modules; + + public static partial class SkinHelpers + { + public static IHtmlString Content(this HtmlHelper htmlHelper) + { + var model = htmlHelper.ViewData.Model; + if (model == null) + { + throw new InvalidOperationException("The model need to be present."); + } + + var moduleDiv = new TagBuilder("div"); + moduleDiv.AddCssClass(model.ModuleHost.CssClass); + + // render module control + IMvcModuleControl moduleControl = null; + try + { + moduleControl = ModuleControlFactory.CreateModuleControl(model.ModuleConfiguration, model.ModuleConfiguration.ModuleControl.ControlSrc); + moduleDiv.InnerHtml += htmlHelper.Control(moduleControl); + } + catch (Exception ex) + { + if (TabPermissionController.CanAdminPage()) + { + moduleDiv.InnerHtml += htmlHelper.ModuleErrorMessage(ex.Message, "Error loading module"); + } + } + + var moduleContentPaneDiv = new TagBuilder("div"); + if (!string.IsNullOrEmpty(model.ContentPaneCssClass)) + { + moduleContentPaneDiv.AddCssClass(model.ContentPaneCssClass); + } + + if (!ModuleHostModel.IsViewMode(model.ModuleConfiguration, model.PortalSettings) && htmlHelper.ViewContext.HttpContext.Request.QueryString["dnnprintmode"] != "true") + { + JavaScript.RequestRegistration(CommonJs.DnnPlugins); + if (model.EditMode && model.ModuleConfiguration.ModuleID > 0 && moduleControl != null) + { + // render module actions + moduleContentPaneDiv.InnerHtml += htmlHelper.ModuleActions(moduleControl); + } + + // register admin.css + var controller = HtmlHelpers.GetClientResourcesController(htmlHelper); + controller.RegisterStylesheet(Globals.HostPath + "admin.css", FileOrder.Css.AdminCss, false); + } + + if (!string.IsNullOrEmpty(model.ContentPaneStyle)) + { + moduleContentPaneDiv.Attributes["style"] = model.ContentPaneStyle; + } + + if (!string.IsNullOrEmpty(model.Header)) + { + moduleContentPaneDiv.InnerHtml += model.Header; + } + + moduleContentPaneDiv.InnerHtml += moduleDiv.ToString(); + if (!string.IsNullOrEmpty(model.Footer)) + { + moduleContentPaneDiv.InnerHtml += model.Footer; + } + + return MvcHtmlString.Create(moduleContentPaneDiv.InnerHtml); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.Title.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.Title.cs new file mode 100644 index 00000000000..86a7da9fdf9 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.Title.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Containers +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Container-related HTML helper methods for skinning, including module title rendering. + /// + public static partial class SkinHelpers + { + /// + /// Renders the module title for the current container, optionally applying a CSS class. + /// + /// The HTML helper for the container model. + /// An optional CSS class to apply to the title wrapper element. + /// The HTML string representing the module title. + public static IHtmlString Title(this HtmlHelper htmlHelper, string cssClass = "") + { + var model = htmlHelper.ViewData.Model; + if (model == null) + { + throw new InvalidOperationException("The model need to be present."); + } + + var labelDiv = new TagBuilder("div"); + labelDiv.InnerHtml = model.ModuleConfiguration.ModuleTitle; + if (!string.IsNullOrEmpty(cssClass)) + { + labelDiv.AddCssClass(cssClass); + } + + return MvcHtmlString.Create(labelDiv.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.cs new file mode 100644 index 00000000000..eed7c59388d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Containers +{ + /// + /// Provides helper methods for rendering skin-related content in the MVC pipeline. + /// + public static partial class SkinHelpers + { + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/DnnPageController.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/DnnPageController.cs new file mode 100644 index 00000000000..55a9ed01909 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/DnnPageController.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Controllers +{ + using System; + using System.Web.Mvc; + + using DotNetNuke.Entities.Portals; + + /// + /// Base controller for DNN MVC page controllers, exposing common services and portal context. + /// + public abstract class DnnPageController : Controller, IMvcController + { + /// + /// Initializes a new instance of the class. + /// + /// The dependency injection service provider. + protected DnnPageController(IServiceProvider dependencyProvider) + { + this.DependencyProvider = dependencyProvider; + } + + /// + /// Gets the dependency injection service provider for the current request. + /// + public IServiceProvider DependencyProvider { get; private set; } + + /// + /// Gets the current portal settings. + /// + public PortalSettings PortalSettings + { + get + { + return PortalController.Instance.GetCurrentPortalSettings(); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/IMvcController.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/IMvcController.cs new file mode 100644 index 00000000000..d19c5e63160 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/IMvcController.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Controllers +{ + using System.Web.Mvc; + + /// + /// Marker interface for MVC controllers participating in the DNN MVC pipeline. + /// + public interface IMvcController : IController + { + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleControllerBase.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleControllerBase.cs new file mode 100644 index 00000000000..d90066683c5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleControllerBase.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Controllers +{ + using System; + using System.Web.Mvc; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Web.MvcPipeline.Routing; + using DotNetNuke.Web.MvcPipeline.Utils; + + /// + /// Base controller for MVC module controllers, exposing common DNN context and services. + /// + public class ModuleControllerBase : DnnPageController, IMvcController + { + private readonly Lazy activeModule; + + /// + /// Initializes a new instance of the class. + /// + /// The dependency injection service provider. + public ModuleControllerBase(IServiceProvider dependencyProvider) : + base(dependencyProvider) + { + this.activeModule = new Lazy(this.InitModuleInfo); + } + + /// + /// Gets the current portal settings. + /// + public PortalSettings PortalSettings + { + get + { + return PortalController.Instance.GetCurrentPortalSettings(); + } + } + + /// + /// Gets the user information for the current user. + /// + public UserInfo UserInfo + { + get { return this.PortalSettings.UserInfo; } + } + + /// + /// Gets the active module associated with the current request. + /// + public ModuleInfo ActiveModule + { + get { return this.activeModule.Value; } + } + + private ModuleInfo InitModuleInfo() + { + return this.HttpContext.Request.FindModuleInfo(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleViewControllerBase.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleViewControllerBase.cs new file mode 100644 index 00000000000..48ee11ec39a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleViewControllerBase.cs @@ -0,0 +1,412 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Controllers +{ + using System; + using System.Collections; + using System.ComponentModel; + using System.IO; + using System.Threading; + using System.Web.Mvc; + + using DotNetNuke.Common; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Internal.SourceGenerators; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.MvcPipeline.Models; + using DotNetNuke.Web.MvcPipeline.Utils; + + /// + /// Base controller for MVC view controllers hosting classic module controls. + /// + public abstract class ModuleViewControllerBase : Controller, IMvcController + { + private string localResourceFile; + private ModuleInstanceContext moduleContext; + + /// + /// Initializes a new instance of the class. + /// + public ModuleViewControllerBase() + { + } + + /// + /// Gets a value indicating whether the current tab is a host (superuser) tab. + /// + public bool IsHostMenu + { + get + { + return Globals.IsHostTab(this.PortalSettings.ActiveTab.TabID); + } + } + + /// + /// Gets the current portal settings. + /// + public PortalSettings PortalSettings + { + get + { + return PortalController.Instance.GetCurrentPortalSettings(); + } + } + + /// + /// Gets a value indicating whether the EditMode property is used to determine whether the user is in the + /// Administrator role + /// Cache. + /// + public bool EditMode + { + get + { + return this.ModuleContext.EditMode; + } + } + + /// + /// Gets a value indicating whether the current module is editable for the user. + /// + public bool IsEditable + { + get + { + return this.ModuleContext.IsEditable; + } + } + + /// + /// Gets the portal identifier for the current module context. + /// + public int PortalId + { + get + { + return this.ModuleContext.PortalId; + } + } + + /// + /// Gets the tab identifier for the current module context. + /// + public int TabId + { + get + { + return this.ModuleContext.TabId; + } + } + + /// + /// Gets the user information for the current user. + /// + public UserInfo UserInfo + { + get + { + return this.PortalSettings.UserInfo; + } + } + + /// + /// Gets the identifier of the current user. + /// + public int UserId + { + get + { + return this.PortalSettings.UserId; + } + } + + /// + /// Gets the current portal alias information. + /// + public PortalAliasInfo PortalAlias + { + get + { + return this.PortalSettings.PortalAlias; + } + } + + /// + /// Gets the settings for the current module instance. + /// + public Hashtable Settings + { + get + { + return this.ModuleContext.Settings; + } + } + + /// Gets the Module Context for this control. + /// A ModuleInstanceContext. + public ModuleInstanceContext ModuleContext + { + get + { + if (this.moduleContext == null) + { + this.moduleContext = new ModuleInstanceContext(); + } + + return this.moduleContext; + } + } + + /// + /// Gets or sets the help URL associated with the module. + /// + public string HelpURL + { + get + { + return this.ModuleContext.HelpURL; + } + + set + { + this.ModuleContext.HelpURL = value; + } + } + + /// + /// Gets or sets the module configuration. + /// + public ModuleInfo ModuleConfiguration + { + get + { + return this.ModuleContext.Configuration; + } + + set + { + this.ModuleContext.Configuration = value; + } + } + + /// + /// Gets or sets the tab-module identifier for the current instance. + /// + public int TabModuleId + { + get + { + return this.ModuleContext.TabModuleId; + } + + set + { + this.ModuleContext.TabModuleId = value; + } + } + + /// + /// Gets or sets the module identifier for the current instance. + /// + public int ModuleId + { + get + { + return this.ModuleContext.ModuleId; + } + + set + { + this.ModuleContext.ModuleId = value; + } + } + + /// Gets or sets the local resource file for this control. + /// A String. + public string LocalResourceFile + { + get + { + string fileRoot; + if (string.IsNullOrEmpty(this.localResourceFile)) + { + fileRoot = "~/DesktopModules/" + this.FolderName + "/" + Localization.LocalResourceDirectory + "/" + this.ResourceName; + } + else + { + fileRoot = this.localResourceFile; + } + + return fileRoot; + } + + set + { + this.localResourceFile = value; + } + } + + /// + /// Gets the folder name of the associated desktop module. + /// + public virtual string FolderName + { + get + { + return this.ModuleContext.Configuration.DesktopModule.FolderName; + } + } + + /// + /// Gets the base name of the resource file used for localization. + /// + public virtual string ResourceName + { + get + { + return this.GetType().Name.Replace("ViewController", string.Empty); + } + } + + /// + /// Builds an edit URL for the current module instance. + /// + /// The edit URL. + public string EditUrl() + { + return this.ModuleContext.EditUrl(); + } + + /// + /// Builds an edit URL for the specified control key. + /// + /// The control key. + /// The edit URL. + public string EditUrl(string controlKey) + { + return this.ModuleContext.EditUrl(controlKey); + } + + /// + /// Builds an edit URL for the specified key and value. + /// + /// The query string key. + /// The query string value. + /// The edit URL. + public string EditUrl(string keyName, string keyValue) + { + return this.ModuleContext.EditUrl(keyName, keyValue); + } + + /// + /// Builds an edit URL for the specified key, value, and control key. + /// + /// The query string key. + /// The query string value. + /// The control key. + /// The edit URL. + public string EditUrl(string keyName, string keyValue, string controlKey) + { + return this.ModuleContext.EditUrl(keyName, keyValue, controlKey); + } + + /// + /// Builds an edit URL for the specified key, value, control key, and additional parameters. + /// + /// The query string key. + /// The query string value. + /// The control key. + /// Additional route parameters. + /// The edit URL. + public string EditUrl(string keyName, string keyValue, string controlKey, params string[] additionalParameters) + { + return this.ModuleContext.EditUrl(keyName, keyValue, controlKey, additionalParameters); + } + + /// + /// Builds an edit URL for the specified tab, control key, and additional parameters. + /// + /// The target tab identifier. + /// The control key. + /// A value indicating whether to perform a page redirect. + /// Additional route parameters. + /// The edit URL. + public string EditUrl(int tabID, string controlKey, bool pageRedirect, params string[] additionalParameters) + { + return this.ModuleContext.NavigateUrl(tabID, controlKey, pageRedirect, additionalParameters); + } + + /// + /// Gets the next available action identifier. + /// + /// The next action identifier. + public int GetNextActionID() + { + return this.ModuleContext.GetNextActionID(); + } + + /// + /// Gets the view model for the module view. + /// + /// The view model object. + public abstract object ViewModel(); + + /// + /// Gets the name of the view used to render the module. + /// + /// The view name. + public abstract string ViewName(); + + /// + /// Gets the localized string for the specified key. + /// + /// The localization key. + /// The localized string. + protected string LocalizeString(string key) + { + return Localization.GetString(key, this.LocalResourceFile); + } + + /// + /// Gets a localized string that is safe for use in JavaScript. + /// + /// The localization key. + /// The localized JavaScript-safe string. + protected string LocalizeSafeJsString(string key) + { + return Localization.GetSafeJSString(key, this.LocalResourceFile); + } + + /// + /// Invokes the module view for the specified input configuration. + /// + /// The control view model input. + /// The partial view result. + [ChildActionOnly] + public virtual ActionResult Invoke(ControlViewModel input) + { + this.moduleContext = new ModuleInstanceContext(); + var activeModule = ModuleController.Instance.GetModule(input.ModuleId, input.TabId, false); + + if (activeModule.ModuleControlId != input.ModuleControlId) + { + activeModule = activeModule.Clone(); + activeModule.ContainerPath = input.ContainerPath; + activeModule.ContainerSrc = input.ContainerSrc; + activeModule.ModuleControlId = input.ModuleControlId; + activeModule.PaneName = input.PanaName; + activeModule.IconFile = input.IconFile; + } + this.moduleContext.Configuration = activeModule; + var model = this.ViewModel(); + return this.PartialView(this.ViewName(), model); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/DnnMvcPipelineDependencyResolver.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/DnnMvcPipelineDependencyResolver.cs new file mode 100644 index 00000000000..2992b9321e6 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/DnnMvcPipelineDependencyResolver.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline +{ + using System; + using System.Collections.Generic; + using System.Web.Mvc; + + using DotNetNuke.Services.DependencyInjection; + using Microsoft.Extensions.DependencyInjection; + + /// + /// The implementation used in the + /// MVC Modules of DNN. + /// + internal class DnnMvcPipelineDependencyResolver : IDependencyResolver + { + private readonly IServiceProvider serviceProvider; + + /// Initializes a new instance of the class. + /// The service provider. + public DnnMvcPipelineDependencyResolver(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + /// Returns the specified service from the scope. + /// + /// The service to be retrieved. + /// + /// + /// The retrieved service. + /// + public object GetService(Type serviceType) + { + var accessor = this.serviceProvider.GetRequiredService(); + var scope = accessor.GetScope(); + if (scope != null) + { + return scope.ServiceProvider.GetService(serviceType); + } + + throw new InvalidOperationException("IServiceScope not provided"); + } + + /// Returns the specified services from the scope. + /// + /// The service to be retrieved. + /// + /// + /// The retrieved service. + /// + public IEnumerable GetServices(Type serviceType) + { + var accessor = this.serviceProvider.GetRequiredService(); + var scope = accessor.GetScope(); + if (scope != null) + { + return scope.ServiceProvider.GetServices(serviceType); + } + + throw new InvalidOperationException("IServiceScope not provided"); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/DotNetNuke.Web.MvcPipeline.csproj b/DNN Platform/DotNetNuke.Web.MvcPipeline/DotNetNuke.Web.MvcPipeline.csproj new file mode 100644 index 00000000000..0a0532b905c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/DotNetNuke.Web.MvcPipeline.csproj @@ -0,0 +1,71 @@ + + + DotNetNuke.Web.MvcPipeline + net48 + true + latest + bin + false + false + Sacha Trauwaen + Dnn + MvcPipeline + 2025 + MvcPipeline + 0.0.1.0 + 0.0.1.0 + DNN MVC-pipeline project + en-US + + Library + + Portable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + stylecop.json + + + + + + + + + + + + + + diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/AccesDeniedException.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/AccesDeniedException.cs new file mode 100644 index 00000000000..5eeb287e380 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/AccesDeniedException.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Exceptions +{ + using System; + using System.Runtime.Serialization; + + /// + /// Exception thrown when access to an MVC page or resource is denied. + /// + [Serializable] + public class AccesDeniedException : MvcPageException + { + /// + /// Initializes a new instance of the class. + /// + public AccesDeniedException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message and redirect URL. + /// + /// The error message. + /// The URL to which the user should be redirected. + public AccesDeniedException(string message, string redirectUrl) + : base(message, redirectUrl) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message. + /// The inner exception. + public AccesDeniedException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The serialization information. + /// The streaming context. + protected AccesDeniedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/DisabledPageException.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/DisabledPageException.cs new file mode 100644 index 00000000000..46a207f3a13 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/DisabledPageException.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Exceptions +{ + using System; + using System.Runtime.Serialization; + + /// + /// Exception thrown when an MVC page is disabled and cannot be accessed. + /// + [Serializable] + public class DisabledPageException : MvcPageException + { + /// + /// Initializes a new instance of the class. + /// + public DisabledPageException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message. + public DisabledPageException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and redirect URL. + /// + /// The error message. + /// The URL to which the user should be redirected. + public DisabledPageException(string message, string redirectUrl) + : base(message, redirectUrl) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message. + /// The inner exception. + public DisabledPageException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The serialization information. + /// The streaming context. + protected DisabledPageException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/MvcPageException.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/MvcPageException.cs new file mode 100644 index 00000000000..f3236994861 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/MvcPageException.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Exceptions +{ + using System; + using System.Runtime.Serialization; + + /// + /// Base exception type for MVC page errors within the MVC pipeline. + /// + public class MvcPageException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public MvcPageException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message. + public MvcPageException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and redirect URL. + /// + /// The error message. + /// The URL to which the user should be redirected. + public MvcPageException(string message, string redirectUrl) + : base(message) + { + this.RedirectUrl = redirectUrl; + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message. + /// The inner exception. + public MvcPageException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The serialization information. + /// The streaming context. + protected MvcPageException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + /// + /// Gets the URL to which the user should be redirected. + /// + public string RedirectUrl { get; private set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/NotFoundException.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/NotFoundException.cs new file mode 100644 index 00000000000..336d694c75d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/NotFoundException.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Exceptions +{ + using System; + using System.Runtime.Serialization; + + /// + /// Exception thrown when an MVC page or resource cannot be found. + /// + [Serializable] + public class NotFoundException : MvcPageException + { + /// + /// Initializes a new instance of the class. + /// + public NotFoundException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message. + public NotFoundException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and redirect URL. + /// + /// The error message. + /// The URL to which the user should be redirected. + public NotFoundException(string message, string redirectUrl) + : base(message, redirectUrl) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message. + /// The inner exception. + public NotFoundException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The serialization information. + /// The streaming context. + protected NotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Extensions/StartupExtensions.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Extensions/StartupExtensions.cs new file mode 100644 index 00000000000..a54f25bf0d2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Extensions/StartupExtensions.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.Mvc.Extensions +{ + using System.Linq; + + using DotNetNuke.DependencyInjection.Extensions; + using DotNetNuke.Instrumentation; + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + + /// Adds DNN MVC Controller Specific startup extensions to simplify the Class. + public static class StartupExtensions + { + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(StartupExtensions)); + + /// Configures all of the 's to be used with the Service Collection for Dependency Injection. + /// Service Collection used to registering services in the container. + public static void AddMvcControllers(this IServiceCollection services) + { + var allTypes = TypeExtensions.SafeGetTypes(); + allTypes.LogOtherExceptions(Logger); + + var mvcControllerTypes = allTypes.Types + .Where( + type => typeof(IMvcController).IsAssignableFrom(type) && + type is { IsClass: true, IsAbstract: false }); + foreach (var controller in mvcControllerTypes) + { + services.TryAddTransient(controller); + } + + var mvcControlTypes = allTypes.Types + .Where( + type => typeof(IMvcModuleControl).IsAssignableFrom(type) && + type is { IsClass: true, IsAbstract: false }); + foreach (var controller in mvcControlTypes) + { + services.TryAddTransient(controller); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServiceFrameworkInternals.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServiceFrameworkInternals.cs new file mode 100644 index 00000000000..4485db4cb6c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServiceFrameworkInternals.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + using System.Web.Mvc; + + /// + /// Internal services framework contract used by the MVC pipeline to manage script and anti-forgery support. + /// + internal interface IMvcServiceFrameworkInternals + { + /// + /// Gets a value indicating whether AJAX anti-forgery support has been requested for the current request. + /// + bool IsAjaxAntiForgerySupportRequired { get; } + + /// + /// Gets a value indicating whether AJAX script support has been requested for the current request. + /// + bool IsAjaxScriptSupportRequired { get; } + + /// + /// Registers services-framework AJAX anti-forgery scripts and variables on the page. + /// + void RegisterAjaxAntiForgery(); + + /// + /// Registers services-framework AJAX scripts and variables on the page. + /// + void RegisterAjaxScript(); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServicesFramework.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServicesFramework.cs new file mode 100644 index 00000000000..a06af9ebb8d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServicesFramework.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + /// + /// Do not implement. This interface is only implemented by the DotNetNuke core framework. Outside the framework it should be used as a type and for unit test purposes only. + /// There is no guarantee that this interface will not change. + /// + public interface IMvcServicesFramework + { + /// + /// Requests that anti-forgery tokens be included in the current page for AJAX calls. + /// + void RequestAjaxAntiForgerySupport(); + + /// + /// Requests that the services framework AJAX scripts be included in the current page. + /// + void RequestAjaxScriptSupport(); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/JavascriptLibraries/MvcJavaScript.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/JavascriptLibraries/MvcJavaScript.cs new file mode 100644 index 00000000000..1b6f4c808cf --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/JavascriptLibraries/MvcJavaScript.cs @@ -0,0 +1,331 @@ +namespace DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Controllers; + using DotNetNuke.Entities.Host; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Services.Installer.Packages; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Log.EventLog; + using DotNetNuke.UI.Utilities; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + using Microsoft.Extensions.DependencyInjection; + + public class MvcJavaScript + { + private const string ScriptPrefix = "JSL."; + private const string LegacyPrefix = "LEGACY."; + + /// Initializes a new instance of the class. + protected MvcJavaScript() + { + } + + /// method is called once per page event cycle and will load all scripts requested during that page processing cycle. + public static void Register() + { + IEnumerable scripts = GetScriptVersions(); + IEnumerable finalScripts = ResolveVersionConflicts(scripts); + foreach (JavaScriptLibrary jsl in finalScripts) + { + if (jsl.LibraryName != "jQuery-Migrate") + { + RegisterScript(jsl); + } + } + } + + public static void RegisterClientReference(ClientAPI.ClientNamespaceReferences reference) + { + var controller = GetClientResourcesController(); + + switch (reference) + { + case ClientAPI.ClientNamespaceReferences.dnn: + case ClientAPI.ClientNamespaceReferences.dnn_dom: + if (HttpContextSource.Current.Items.Contains(LegacyPrefix + "dnn.js")) + { + break; + } + + // MvcClientResourceManager.RegisterScript(page, ClientAPI.ScriptPath + "MicrosoftAjax.js", 10); + controller.CreateScript(ClientAPI.ScriptPath + "mvc.js") + .SetPriority(11) + .Register(); + controller.CreateScript(ClientAPI.ScriptPath + "dnn.js") + .SetPriority(12) + .Register(); + + HttpContextSource.Current.Items.Add(LegacyPrefix + "dnn.js", true); + + if (!ClientAPI.BrowserSupportsFunctionality(ClientAPI.ClientFunctionality.SingleCharDelimiters)) + { + MvcClientAPI.RegisterClientVariable("__scdoff", "1", true); + } + + if (!ClientAPI.UseExternalScripts) + { + MvcClientAPI.RegisterEmbeddedResource("dnn.scripts.js", typeof(ClientAPI)); + } + + break; + case ClientAPI.ClientNamespaceReferences.dnn_dom_positioning: + RegisterClientReference(ClientAPI.ClientNamespaceReferences.dnn); + controller.CreateScript(ClientAPI.ScriptPath + "dnn.dom.positioning.js") + .SetPriority(13) + .Register(); + break; + } + } + + private static IEnumerable ResolveVersionConflicts(IEnumerable scripts) + { + var finalScripts = new List(); + foreach (string libraryId in scripts) + { + var processingLibrary = JavaScriptLibraryController.Instance.GetLibrary(l => l.JavaScriptLibraryID.ToString(CultureInfo.InvariantCulture) == libraryId); + + var existingLatestLibrary = finalScripts.FindAll(lib => lib.LibraryName.Equals(processingLibrary.LibraryName, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(l => l.Version) + .SingleOrDefault(); + if (existingLatestLibrary != null) + { + // determine previous registration for same JSL + if (existingLatestLibrary.Version > processingLibrary.Version) + { + // skip new library & log + var collisionText = string.Format( + CultureInfo.CurrentCulture, + "{0}-{1} -> {2}-{3}", + existingLatestLibrary.LibraryName, + existingLatestLibrary.Version, + processingLibrary.LibraryName, + processingLibrary.Version); + LogCollision(collisionText); + } + else if (existingLatestLibrary.Version != processingLibrary.Version) + { + finalScripts.Remove(existingLatestLibrary); + finalScripts.Add(processingLibrary); + } + } + else + { + finalScripts.Add(processingLibrary); + } + } + + return finalScripts; + } + + private static JavaScriptLibrary GetHighestVersionLibrary(string jsname) + { + if (DotNetNuke.Common.Globals.Status == DotNetNuke.Common.Globals.UpgradeStatus.Install) + { + // if in install process, then do not use JSL but all use the legacy versions. + return null; + } + + try + { + return JavaScriptLibraryController.Instance.GetLibraries(l => l.LibraryName.Equals(jsname, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(l => l.Version) + .FirstOrDefault(); + } + catch (Exception) + { + // no library found (install or upgrade) + return null; + } + } + + private static string GetScriptPath(JavaScriptLibrary js, HttpRequestBase request) + { + if (Host.CdnEnabled) + { + // load custom CDN path setting + var customCdn = HostController.Instance.GetString("CustomCDN_" + js.LibraryName); + if (!string.IsNullOrEmpty(customCdn)) + { + return customCdn; + } + + // cdn enabled but jsl does not have one defined + if (!string.IsNullOrEmpty(js.CDNPath)) + { + var cdnPath = js.CDNPath; + if (cdnPath.StartsWith("//")) + { + cdnPath = $"{(UrlUtils.IsSecureConnectionOrSslOffload(request) ? "https" : "http")}:{cdnPath}"; + } + + return cdnPath; + } + } + + return "~/Resources/libraries/" + js.LibraryName + "/" + DotNetNuke.Common.Globals.FormatVersion(js.Version, "00", 3, "_") + "/" + js.FileName; + } + + private static IEnumerable GetScriptVersions() + { + List orderedScripts = (from object item in HttpContextSource.Current.Items.Keys + where item.ToString().StartsWith(ScriptPrefix) + select item.ToString().Substring(4)).ToList(); + orderedScripts.Sort(); + List finalScripts = orderedScripts.ToList(); + foreach (string libraryId in orderedScripts) + { + // find dependencies + var library = JavaScriptLibraryController.Instance.GetLibrary(l => l.JavaScriptLibraryID.ToString() == libraryId); + if (library == null) + { + continue; + } + + foreach (var dependencyLibrary in GetAllDependencies(library).Distinct()) + { + if (HttpContextSource.Current.Items[ScriptPrefix + "." + dependencyLibrary.JavaScriptLibraryID] == null) + { + finalScripts.Add(dependencyLibrary.JavaScriptLibraryID.ToString()); + } + } + } + + return finalScripts; + } + + private static IEnumerable GetAllDependencies(JavaScriptLibrary library) + { + var package = PackageController.Instance.GetExtensionPackage(Null.NullInteger, p => p.PackageID == library.PackageID); + foreach (var dependency in package.Dependencies) + { + var dependencyLibrary = GetHighestVersionLibrary(dependency.PackageName); + yield return dependencyLibrary; + + foreach (var childDependency in GetAllDependencies(dependencyLibrary)) + { + yield return childDependency; + } + } + } + + private static void LogCollision(string collisionText) + { + // need to log an event + EventLogController.Instance.AddLog( + "Javascript Libraries", + collisionText, + PortalController.Instance.GetCurrentPortalSettings(), + UserController.Instance.GetCurrentUserInfo().UserID, + EventLogController.EventLogType.SCRIPT_COLLISION); + string strMessage = Localization.GetString("ScriptCollision", Localization.SharedResourceFile); + /* + var page = HttpContextSource.Current.Handler as Page; + if (page != null) + { + Skin.AddPageMessage(page, string.Empty, strMessage, ModuleMessage.ModuleMessageType.YellowWarning); + } + */ + } + + private static void RegisterScript(JavaScriptLibrary jsl) + { + if (string.IsNullOrEmpty(jsl.FileName)) + { + return; + } + + var controller = GetClientResourcesController(); + controller.CreateScript(GetScriptPath(jsl, HttpContextSource.Current?.Request)) + .SetPriority(GetFileOrder(jsl)) + .SetProvider(GetProvider(jsl)) + .SetNameAndVersion(jsl.LibraryName, jsl.Version.ToString(3), false) + .Register(); + + /* + if (Host.CdnEnabled && !string.IsNullOrEmpty(jsl.ObjectName)) + { + string pagePortion; + switch (jsl.PreferredScriptLocation) + { + case ScriptLocation.PageHead: + + pagePortion = "ClientDependencyHeadJs"; + break; + case ScriptLocation.BodyBottom: + pagePortion = "ClientResourcesFormBottom"; + break; + case ScriptLocation.BodyTop: + pagePortion = "BodySCRIPTS"; + break; + default: + pagePortion = "BodySCRIPTS"; + break; + } + + Control scriptloader = page.FindControl(pagePortion); + var fallback = new DnnJsIncludeFallback(jsl.ObjectName, VirtualPathUtility.ToAbsolute("~/Resources/libraries/" + jsl.LibraryName + "/" + Globals.FormatVersion(jsl.Version, "00", 3, "_") + "/" + jsl.FileName)); + if (scriptloader != null) + { + // add the fallback control after script loader. + var index = scriptloader.Parent.Controls.IndexOf(scriptloader); + scriptloader.Parent.Controls.AddAt(index + 1, fallback); + } + } + */ + } + + private static string GetProvider(JavaScriptLibrary jsl) + { + if (jsl.PreferredScriptLocation== ScriptLocation.PageHead) + { + return "DnnPageHeaderProvider"; + } + else if (jsl.PreferredScriptLocation == ScriptLocation.PageHead) + { + return "DnnBodyProvider"; + } + else if (jsl.PreferredScriptLocation == ScriptLocation.PageHead) + { + return "DnnFormBottomProvider"; + } + return string.Empty; + } + + private static int GetFileOrder(JavaScriptLibrary jsl) + { + switch (jsl.LibraryName) + { + case CommonJs.jQuery: + return (int)FileOrder.Js.jQuery; + case CommonJs.jQueryMigrate: + return (int)FileOrder.Js.jQueryMigrate; + case CommonJs.jQueryUI: + return (int)FileOrder.Js.jQueryUI; + case CommonJs.HoverIntent: + return (int)FileOrder.Js.HoverIntent; + default: + return jsl.PackageID + (int)FileOrder.Js.DefaultPriority; + } + } + + private static IClientResourceController GetClientResourcesController() + { + var serviceProvider = DotNetNuke.Common.Globals.GetCurrentServiceProvider(); + return serviceProvider.GetRequiredService(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFramework.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFramework.cs new file mode 100644 index 00000000000..3d19981e92b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFramework.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + using System; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.Framework; + + /// Enables modules to support Services Framework features. + public class MvcServicesFramework : ServiceLocator + { + /// + /// Gets the services framework root path for the current portal alias. + /// + /// The root path (including application path if present). + public static string GetServiceFrameworkRoot() + { + var portalSettings = PortalSettings.Current; + if (portalSettings == null) + { + return string.Empty; + } + + var path = portalSettings.PortalAlias.HTTPAlias; + var index = path.IndexOf('/'); + if (index > 0) + { + path = path.Substring(index); + if (!path.EndsWith("/")) + { + path += "/"; + } + } + else + { + path = "/"; + } + + return path; + } + + /// + protected override Func GetFactory() + { + return () => new MvcServicesFrameworkImpl(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkImpl.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkImpl.cs new file mode 100644 index 00000000000..7c45d866271 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkImpl.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + using System.Globalization; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.UI; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Framework; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Services.ClientCapability; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.UI.Utilities; + using DotNetNuke.Web.Client.ClientResourceManagement; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Default implementation of the MVC services framework used to coordinate AJAX scripts and anti-forgery support. + /// + internal class MvcServicesFrameworkImpl : IMvcServicesFramework, IMvcServiceFrameworkInternals + { + private const string AntiForgeryKey = "dnnAntiForgeryRequested"; + private const string ScriptKey = "dnnSFAjaxScriptRequested"; + + /// + public bool IsAjaxAntiForgerySupportRequired + { + get { return CheckKey(AntiForgeryKey); } + } + + /// + public bool IsAjaxScriptSupportRequired + { + get { return CheckKey(ScriptKey); } + } + + /// + public void RequestAjaxAntiForgerySupport() + { + this.RequestAjaxScriptSupport(); + SetKey(AntiForgeryKey); + } + + /// + public void RequestAjaxScriptSupport() + { + JavaScript.RequestRegistration(CommonJs.jQuery); + SetKey(ScriptKey); + } + + /// + public void RegisterAjaxScript() + { + var path = ServicesFramework.GetServiceFrameworkRoot(); + if (string.IsNullOrEmpty(path)) + { + return; + } + + MvcJavaScript.RegisterClientReference(ClientAPI.ClientNamespaceReferences.dnn); + MvcClientAPI.RegisterClientVariable("sf_siteRoot", path, /*overwrite*/ true); + MvcClientAPI.RegisterClientVariable("sf_tabId", PortalSettings.Current.ActiveTab.TabID.ToString(CultureInfo.InvariantCulture), /*overwrite*/ true); + + string scriptPath; + if (HttpContextSource.Current.IsDebuggingEnabled) + { + scriptPath = "~/js/Debug/dnn.servicesframework.js"; + } + else + { + scriptPath = "~/js/dnn.servicesframework.js"; + } + + var controller = GetClientResourcesController(); + controller.RegisterScript(scriptPath); + } + + private static void SetKey(string key) + { + HttpContextSource.Current.Items[key] = true; + } + + private static bool CheckKey(string antiForgeryKey) + { + return HttpContextSource.Current.Items.Contains(antiForgeryKey); + } + + /// + public void RegisterAjaxAntiForgery() + { + var path = ServicesFramework.GetServiceFrameworkRoot(); + if (string.IsNullOrEmpty(path)) + { + return; + } + + MvcJavaScript.RegisterClientReference(ClientAPI.ClientNamespaceReferences.dnn); + MvcClientAPI.RegisterClientVariable("sf_siteRoot", path, /*overwrite*/ true); + MvcClientAPI.RegisterClientVariable("sf_tabId", PortalSettings.Current.ActiveTab.TabID.ToString(CultureInfo.InvariantCulture), /*overwrite*/ true); + + string scriptPath; + if (HttpContextSource.Current.IsDebuggingEnabled) + { + scriptPath = "~/js/Debug/dnn.servicesframework.js"; + } + else + { + scriptPath = "~/js/dnn.servicesframework.js"; + } + GetClientResourcesController() + .CreateScript(scriptPath) + .Register(); + } + + private static IClientResourceController GetClientResourcesController() + { + var serviceProvider = DotNetNuke.Common.Globals.GetCurrentServiceProvider(); + return serviceProvider.GetRequiredService(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkInternal.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkInternal.cs new file mode 100644 index 00000000000..32773a9b73c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkInternal.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + using System; + + using DotNetNuke.Framework; + + /// + /// Service locator wrapper for resolving MVC services framework internal implementation. + /// + internal class MvcServicesFrameworkInternal : ServiceLocator + { + /// + protected override Func GetFactory() + { + return () => new MvcServicesFrameworkImpl(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/HtmlHelpers.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/HtmlHelpers.cs new file mode 100644 index 00000000000..6f004d09cd9 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/HtmlHelpers.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline +{ + using System; + using System.IO; + using System.Linq; + using System.Runtime.CompilerServices; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Pages; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Framework; + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.Framework; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + using Microsoft.Extensions.DependencyInjection; + + /// + /// HTML helper extensions for rendering MVC pipeline module controls and related utilities. + /// + public static partial class HtmlHelpers + { + /// + /// Renders an MVC module control and configures page contributions when supported. + /// + /// The MVC HTML helper. + /// The MVC module control to render. + /// An HTML string representing the rendered module control. + public static IHtmlString Control(this HtmlHelper htmlHelper, IMvcModuleControl moduleControl) + { + if (moduleControl is IPageContributor) + { + var pageContributor = (IPageContributor)moduleControl; + pageContributor.ConfigurePage(new PageConfigurationContext(Common.Globals.GetCurrentServiceProvider())); + } + + return moduleControl.Html(htmlHelper); + } + + /// + /// Creates and renders an MVC module control for the specified module and control source. + /// + /// The MVC HTML helper. + /// The control source path. + /// The module configuration. + /// An HTML string representing the rendered module control. + public static IHtmlString Control(this HtmlHelper htmlHelper, string controlSrc, ModuleInfo module) + { + var moduleControl = ModuleControlFactory.CreateModuleControl(module, controlSrc); + if (moduleControl is IPageContributor) + { + var pageContributor = (IPageContributor)moduleControl; + pageContributor.ConfigurePage(new PageConfigurationContext(Common.Globals.GetCurrentServiceProvider())); + } + + return moduleControl.Html(htmlHelper); + } + + /// + /// Creates and renders the default MVC module control for the specified module. + /// + /// The MVC HTML helper. + /// The module configuration. + /// An HTML string representing the rendered module control. + public static IHtmlString Control(this HtmlHelper htmlHelper, ModuleInfo module) + { + return htmlHelper.Control(module.ModuleControl.ControlSrc, module); + } + + /// + /// Renders the Content Security Policy nonce for the current request, when available. + /// + /// The MVC HTML helper. + /// The CSP nonce wrapped as an HTML string, or an empty string when CSP is not configured. + public static IHtmlString CspNonce(this HtmlHelper htmlHelper) + { + // TODO: CSP - implement nonce support + // return new MvcHtmlString(htmlHelper.ViewContext.HttpContext.Items["CSP-NONCE"].ToString()); + return new MvcHtmlString(string.Empty); + } + + /// + /// Registers the MVC AJAX support script when required by the services framework. + /// + /// The MVC HTML helper. + /// An empty HTML string. + public static IHtmlString RegisterAjaxScriptIfRequired(this HtmlHelper htmlHelper) + { + if (MvcServicesFrameworkInternal.Instance.IsAjaxScriptSupportRequired) + { + MvcServicesFrameworkInternal.Instance.RegisterAjaxScript(); + } + + return new MvcHtmlString(string.Empty); + } + + /// + /// Renders an anti-forgery token when AJAX anti-forgery support is required. + /// + /// The MVC HTML helper. + /// + /// The rendered anti-forgery token, or an empty HTML string when anti-forgery support is not required. + /// + public static IHtmlString AntiForgeryIfRequired(this HtmlHelper htmlHelper) + { + if (ServicesFrameworkInternal.Instance.IsAjaxAntiForgerySupportRequired) + { + return AntiForgery.GetHtml(); + } + + return new MvcHtmlString(string.Empty); + } + + /// + /// Gets the dependency injection service provider from the current . + /// + /// The MVC HTML helper. + /// The service provider associated with the current controller. + /// + /// Thrown when the current controller is not a . + /// + internal static IServiceProvider GetDependencyProvider(HtmlHelper htmlHelper) + { + var controller = htmlHelper.ViewContext.Controller as DnnPageController; + + if (controller == null) + { + throw new InvalidOperationException("The HtmlHelper can only be used from DnnPageController"); + } + + return controller.DependencyProvider; + } + + /// + /// Gets the client resource controller from the current dependency injection provider. + /// + /// The MVC HTML helper. + /// The client resource controller instance. + internal static IClientResourceController GetClientResourcesController(HtmlHelper htmlHelper) + { + return GetDependencyProvider(htmlHelper).GetRequiredService(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ContainerModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ContainerModelFactory.cs new file mode 100644 index 00000000000..be32311db8a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ContainerModelFactory.cs @@ -0,0 +1,237 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using System; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.Client; + using DotNetNuke.Web.MvcPipeline.Models; + using DotNetNuke.Services.ClientDependency; + + /// + /// Builds and configures instances for module containers. + /// + public class ContainerModelFactory : IContainerModelFactory + { + private readonly IClientResourceController clientResourceController; + + /// + /// Initializes a new instance of the class. + /// + /// The client resource controller. + public ContainerModelFactory(IClientResourceController clientResourceController) + { + this.clientResourceController = clientResourceController; + } + + /// + public ContainerModel CreateContainerModel(ModuleInfo configuration, PortalSettings portalSettings, string containerSrc) + { + var container = new ContainerModel(configuration, portalSettings); + container.ContainerSrc = containerSrc; + container = this.ProcessModule(container, portalSettings); + return container; + } + + private ContainerModel ProcessModule(ContainerModel container, PortalSettings portalSettings) + { + // Process Content Pane Attributes + container = this.ProcessContentPane(container, portalSettings); + + // Process Module Header + container = this.ProcessHeader(container); + + // Process Module Footer + container = this.ProcessFooter(container); + + // Add Module Stylesheets + container = this.ProcessStylesheets(container, container.ModuleHost != null); + + return container; + } + + private ContainerModel ProcessContentPane(ContainerModel container, PortalSettings portalSettings) + { + container = this.SetAlignment(container); + + container = this.SetBackground(container); + + container = this.SetBorder(container); + + // display visual indicator if module is only visible to administrators + var viewRoles = container.ModuleConfiguration.InheritViewPermissions + ? TabPermissionController.GetTabPermissions(container.ModuleConfiguration.TabID, container.ModuleConfiguration.PortalID).ToString("VIEW") + : container.ModuleConfiguration.ModulePermissions.ToString("VIEW"); + + var pageEditRoles = TabPermissionController.GetTabPermissions(container.ModuleConfiguration.TabID, container.ModuleConfiguration.PortalID).ToString("EDIT"); + var moduleEditRoles = container.ModuleConfiguration.ModulePermissions.ToString("EDIT"); + + viewRoles = viewRoles.Replace(";", string.Empty).Trim().ToLowerInvariant(); + pageEditRoles = pageEditRoles.Replace(";", string.Empty).Trim().ToLowerInvariant(); + moduleEditRoles = moduleEditRoles.Replace(";", string.Empty).Trim().ToLowerInvariant(); + + var showMessage = false; + var adminMessage = Null.NullString; + if (viewRoles.Equals(portalSettings.AdministratorRoleName, StringComparison.InvariantCultureIgnoreCase) + && (moduleEditRoles.Equals(portalSettings.AdministratorRoleName, StringComparison.InvariantCultureIgnoreCase) + || string.IsNullOrEmpty(moduleEditRoles)) + && pageEditRoles.Equals(portalSettings.AdministratorRoleName, StringComparison.InvariantCultureIgnoreCase)) + { + adminMessage = Localization.GetString("ModuleVisibleAdministrator.Text"); + showMessage = !container.ModuleConfiguration.HideAdminBorder && !Globals.IsAdminControl(); + } + + if (container.ModuleConfiguration.StartDate >= DateTime.Now) + { + adminMessage = string.Format(Localization.GetString("ModuleEffective.Text"), container.ModuleConfiguration.StartDate); + showMessage = !Globals.IsAdminControl(); + } + + if (container.ModuleConfiguration.EndDate <= DateTime.Now) + { + adminMessage = string.Format(Localization.GetString("ModuleExpired.Text"), container.ModuleConfiguration.EndDate); + showMessage = !Globals.IsAdminControl(); + } + + if (showMessage) + { + container = this.AddAdministratorOnlyHighlighting(container, adminMessage); + } + + return container; + } + + /// ProcessFooter adds an optional footer (and an End_Module comment).. + private ContainerModel ProcessFooter(ContainerModel container) + { + // inject the footer + if (!string.IsNullOrEmpty(container.ModuleConfiguration.Footer)) + { + container.Footer = container.ModuleConfiguration.Footer; + } + + // inject an end comment around the module content + if (!Globals.IsAdminControl()) + { + // this.ContentPane.Controls.Add(new LiteralControl("")); + } + + return container; + } + + /// ProcessHeader adds an optional header (and a Start_Module_ comment).. + private ContainerModel ProcessHeader(ContainerModel container) + { + if (!Globals.IsAdminControl()) + { + // inject a start comment around the module content + // this.ContentPane.Controls.Add(new LiteralControl("")); + } + + // inject the header + if (!string.IsNullOrEmpty(container.ModuleConfiguration.Header)) + { + container.Header = container.ModuleConfiguration.Header; + } + + return container; + } + + /// + /// ProcessStylesheets processes the Module and Container stylesheets and adds + /// them to the Page. + /// + private ContainerModel ProcessStylesheets(ContainerModel container, bool includeModuleCss) + { + // MvcClientResourceManager.RegisterStyleSheet(this.Page.ControllerContext, container.ContainerPath + "container.css", FileOrder.Css.ContainerCss); + //container.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = container.ContainerPath + "container.css", FileOrder = FileOrder.Css.ContainerCss }); + this.clientResourceController.RegisterStylesheet(container.ContainerPath + "container.css" , DotNetNuke.Abstractions.ClientResources.FileOrder.Css.ContainerCss, true); + + if (!string.IsNullOrEmpty(container.ContainerSrc)) + { + // MvcClientResourceManager.RegisterStyleSheet(this.Page.ControllerContext, container.ContainerSrc.Replace(".ascx", ".css"), FileOrder.Css.SpecificContainerCss); + //container.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = container.ContainerSrc.Replace(".ascx", ".css"), FileOrder = FileOrder.Css.SpecificContainerCss }); + this.clientResourceController.RegisterStylesheet(container.ContainerSrc.Replace(".ascx", ".css"), DotNetNuke.Abstractions.ClientResources.FileOrder.Css.SpecificContainerCss, true); + } + // process the base class module properties + if (includeModuleCss) + { + var controlSrc = container.ModuleConfiguration.ModuleControl.ControlSrc; + var folderName = container.ModuleConfiguration.DesktopModule.FolderName; + + var stylesheet = string.Empty; + if (string.IsNullOrEmpty(folderName) == false) + { + if (controlSrc.EndsWith(".mvc")) + { + stylesheet = Globals.ApplicationPath + "/DesktopModules/MVC/" + folderName.Replace("\\", "/") + "/module.css"; + } + else + { + stylesheet = Globals.ApplicationPath + "/DesktopModules/" + folderName.Replace("\\", "/") + "/module.css"; + } + + // MvcClientResourceManager.RegisterStyleSheet(this.Page.ControllerContext, stylesheet, FileOrder.Css.ModuleCss); + //container.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = stylesheet, FileOrder = FileOrder.Css.ModuleCss }); + this.clientResourceController.RegisterStylesheet(stylesheet, DotNetNuke.Abstractions.ClientResources.FileOrder.Css.ModuleCss, true); + } + + var ix = controlSrc.LastIndexOf("/", StringComparison.Ordinal); + if (ix >= 0) + { + stylesheet = Globals.ApplicationPath + "/" + controlSrc.Substring(0, ix + 1) + "module.css"; + + // MvcClientResourceManager.RegisterStyleSheet(this.Page.ControllerContext, stylesheet, FileOrder.Css.ModuleCss); + //container.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = stylesheet, FileOrder = FileOrder.Css.ModuleCss }); + this.clientResourceController.RegisterStylesheet(stylesheet, DotNetNuke.Abstractions.ClientResources.FileOrder.Css.ModuleCss, true); + } + } + + return container; + } + + private ContainerModel SetAlignment(ContainerModel container) + { + if (!string.IsNullOrEmpty(container.ModuleConfiguration.Alignment)) + { + container.ContentPaneCssClass += " DNNAlign" + container.ModuleConfiguration.Alignment.ToLowerInvariant(); + } + + return container; + } + + private ContainerModel SetBackground(ContainerModel container) + { + if (!string.IsNullOrEmpty(container.ModuleConfiguration.Color)) + { + container.ContentPaneStyle += "background-color:" + container.ModuleConfiguration.Color + ";"; + } + + return container; + } + + private ContainerModel SetBorder(ContainerModel container) + { + if (!string.IsNullOrEmpty(container.ModuleConfiguration.Border)) + { + container.ContentPaneStyle += "border:" + string.Format("{0}px #000000 solid", container.ModuleConfiguration.Border) + ";"; + } + + return container; + } + + private ContainerModel AddAdministratorOnlyHighlighting(ContainerModel container, string message) + { + // this.ContentPane.Controls.Add(new LiteralControl(string.Format("
{0}
", message))); + return container; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IContainerModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IContainerModelFactory.cs new file mode 100644 index 00000000000..fd6cba3b7ea --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IContainerModelFactory.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Creates container models used to render modules within a pane. + /// + public interface IContainerModelFactory + { + /// + /// Creates a container model for the specified module configuration and portal settings. + /// + /// The module configuration. + /// The current portal settings. + /// The container source path. + /// The created . + ContainerModel CreateContainerModel(ModuleInfo configuration, PortalSettings portalSettings, string containerSrc); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPageModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPageModelFactory.cs new file mode 100644 index 00000000000..f08b197a6b3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPageModelFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Creates instances for DNN MVC pages. + /// + public interface IPageModelFactory + { + /// + /// Creates a page model for the specified page controller. + /// + /// The DNN MVC page controller. + /// A populated . + PageModel CreatePageModel(DnnPageController page); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPaneModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPaneModelFactory.cs new file mode 100644 index 00000000000..343a8c4665e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPaneModelFactory.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Creates and processes pane models used to host modules within a skin. + /// + public interface IPaneModelFactory + { + /// + /// Creates a new pane with the specified name. + /// + /// The name of the pane. + /// The created . + PaneModel CreatePane(string name); + + /// + /// Injects a module into the specified pane. + /// + /// The target pane. + /// The module to inject. + /// The current portal settings. + /// The updated . + PaneModel InjectModule(PaneModel pane, ModuleInfo module, PortalSettings portalSettings); + + /// + /// Applies layout and behavior rules to the specified pane. + /// + /// The pane to process. + /// The processed . + PaneModel ProcessPane(PaneModel pane); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ISkinModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ISkinModelFactory.cs new file mode 100644 index 00000000000..872315a72ee --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ISkinModelFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Creates instances for a given DNN MVC page. + /// + public interface ISkinModelFactory + { + /// + /// Creates a skin model for the specified page controller. + /// + /// The DNN MVC page controller. + /// A populated . + SkinModel CreateSkinModel(DnnPageController page); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PageModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PageModelFactory.cs new file mode 100644 index 00000000000..e98c69f4df2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PageModelFactory.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using System; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading; + using System.Web; + using System.Web.Helpers; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.Application; + using DotNetNuke.Abstractions.Pages; + using DotNetNuke.Application; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Host; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Services.FileSystem; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Personalization; + using DotNetNuke.UI.Internals; + using DotNetNuke.UI.Modules; + using DotNetNuke.UI.Skins; + using DotNetNuke.Web.Client; + using DotNetNuke.Web.Client.ClientResourceManagement; + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Builds instances from DNN portal and tab state for the MVC pipeline. + /// + public class PageModelFactory : IPageModelFactory + { + private static readonly Regex HeaderTextRegex = new Regex( + "])+name=('|\")robots('|\")", + RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled); + + private readonly INavigationManager navigationManager; + private readonly IPortalController portalController; + private readonly IModuleControlPipeline moduleControlPipeline; + private readonly IApplicationInfo applicationInfo; + private readonly ISkinModelFactory skinModelFactory; + private readonly IHostSettings hostSettings; + private readonly IPageService pageService; + + /// + /// Initializes a new instance of the class. + /// + /// The navigation manager. + /// The portal controller. + /// The module control pipeline. + /// The application information service. + /// The skin model factory. + /// The host settings service. + /// The page service used for meta data. + public PageModelFactory( + INavigationManager navigationManager, + IPortalController portalController, + IModuleControlPipeline moduleControlPipeline, + IApplicationInfo applicationInfo, + ISkinModelFactory skinModelFactory, + IHostSettings hostSettings, + IPageService pageService) + { + this.navigationManager = navigationManager; + this.portalController = portalController; + this.moduleControlPipeline = moduleControlPipeline; + this.applicationInfo = applicationInfo; + this.skinModelFactory = skinModelFactory; + this.hostSettings = hostSettings; + this.pageService = pageService; + } + + /// + public PageModel CreatePageModel(DnnPageController controller) + { + var ctl = controller.Request.QueryString["ctl"] != null ? controller.Request.QueryString["ctl"] : string.Empty; + var pageModel = new PageModel + { + IsEditMode = Globals.IsEditMode(), + AntiForgery = AntiForgery.GetHtml().ToHtmlString(), + PortalId = controller.PortalSettings.PortalId, + TabId = controller.PortalSettings.ActiveTab.TabID, + Language = Thread.CurrentThread.CurrentCulture.Name, + //TODO: CSP - enable when CSP implementation is ready + // ContentSecurityPolicy = this.contentSecurityPolicy, + NavigationManager = this.navigationManager, + PageService = this.pageService, + FavIconLink = FavIcon.GetHeaderLink(hostSettings, controller.PortalSettings.PortalId), + }; + if (controller.PortalSettings.ActiveTab.PageHeadText != Null.NullString && !Globals.IsAdminControl()) + { + pageModel.PageHeadText = controller.PortalSettings.ActiveTab.PageHeadText; + } + + if (!string.IsNullOrEmpty(controller.PortalSettings.PageHeadText)) + { + pageModel.PortalHeadText = controller.PortalSettings.PageHeadText; + } + + // set page title + if (UrlUtils.InPopUp()) + { + var strTitle = new StringBuilder(controller.PortalSettings.PortalName); + var slaveModule = DotNetNuke.UI.UIUtilities.GetSlaveModule(controller.PortalSettings.ActiveTab.TabID); + + // Skip is popup is just a tab (no slave module) + if (slaveModule.DesktopModuleID != Null.NullInteger) + { + var control = this.moduleControlPipeline.CreateModuleControl(slaveModule) as IModuleControl; + var extension = Path.GetExtension(slaveModule.ModuleControl.ControlSrc.ToLowerInvariant()); + switch (extension) + { + case ".mvc": + var segments = slaveModule.ModuleControl.ControlSrc.Replace(".mvc", string.Empty).Split('/'); + + control.LocalResourceFile = string.Format( + "~/DesktopModules/MVC/{0}/{1}/{2}.resx", + slaveModule.DesktopModule.FolderName, + Localization.LocalResourceDirectory, + segments[0]); + break; + default: + control.LocalResourceFile = string.Concat( + slaveModule.ModuleControl.ControlSrc.Replace( + Path.GetFileName(slaveModule.ModuleControl.ControlSrc), + string.Empty), + Localization.LocalResourceDirectory, + "/", + Path.GetFileName(slaveModule.ModuleControl.ControlSrc)); + break; + } + + var title = Localization.LocalizeControlTitle(control); + + strTitle.Append(string.Concat(" > ", controller.PortalSettings.ActiveTab.LocalizedTabName)); + strTitle.Append(string.Concat(" > ", title)); + } + else + { + strTitle.Append(string.Concat(" > ", controller.PortalSettings.ActiveTab.LocalizedTabName)); + } + pageService.SetTitle(strTitle.ToString(), PagePriority.Page); + } + else + { + // If tab is named, use that title, otherwise build it out via breadcrumbs + if (!string.IsNullOrEmpty(controller.PortalSettings.ActiveTab.Title)) + { + pageService.SetTitle(controller.PortalSettings.ActiveTab.Title, PagePriority.Page); + } + else + { + // Elected for SB over true concatenation here due to potential for long nesting depth + var strTitle = new StringBuilder(controller.PortalSettings.PortalName); + foreach (TabInfo tab in controller.PortalSettings.ActiveTab.BreadCrumbs) + { + strTitle.Append(string.Concat(" > ", tab.TabName)); + } + pageService.SetTitle(strTitle.ToString(), PagePriority.Page); + } + } + + // Set to page + pageModel.Title = pageService.GetTitle(); + + // set the background image if there is one selected + if (!UrlUtils.InPopUp()) + { + if (!string.IsNullOrEmpty(controller.PortalSettings.BackgroundFile)) + { + var fileInfo = this.GetBackgroundFileInfo(controller.PortalSettings); + pageModel.BackgroundUrl = FileManager.Instance.GetUrl(fileInfo); + + // ((HtmlGenericControl)this.FindControl("Body")).Attributes["style"] = string.Concat("background-image: url('", url, "')"); + } + } + + // META Refresh + // Only autorefresh the page if we are in VIEW-mode and if we aren't displaying some module's subcontrol. + if (controller.PortalSettings.ActiveTab.RefreshInterval > 0 && Personalization.GetUserMode() == PortalSettings.Mode.View && string.IsNullOrEmpty(ctl)) + { + pageModel.MetaRefresh = controller.PortalSettings.ActiveTab.RefreshInterval.ToString(); + } + + // META description + if (!string.IsNullOrEmpty(controller.PortalSettings.ActiveTab.Description)) + { + pageService.SetDescription(controller.PortalSettings.ActiveTab.Description, PagePriority.Page); + } + else + { + pageService.SetDescription(controller.PortalSettings.Description, PagePriority.Site); + } + pageModel.Description = pageService.GetDescription(); + + // META keywords + if (!string.IsNullOrEmpty(controller.PortalSettings.ActiveTab.KeyWords)) + { + pageService.SetKeyWords(controller.PortalSettings.ActiveTab.KeyWords, PagePriority.Page); + } + else + { + pageService.SetKeyWords(controller.PortalSettings.KeyWords, PagePriority.Site); + } + pageModel.KeyWords = pageService.GetKeyWords(); + + // META copyright + if (!string.IsNullOrEmpty(controller.PortalSettings.FooterText)) + { + pageModel.Copyright = controller.PortalSettings.FooterText.Replace("[year]", DateTime.Now.Year.ToString()); + } + else + { + pageModel.Copyright = string.Concat("Copyright (c) ", DateTime.Now.Year, " by ", controller.PortalSettings.PortalName); + } + + // META generator + pageModel.Generator = string.Empty; + + // META Robots - hide it inside popups and if PageHeadText of current tab already contains a robots meta tag + if (!UrlUtils.InPopUp() && + !(HeaderTextRegex.IsMatch(controller.PortalSettings.ActiveTab.PageHeadText) || + HeaderTextRegex.IsMatch(controller.PortalSettings.PageHeadText))) + { + var allowIndex = true; + if (controller.PortalSettings.ActiveTab.TabSettings.ContainsKey("AllowIndex") && + bool.TryParse(controller.PortalSettings.ActiveTab.TabSettings["AllowIndex"].ToString(), out allowIndex) && + !allowIndex || ctl == "Login" || ctl == "Register") + { + pageModel.MetaRobots = "NOINDEX, NOFOLLOW"; + } + else + { + pageModel.MetaRobots = "INDEX, FOLLOW"; + } + } + + + foreach (var item in this.pageService.GetMessages()) + { + //pageModel.(this, item.Heading, item.Message, item.MessageType.ToModuleMessageType(), item.IconSrc); + } + + pageModel.CanonicalLinkUrl = this.pageService.GetCanonicalLinkUrl(); + + // NonProduction Label Injection + if (this.applicationInfo.Status != Abstractions.Application.ReleaseMode.Stable && Host.DisplayBetaNotice && !UrlUtils.InPopUp()) + { + var versionString = + $" ({this.applicationInfo.Status} Version: {this.applicationInfo.Version})"; + pageModel.Title += versionString; + } + + pageModel.Skin = this.skinModelFactory.CreateSkinModel(controller); + + return pageModel; + } + + private IFileInfo GetBackgroundFileInfo(PortalSettings portalSettings) + { + var cacheKey = string.Format(DataCache.PortalCacheKey, portalSettings.PortalId, "BackgroundFile"); + var file = CBO.GetCachedObject( + new CacheItemArgs(cacheKey, DataCache.PortalCacheTimeOut, DataCache.PortalCachePriority, portalSettings.PortalId, portalSettings.BackgroundFile), + this.GetBackgroundFileInfoCallBack); + + return file; + } + + private IFileInfo GetBackgroundFileInfoCallBack(CacheItemArgs itemArgs) + { + return FileManager.Instance.GetFile((int)itemArgs.Params[0], (string)itemArgs.Params[1]); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PaneModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PaneModelFactory.cs new file mode 100644 index 00000000000..ad77baef4f7 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PaneModelFactory.cs @@ -0,0 +1,326 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using System; + using System.IO; + using System.Threading; + using System.Web; + + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Services.Exceptions; + using DotNetNuke.Services.Personalization; + using DotNetNuke.UI.Skins; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Builds and configures instances for module placement. + /// + public class PaneModelFactory : IPaneModelFactory + { + private readonly IContainerModelFactory containerModelFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The container model factory. + public PaneModelFactory(IContainerModelFactory containerModelFactory) + { + this.containerModelFactory = containerModelFactory; + } + + /// + public PaneModel CreatePane(string name) + { + var pane = new PaneModel(name); + return pane; + } + + /// + public PaneModel InjectModule(PaneModel pane, ModuleInfo module, PortalSettings portalSettings) + { + // this.containerWrapperControl = new HtmlGenericControl("div"); + // this.PaneControl.Controls.Add(this.containerWrapperControl); + + // inject module classes + var classFormatString = "DnnModule DnnModule-{0} DnnModule-{1}"; + var sanitizedModuleName = Null.NullString; + + if (!string.IsNullOrEmpty(module.DesktopModule.ModuleName)) + { + sanitizedModuleName = Globals.CreateValidClass(module.DesktopModule.ModuleName, false); + } + + if (this.IsVesionableModule(module)) + { + classFormatString += " DnnVersionableControl"; + } + + // this.containerWrapperControl.Attributes["class"] = string.Format(classFormatString, sanitizedModuleName, module.ModuleID); + try + { + if (!Globals.IsAdminControl() && (portalSettings.InjectModuleHyperLink || Personalization.GetUserMode() != PortalSettings.Mode.View)) + { + // this.containerWrapperControl.Controls.Add(new LiteralControl("")); + } + + // Load container control + var container = this.LoadModuleContainer(module, portalSettings); + + // Add Container to Dictionary + pane.Containers.Add(container.ID, container); + } + catch (ThreadAbortException) + { + // Response.Redirect may called in module control's OnInit method, so it will cause ThreadAbortException, no need any action here. + } + catch (Exception exc) + { + var lex = new ModuleLoadException(string.Format(Skin.MODULEADD_ERROR, pane.Name), exc); + /* + if (TabPermissionController.CanAdminPage()) + { + // only display the error to administrators + this.containerWrapperControl.Controls.Add(new ErrorContainer(this.PortalSettings, Skin.MODULELOAD_ERROR, lex).Container); + } + + Exceptions.LogException(exc); + */ + throw lex; + } + + return pane; + } + + /// + public PaneModel ProcessPane(PaneModel pane) + { + if (Globals.IsLayoutMode()) + { + /* + this.PaneControl.Visible = true; + + // display pane border + string cssclass = this.PaneControl.Attributes["class"]; + if (string.IsNullOrEmpty(cssclass)) + { + this.PaneControl.Attributes["class"] = CPaneOutline; + } + else + { + this.PaneControl.Attributes["class"] = cssclass.Replace(CPaneOutline, string.Empty).Trim().Replace(" ", " ") + " " + CPaneOutline; + } + + // display pane name + var ctlLabel = new Label { Text = "
" + this.Name + "

", CssClass = "SubHead" }; + this.PaneControl.Controls.AddAt(0, ctlLabel); + */ + } + + /* + else + { + if (this.CanCollapsePane(pane)) + { + pane.CssClass += " DNNEmptyPane"; + } + + // Add support for drag and drop + if (Globals.IsEditMode()) + { + pane.CssClass += " dnnSortable"; + + } + } + */ + return pane; + } + + private bool CanCollapsePane(PaneModel pane) + { + // This section sets the width to "0" on panes that have no modules. + // This preserves the integrity of the HTML syntax so we don't have to set + // the visiblity of a pane to false. Setting the visibility of a pane to + // false where there are colspans and rowspans can render the skin incorrectly. + var canCollapsePane = true; + if (pane.Containers.Count > 0) + { + canCollapsePane = false; + } + + return canCollapsePane; + } + + private bool IsVesionableModule(ModuleInfo moduleInfo) + { + if (string.IsNullOrEmpty(moduleInfo.DesktopModule.BusinessControllerClass)) + { + return false; + } + + // TODO: Replace with DI-based resolution when business controller classes are migrated. + var controller = DotNetNuke.Framework.Reflection.CreateObject(moduleInfo.DesktopModule.BusinessControllerClass, string.Empty); + return controller is IVersionable; + } + + private ContainerModel LoadContainerFromCookie(HttpRequest request, PortalSettings portalSettings) + { + ContainerModel container = null; + var cookie = request.Cookies["_ContainerSrc" + portalSettings.PortalId]; + if (cookie != null) + { + if (!string.IsNullOrEmpty(cookie.Value)) + { + // container = this.LoadContainerByPath(SkinController.FormatSkinSrc(cookie.Value + ".ascx", this.PortalSettings)); + } + } + + return container; + } + + private ContainerModel LoadModuleContainer(ModuleInfo module, PortalSettings portalSettings) + { + var containerSrc = Null.NullString; + + // var request = this.PaneControl.Page.Request; + ContainerModel container = null; + + if (portalSettings.EnablePopUps && UrlUtils.InPopUp()) + { + containerSrc = module.ContainerPath + "popUpContainer.ascx"; + + // Check Skin for a popup Container + if (module.ContainerSrc == portalSettings.ActiveTab.ContainerSrc) + { + if (File.Exists(HttpContext.Current.Server.MapPath(containerSrc))) + { + container = this.LoadContainerByPath(containerSrc, module, portalSettings); + } + } + + // error loading container - load default popup container + if (container == null) + { + containerSrc = Globals.HostPath + "Containers/_default/popUpContainer.ascx"; + container = this.LoadContainerByPath(containerSrc, module, portalSettings); + } + } + else + { + /* + container = (this.LoadContainerFromQueryString(module, request) ?? this.LoadContainerFromCookie(request)) ?? this.LoadNoContainer(module); + if (container == null) + { + // Check Skin for Container + var masterModules = this.PortalSettings.ActiveTab.ChildModules; + if (masterModules.ContainsKey(module.ModuleID) && string.IsNullOrEmpty(masterModules[module.ModuleID].ContainerSrc)) + { + // look for a container specification in the skin pane + if (this.PaneControl != null) + { + if (this.PaneControl.Attributes["ContainerSrc"] != null) + { + container = this.LoadContainerFromPane(); + } + } + } + } + */ + // else load assigned container + if (container == null) + { + containerSrc = module.ContainerSrc; + if (!string.IsNullOrEmpty(containerSrc)) + { + containerSrc = SkinController.FormatSkinSrc(containerSrc, portalSettings); + container = this.LoadContainerByPath(containerSrc, module, portalSettings); + } + } + + // error loading container - load from tab + if (container == null) + { + containerSrc = portalSettings.ActiveTab.ContainerSrc; + if (!string.IsNullOrEmpty(containerSrc)) + { + containerSrc = SkinController.FormatSkinSrc(containerSrc, portalSettings); + container = this.LoadContainerByPath(containerSrc, module, portalSettings); + } + } + + // error loading container - load default + if (container == null) + { + containerSrc = SkinController.FormatSkinSrc(SkinController.GetDefaultPortalContainer(), portalSettings); + container = this.LoadContainerByPath(containerSrc, module, portalSettings); + } + } + + // Set container path + module.ContainerPath = SkinController.FormatSkinPath(containerSrc); + + // set container id to an explicit short name to reduce page payload + container.ID = "ctr"; + + // make the container id unique for the page + if (module.ModuleID > -1) + { + container.ID += module.ModuleID.ToString(); + } + + container.EditMode = Personalization.GetUserMode() == PortalSettings.Mode.Edit; + + return container; + } + + private ContainerModel LoadContainerByPath(string containerPath, ModuleInfo module, PortalSettings portalSettings) + { + if (containerPath.IndexOf("/skins/", StringComparison.InvariantCultureIgnoreCase) != -1 || containerPath.IndexOf("/skins\\", StringComparison.InvariantCultureIgnoreCase) != -1 || containerPath.IndexOf("\\skins\\", StringComparison.InvariantCultureIgnoreCase) != -1 || + containerPath.IndexOf("\\skins/", StringComparison.InvariantCultureIgnoreCase) != -1) + { + throw new Exception(); + } + + ContainerModel container = null; + + try + { + var containerSrc = containerPath; + if (containerPath.IndexOf(Globals.ApplicationPath, StringComparison.InvariantCultureIgnoreCase) != -1) + { + containerPath = containerPath.Remove(0, Globals.ApplicationPath.Length); + } + + // container = ControlUtilities.LoadControl(this.PaneControl.Page, containerPath); + container = this.containerModelFactory.CreateContainerModel(module, portalSettings, containerSrc); + + // container.ContainerSrc = containerSrc; + + // call databind so that any server logic in the container is executed + // container.DataBind(); + } + catch (Exception exc) + { + // could not load user control + var lex = new ModuleLoadException(Skin.MODULELOAD_ERROR, exc); + if (TabPermissionController.CanAdminPage()) + { + // only display the error to administrators + /* + this.containerWrapperControl.Controls.Add(new ErrorContainer(this.PortalSettings, string.Format(Skin.CONTAINERLOAD_ERROR, containerPath), lex).Container); + */ + } + + Exceptions.LogException(lex); + } + + return container; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/SkinModelFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/SkinModelFactory.cs new file mode 100644 index 00000000000..2ed4445ae45 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/SkinModelFactory.cs @@ -0,0 +1,739 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModelFactories +{ + using System; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading; + using System.Web; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Pages; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Host; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Portals.Extensions; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Entities.Tabs.TabVersions; + using DotNetNuke.Entities.Users; + using DotNetNuke.Framework; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Services.Exceptions; + using DotNetNuke.Services.FileSystem; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Pages; + using DotNetNuke.UI; + using DotNetNuke.UI.ControlPanels; + using DotNetNuke.UI.Modules; + using DotNetNuke.UI.Skins; + using DotNetNuke.UI.Skins.Controls; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.Exceptions; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.Models; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + using Microsoft.Extensions.DependencyInjection; + using DotNetNuke.Services.Pages; + using System.Globalization; + using DotNetNuke.Services.ClientDependency; + + /// + /// Builds instances from portal, tab, and module configuration. + /// + public class SkinModelFactory : ISkinModelFactory + { + public const string OnInitMessage = "Skin_InitMessage"; + public const string OnInitMessageType = "Skin_InitMessageType"; + + private readonly INavigationManager navigationManager; + private readonly IPaneModelFactory paneModelFactory; + private readonly IPageService PageService; + private readonly IClientResourceController clientResourceController; + + /// + /// Initializes a new instance of the class. + /// + /// The navigation manager. + /// The pane model factory. + /// The client resource controller. + /// The page service used for messages and meta data. + public SkinModelFactory( + INavigationManager navigationManager, + IPaneModelFactory paneModelFactory, + IClientResourceController clientResourceController, + IPageService pageService) + { + this.navigationManager = navigationManager; + this.paneModelFactory = paneModelFactory; + this.clientResourceController = clientResourceController; + this.PageService = pageService; + } + + /// + public SkinModel CreateSkinModel(DnnPageController page) + { + SkinModel skin = null; + var skinSource = Null.NullString; + + if (page.PortalSettings.EnablePopUps && UrlUtils.InPopUp()) + { + // attempt to find and load a popup skin from the assigned skinned source + skinSource = Globals.IsAdminSkin() ? SkinController.FormatSkinSrc(page.PortalSettings.DefaultAdminSkin, page.PortalSettings) : page.PortalSettings.ActiveTab.SkinSrc; + if (!string.IsNullOrEmpty(skinSource)) + { + skinSource = SkinController.FormatSkinSrc(SkinController.FormatSkinPath(skinSource) + "popUpSkin.ascx", page.PortalSettings); + + if (File.Exists(HttpContext.Current.Server.MapPath(SkinController.FormatSkinSrc(skinSource, page.PortalSettings)))) + { + skin = this.LoadSkin(page, skinSource); + } + } + + // error loading popup skin - load default popup skin + if (skin == null) + { + skinSource = Globals.HostPath + "Skins/_default/popUpSkin.ascx"; + skin = this.LoadSkin(page, skinSource); + } + + // set skin path + page.PortalSettings.ActiveTab.SkinPath = SkinController.FormatSkinPath(skinSource); + } + else + { + // skin preview + if (page.Request.QueryString["SkinSrc"] != null) + { + skinSource = SkinController.FormatSkinSrc(Globals.QueryStringDecode(page.Request.QueryString["SkinSrc"]) + ".ascx", page.PortalSettings); + skin = this.LoadSkin(page, skinSource); + } + + // load user skin ( based on cookie ) + if (skin == null) + { + var skinCookie = page.Request.Cookies["_SkinSrc" + page.PortalSettings.PortalId]; + if (skinCookie != null) + { + if (!string.IsNullOrEmpty(skinCookie.Value)) + { + skinSource = SkinController.FormatSkinSrc(skinCookie.Value + ".ascx", page.PortalSettings); + skin = this.LoadSkin(page, skinSource); + } + } + } + + // load assigned skin + if (skin == null) + { + // DNN-6170 ensure skin value is culture specific + skinSource = Globals.IsAdminSkin() ? PortalController.GetPortalSetting("DefaultAdminSkin", page.PortalSettings.PortalId, Host.DefaultPortalSkin, page.PortalSettings.CultureCode) : page.PortalSettings.ActiveTab.SkinSrc; + if (!string.IsNullOrEmpty(skinSource)) + { + skinSource = SkinController.FormatSkinSrc(skinSource, page.PortalSettings); + skin = this.LoadSkin(page, skinSource); + } + } + + // error loading skin - load default + if (skin == null) + { + skinSource = SkinController.FormatSkinSrc(SkinController.GetDefaultPortalSkin(), page.PortalSettings); + skin = this.LoadSkin(page, skinSource); + } + + // set skin path + page.PortalSettings.ActiveTab.SkinPath = SkinController.FormatSkinPath(skinSource); + } + + if (page.PortalSettings.ActiveTab.DisableLink) + { + if (TabPermissionController.CanAdminPage()) + { + var heading = Localization.GetString("PageDisabled.Header"); + var message = Localization.GetString("PageDisabled.Text"); + this.PageService.AddWarningMessage(heading, message); + } + } + + + // add CSS links + this.clientResourceController.CreateStylesheet("~/Resources/Shared/stylesheets/dnndefault/10.0.0/default.css") + .SetNameAndVersion("dnndefault", "10.0.0", false) + .SetPriority(FileOrder.Css.DefaultCss) + .Register(); + + this.clientResourceController.RegisterStylesheet(string.Concat(page.PortalSettings.ActiveTab.SkinPath, "skin.css"), FileOrder.Css.SkinCss, true); + this.clientResourceController.RegisterStylesheet(page.PortalSettings.ActiveTab.SkinSrc.Replace(".ascx", ".css"), FileOrder.Css.SpecificSkinCss, true); + + + // register css variables + var cssVariablesStyleSheet = this.GetCssVariablesStylesheet(page.PortalSettings.PortalId, page.PortalSettings.GetStyles(), page.PortalSettings.HomeSystemDirectory); + skin.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = cssVariablesStyleSheet, FileOrder = FileOrder.Css.DefaultCss }); + + // register the custom stylesheet of current page + if (page.PortalSettings.ActiveTab.TabSettings.ContainsKey("CustomStylesheet") && !string.IsNullOrEmpty(page.PortalSettings.ActiveTab.TabSettings["CustomStylesheet"].ToString())) + { + var styleSheet = page.PortalSettings.ActiveTab.TabSettings["CustomStylesheet"].ToString(); + + // Try and go through the FolderProvider first + var stylesheetFile = this.GetPageStylesheetFileInfo(styleSheet, page.PortalSettings.PortalId); + if (stylesheetFile != null) + { + skin.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = FileManager.Instance.GetUrl(stylesheetFile), FileOrder = FileOrder.Css.DefaultCss }); + } + else + { + skin.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = styleSheet, FileOrder = FileOrder.Css.DefaultCss }); + } + } + + if (page.PortalSettings.EnablePopUps) + { + JavaScript.RequestRegistration(CommonJs.jQueryUI); + var popupFilePath = HttpContext.Current.IsDebuggingEnabled + ? "~/js/Debug/dnn.modalpopup.js" + : "~/js/dnn.modalpopup.js"; + skin.RegisteredScripts.Add(new RegisteredScript() { Script = popupFilePath, FileOrder = FileOrder.Js.DnnModalPopup }); + } + return skin; + } + + private SkinModel LoadSkin(DnnPageController page, string skinPath) + { + SkinModel ctlSkin = null; + try + { + var skinSrc = skinPath; + if (skinPath.IndexOf(Globals.ApplicationPath, StringComparison.OrdinalIgnoreCase) != -1) + { + skinPath = skinPath.Remove(0, Globals.ApplicationPath.Length); + } + + ctlSkin = new SkinModel(); + ctlSkin.SkinSrc = skinSrc; + + // Load the Panes + this.LoadPanes(page.PortalSettings); + + // Load the Module Control(s) + var success = Globals.IsAdminControl() ? this.ProcessSlaveModule(page.PortalSettings, ctlSkin) : this.ProcessMasterModules(page.PortalSettings, ctlSkin); + + // Load the Control Panel + this.InjectControlPanel(ctlSkin, page.Request); + + + // Register any error messages on the Skin + if (page.Request.QueryString["error"] != null && Host.ShowCriticalErrors) + { + this.PageService.AddErrorMessage(" ", Localization.GetString("CriticalError.Error")); + + if (UserController.Instance.GetCurrentUserInfo().IsSuperUser) + { + ServicesFramework.Instance.RequestAjaxScriptSupport(); + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + + JavaScript.RequestRegistration(CommonJs.jQueryUI); + MvcJavaScript.RegisterClientReference(DotNetNuke.UI.Utilities.ClientAPI.ClientNamespaceReferences.dnn_dom); + this.clientResourceController.RegisterScript("~/resources/shared/scripts/dnn.logViewer.js"); + } + } + + if (!success && !TabPermissionController.CanAdminPage()) + { + // only display the warning to non-administrators (administrators will see the errors) + this.PageService.AddWarningMessage(Localization.GetString("ModuleLoadWarning.Error"), string.Format(Localization.GetString("ModuleLoadWarning.Text"), page.PortalSettings.Email)); + } + + + if (HttpContext.Current != null && HttpContext.Current.Items.Contains(OnInitMessage)) + { + var messageType = PageMessageType.Warning; + if (HttpContext.Current.Items.Contains(OnInitMessageType)) + { + messageType = (PageMessageType)Enum.Parse(typeof(PageMessageType), HttpContext.Current.Items[OnInitMessageType].ToString(), true); + } + + this.PageService.AddMessage(new PageMessage(string.Empty, HttpContext.Current.Items[OnInitMessage].ToString(), messageType, string.Empty, PagePriority.Default)); + + JavaScript.RequestRegistration(CommonJs.DnnPlugins); + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + } + + + // Process the Panes attributes + foreach (var key in ctlSkin.Panes.Keys) + { + this.paneModelFactory.ProcessPane(ctlSkin.Panes[key]); + } + + var isSpecialPageMode = UrlUtils.InPopUp() || page.Request.QueryString["dnnprintmode"] == "true"; + if (TabPermissionController.CanAddContentToPage() && Globals.IsEditMode() && !isSpecialPageMode) + { + // Register Drag and Drop plugin + JavaScript.RequestRegistration(CommonJs.DnnPlugins); + + // MvcClientResourceManager.RegisterStyleSheet(page.ControllerContext, "~/resources/shared/stylesheets/dnn.dragDrop.css", FileOrder.Css.FeatureCss); + ctlSkin.RegisteredStylesheets.Add(new RegisteredStylesheet { Stylesheet = "~/resources/shared/stylesheets/dnn.dragDrop.css", FileOrder = FileOrder.Css.FeatureCss }); + + // MvcClientResourceManager.RegisterScript(page.ControllerContext, "~/resources/shared/scripts/dnn.dragDrop.js"); + ctlSkin.RegisteredScripts.Add(new RegisteredScript() { Script = "~/resources/shared/scripts/dnn.dragDrop.js" }); + + // Register Client Script + var sb = new StringBuilder(); + sb.AppendLine(" (function ($) {"); + sb.AppendLine(" $(document).ready(function () {"); + sb.AppendLine(" $('.dnnSortable').dnnModuleDragDrop({"); + sb.AppendLine(" tabId: " + page.PortalSettings.ActiveTab.TabID + ","); + sb.AppendLine(" draggingHintText: '" + Localization.GetSafeJSString("DraggingHintText", Localization.GlobalResourceFile) + "',"); + sb.AppendLine(" dragHintText: '" + Localization.GetSafeJSString("DragModuleHint", Localization.GlobalResourceFile) + "',"); + sb.AppendLine(" dropHintText: '" + Localization.GetSafeJSString("DropModuleHint", Localization.GlobalResourceFile) + "',"); + sb.AppendLine(" dropTargetText: '" + Localization.GetSafeJSString("DropModuleTarget", Localization.GlobalResourceFile) + "'"); + sb.AppendLine(" });"); + sb.AppendLine(" });"); + sb.AppendLine(" } (jQuery));"); + + var script = sb.ToString(); + MvcClientAPI.RegisterStartupScript("DragAndDrop", script); + } + } + catch (MvcPageException mvcExc) + { + throw mvcExc; + } + catch (Exception exc) + { + // could not load user control + var lex = new PageLoadException("Unhandled error loading page.", exc); + if (TabPermissionController.CanAdminPage()) + { + // only display the error to administrators + /* + var skinError = (Label)page.FindControl("SkinError"); + skinError.Text = string.Format(Localization.GetString("SkinLoadError", Localization.GlobalResourceFile), skinPath, page.Server.HtmlEncode(exc.Message)); + skinError.Visible = true; + */ + ctlSkin.SkinError = string.Format(Localization.GetString("SkinLoadError", Localization.GlobalResourceFile), skinPath, page.Server.HtmlEncode(exc.Message)); + } + + Exceptions.LogException(lex); + } + + return ctlSkin; + } + + private void LoadPanes(PortalSettings portalSettings) + { + /* + portalSettings.ActiveTab.Panes.Add("HeaderPane"); + portalSettings.ActiveTab.Panes.Add("ContentPane"); + portalSettings.ActiveTab.Panes.Add("ContentPaneLower"); + */ + + /* + // iterate page controls + foreach (Control ctlControl in this.Controls) + { + var objPaneControl = ctlControl as HtmlContainerControl; + + // Panes must be runat=server controls so they have to have an ID + if (objPaneControl != null && !string.IsNullOrEmpty(objPaneControl.ID)) + { + // load the skin panes + switch (objPaneControl.TagName.ToLowerInvariant()) + { + case "td": + case "div": + case "span": + case "p": + case "section": + case "header": + case "footer": + case "main": + case "article": + case "aside": + // content pane + if (!objPaneControl.ID.Equals("controlpanel", StringComparison.InvariantCultureIgnoreCase)) + { + // Add to the PortalSettings (for use in the Control Panel) + portalSettings.ActiveTab.Panes.Add(objPaneControl.ID); + + // Add to the Panes collection + this.Panes.Add(objPaneControl.ID.ToLowerInvariant(), new Pane(objPaneControl)); + } + else + { + // Control Panel pane + this.controlPanel = objPaneControl; + } + + break; + } + } + } + */ + } + + private bool ProcessMasterModules(PortalSettings portalSettings, SkinModel skin) + { + var success = true; + if (TabPermissionController.CanViewPage()) + { + // We need to ensure that Content Item exists since in old versions Content Items are not needed for tabs + this.EnsureContentItemForTab(portalSettings.ActiveTab); + + // Versioning checks. + if (!TabController.CurrentPage.HasAVisibleVersion) + { + this.HandleAccesDenied(true); + } + + int urlVersion; + if (TabVersionUtils.TryGetUrlVersion(out urlVersion)) + { + if (!TabVersionUtils.CanSeeVersionedPages()) + { + this.HandleAccesDenied(false); + return true; + } + + if (TabVersionController.Instance.GetTabVersions(TabController.CurrentPage.TabID).All(tabVersion => tabVersion.Version != urlVersion)) + { + throw new NotFoundException("ErrorPage404", this.navigationManager.NavigateURL(portalSettings.ErrorPage404, string.Empty, "status=404")); + /* + this.Response.Redirect(this.NavigationManager.NavigateURL(portalSettings.ErrorPage404, string.Empty, "status=404")); + */ + } + } + + // check portal expiry date + if (!this.CheckExpired(portalSettings)) + { + if (portalSettings.ActiveTab.StartDate < DateTime.Now && portalSettings.ActiveTab.EndDate > DateTime.Now || TabPermissionController.CanAdminPage() || Globals.IsLayoutMode()) + { + foreach (var objModule in PortalSettingsController.Instance().GetTabModules(portalSettings)) + { + success = this.ProcessModule(portalSettings, skin, objModule); + } + } + else + { + this.HandleAccesDenied(false); + } + } + else + { + this.PageService.AddErrorMessage( + string.Empty, + string.Format(Localization.GetString("ContractExpired.Error"), portalSettings.PortalName, Globals.GetMediumDate(portalSettings.ExpiryDate.ToString(CultureInfo.InvariantCulture)), portalSettings.Email)); + } + } + else + { + // If request localized page which haven't complete translate yet, redirect to default language version. + var redirectUrl = Globals.AccessDeniedURL(Localization.GetString("TabAccess.Error")); + + // Current locale will use default if did'nt find any + var currentLocale = LocaleController.Instance.GetCurrentLocale(portalSettings.PortalId); + if (portalSettings.ContentLocalizationEnabled && + TabController.CurrentPage.CultureCode != currentLocale.Code) + { + redirectUrl = new LanguageTokenReplace { Language = currentLocale.Code }.ReplaceEnvironmentTokens("[URL]"); + } + + throw new AccesDeniedException("TabAccess.Error", redirectUrl); + /* + this.Response.Redirect(redirectUrl, true); + */ + } + + return success; + } + + private bool ProcessSlaveModule(PortalSettings portalSettings, SkinModel skin) + { + var success = true; + var key = UIUtilities.GetControlKey(); + var moduleId = UIUtilities.GetModuleId(key); + var slaveModule = UIUtilities.GetSlaveModule(moduleId, key, portalSettings.ActiveTab.TabID); + + PaneModel pane; + skin.Panes.TryGetValue(Globals.glbDefaultPane.ToLowerInvariant(), out pane); + if (pane == null) + { + skin.Panes.Add(Globals.glbDefaultPane.ToLowerInvariant(), this.paneModelFactory.CreatePane(Globals.glbDefaultPane.ToLowerInvariant())); + skin.Panes.TryGetValue(Globals.glbDefaultPane.ToLowerInvariant(), out pane); + } + + slaveModule.PaneName = Globals.glbDefaultPane; + slaveModule.ContainerSrc = portalSettings.ActiveTab.ContainerSrc; + if (string.IsNullOrEmpty(slaveModule.ContainerSrc)) + { + slaveModule.ContainerSrc = portalSettings.DefaultPortalContainer; + } + + slaveModule.ContainerSrc = SkinController.FormatSkinSrc(slaveModule.ContainerSrc, portalSettings); + slaveModule.ContainerPath = SkinController.FormatSkinPath(slaveModule.ContainerSrc); + + var moduleControl = ModuleControlController.GetModuleControlByControlKey(key, slaveModule.ModuleDefID); + if (moduleControl != null) + { + slaveModule.ModuleControlId = moduleControl.ModuleControlID; + slaveModule.IconFile = moduleControl.IconFile; + + string permissionKey; + switch (slaveModule.ModuleControl.ControlSrc) + { + case "Admin/Modules/ModuleSettings.ascx": + permissionKey = "MANAGE"; + break; + case "Admin/Modules/Import.ascx": + permissionKey = "IMPORT"; + break; + case "Admin/Modules/Export.ascx": + permissionKey = "EXPORT"; + break; + default: + permissionKey = "CONTENT"; + break; + } + + if (ModulePermissionController.HasModuleAccess(slaveModule.ModuleControl.ControlType, permissionKey, slaveModule)) + { + success = this.InjectModule(portalSettings, pane, slaveModule); + } + else + { + throw new AccesDeniedException("AccesDenied", Globals.AccessDeniedURL(Localization.GetString("ModuleAccess.Error"))); + /* + this.Response.Redirect(Globals.AccessDeniedURL(Localization.GetString("ModuleAccess.Error")), true); + */ + } + } + + return success; + } + + private bool ProcessModule(PortalSettings portalSettings, SkinModel skin, ModuleInfo module) + { + var success = true; + var x = Globals.GetCurrentServiceProvider().GetService(); + if (x.CanInjectModule(module, portalSettings)) + { + // We need to ensure that Content Item exists since in old versions Content Items are not needed for modules + this.EnsureContentItemForModule(module); + + var pane = this.GetPane(skin, module); + + if (pane != null) + { + success = this.InjectModule(portalSettings, pane, module); + } + else + { + var lex = new ModuleLoadException(Localization.GetString("PaneNotFound.Error")); + + // this.Controls.Add(new ErrorContainer(portalSettings, MODULELOAD_ERROR, lex).Container); + Exceptions.LogException(lex); + } + } + + return success; + } + + private PaneModel GetPane(SkinModel skin, ModuleInfo module) + { + PaneModel pane; + var found = skin.Panes.TryGetValue(module.PaneName.ToLowerInvariant(), out pane); + + if (!found) + { + // this.Panes.TryGetValue(Globals.glbDefaultPane.ToLowerInvariant(), out pane); + skin.Panes.Add(module.PaneName.ToLowerInvariant(), this.paneModelFactory.CreatePane(module.PaneName.ToLowerInvariant())); + found = skin.Panes.TryGetValue(module.PaneName.ToLowerInvariant(), out pane); + } + + return pane; + } + + private void HandleAccesDenied(bool v) + { + throw new NotImplementedException(); + } + + private bool CheckExpired(PortalSettings portalSettings) + { + var blnExpired = false; + if (portalSettings.ExpiryDate != Null.NullDate) + { + if (Convert.ToDateTime(portalSettings.ExpiryDate) < DateTime.Now && !Globals.IsHostTab(portalSettings.ActiveTab.TabID)) + { + blnExpired = true; + } + } + + return blnExpired; + } + + private void EnsureContentItemForTab(TabInfo tabInfo) + { + // If tab exists but ContentItem not, then we create it + if (tabInfo.ContentItemId == Null.NullInteger && tabInfo.TabID != Null.NullInteger) + { + TabController.Instance.CreateContentItem(tabInfo); + TabController.Instance.UpdateTab(tabInfo); + } + } + + private void EnsureContentItemForModule(ModuleInfo module) + { + // If module exists but ContentItem not, then we create it + if (module.ContentItemId == Null.NullInteger && module.ModuleID != Null.NullInteger) + { + ModuleController.Instance.CreateContentItem(module); + ModuleController.Instance.UpdateModule(module); + } + } + + private void InjectControlPanel(SkinModel skin, HttpRequestBase request) + { + // if querystring dnnprintmode=true, controlpanel will not be shown + if (request.QueryString["dnnprintmode"] != "true" && !UrlUtils.InPopUp() && request.QueryString["hidecommandbar"] != "true") + { + // if (Host.AllowControlPanelToDetermineVisibility || (ControlPanelBase.IsPageAdminInternal() || ControlPanelBase.IsModuleAdminInternal())) + if (ControlPanelBase.IsPageAdminInternal() || ControlPanelBase.IsModuleAdminInternal()) + { + // ControlPanel processing + skin.ControlPanelRazor = Path.GetFileNameWithoutExtension(Host.ControlPanel); + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + + /* + var controlPanel = ControlUtilities.LoadControl(this, Host.ControlPanel); + var form = (HtmlForm)this.Parent.FindControl("Form"); + + if (controlPanel.IncludeInControlHierarchy) + { + // inject ControlPanel control into skin + if (this.ControlPanel == null || HostController.Instance.GetBoolean("IgnoreControlPanelWrapper", false)) + { + if (form != null) + { + form.Controls.AddAt(0, controlPanel); + } + else + { + this.Page.Controls.AddAt(0, controlPanel); + } + } + else + { + this.ControlPanel.Controls.Add(controlPanel); + } + + // register admin.css + ClientResourceManager.RegisterAdminStylesheet(this.Page, Globals.HostPath + "admin.css"); + } + */ + } + } + } + + private bool InjectModule(PortalSettings portalSettings, PaneModel pane, ModuleInfo module) + { + var bSuccess = true; + + // try to inject the module into the pane + try + { + if (portalSettings.ActiveTab.TabID == portalSettings.UserTabId || portalSettings.ActiveTab.ParentId == portalSettings.UserTabId) + { + /* + var profileModule = this.ModuleControlPipeline.LoadModuleControl(this.Page, module) as IProfileModule; + if (profileModule == null || profileModule.DisplayModule) + { + pane.InjectModule(module); + } + */ + } + else + { + this.paneModelFactory.InjectModule(pane, module, portalSettings); + } + } + catch (ThreadAbortException) + { + // Response.Redirect may called in module control's OnInit method, so it will cause ThreadAbortException, no need any action here. + } + catch (Exception ex) + { + Exceptions.LogException(ex); + bSuccess = false; + } + + return bSuccess; + } + + private IFileInfo GetPageStylesheetFileInfo(string styleSheet, int portalId) + { + var cacheKey = string.Format(DataCache.PortalCacheKey, portalId, "PageStylesheet" + styleSheet); + var file = CBO.GetCachedObject( + new CacheItemArgs(cacheKey, DataCache.PortalCacheTimeOut, DataCache.PortalCachePriority, styleSheet, portalId), + this.GetPageStylesheetInfoCallBack); + + return file; + } + + private IFileInfo GetPageStylesheetInfoCallBack(CacheItemArgs itemArgs) + { + var styleSheet = itemArgs.Params[0].ToString(); + return FileManager.Instance.GetFile((int)itemArgs.Params[1], styleSheet); + } + + + private string GetCssVariablesStylesheet(int portalId, Abstractions.Portals.IPortalStyles portalStyles, string homeSystemDirectory) + { + var cacheKey = string.Format(DataCache.PortalStylesCacheKey, portalId); + var cacheArgs = new CacheItemArgs( + cacheKey, + DataCache.PortalCacheTimeOut, + DataCache.PortalCachePriority, + portalStyles, + homeSystemDirectory); + var filePath = CBO.GetCachedObject(cacheArgs, this.GetCssVariablesStylesheetCallback); + return filePath; + } + + private string GetCssVariablesStylesheetCallback(CacheItemArgs args) + { + var portalStyles = (PortalStyles)args.Params[0]; + var directory = (string)args.Params[1]; + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + var webPath = $"{directory}{portalStyles.FileName}"; + + var physicalPath = $"{directory}{portalStyles.FileName}"; + if (File.Exists(physicalPath)) + { + return webPath; + } + + var styles = portalStyles.ToString(); + File.WriteAllText(physicalPath, styles); + + return webPath; + } + + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ContainerModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ContainerModel.cs new file mode 100644 index 00000000000..d212006ac13 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ContainerModel.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Models +{ + using System.Collections.Generic; + using System.IO; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.UI.Modules; + + /// + /// Represents the data and behavior required to render a module container. + /// + public class ContainerModel + { + private ModuleInfo moduleConfiguration; + private ModuleHostModel moduleHost; + private PortalSettings portalSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The module configuration. + /// The current portal settings. + public ContainerModel(ModuleInfo moduleConfiguration, PortalSettings portalSettings) + { + this.moduleConfiguration = moduleConfiguration; + this.moduleHost = new ModuleHostModel(moduleConfiguration); + this.portalSettings = portalSettings; + } + + /// + /// Gets the portal settings associated with this container. + /// + public PortalSettings PortalSettings + { + get + { + return this.portalSettings; + } + } + /// + /// Gets the module host model used to render the module. + /// + public ModuleHostModel ModuleHost + { + get + { + return this.moduleHost; + } + } + + /// + /// Gets the module control instance associated with this container. + /// + public IModuleControl ModuleControl + { + get + { + IModuleControl moduleControl = null; + if (this.ModuleHost != null) + { + moduleControl = this.ModuleHost.ModuleControl; + } + + return moduleControl; + } + } + + /// + /// Gets or sets the HTML identifier assigned to the container. + /// + public string ID { get; internal set; } + + /// + /// Gets the directory path of the container control. + /// + public string ContainerPath + { + get + { + return Path.GetDirectoryName(this.ContainerSrc) + "/"; + } + } + + /// + /// Gets or sets the source path of the container control. + /// + public string ContainerSrc { get; internal set; } + + /// + /// Gets the MVC action name used to render the module within the container. + /// + public string ActionName + { + get + { + if (this.moduleConfiguration.ModuleControl.ControlKey == "Module") + { + return "LoadDefaultSettings"; + } + else + { + return string.IsNullOrEmpty(this.ModuleName) ? "Index" : this.FileNameWithoutExtension; + } + } + } + + /// + /// Gets the MVC controller name used to render the module within the container. + /// + public string ControllerName + { + get + { + if (this.moduleConfiguration.ModuleControl.ControlKey == "Module") + { + return "ModuleSettings"; + } + else + { + return string.IsNullOrEmpty(this.ModuleName) ? this.FileNameWithoutExtension : this.ModuleName; + } + } + } + + /// + /// Gets the Razor view path that corresponds to the container control. + /// + public string ContainerRazorFile + { + get + { + return "~" + Path.GetDirectoryName(this.ContainerSrc) + "/Views/" + Path.GetFileName(this.ContainerSrc).Replace(".ascx", ".cshtml"); + } + } + + /// + /// Gets the module configuration for this container. + /// + public ModuleInfo ModuleConfiguration + { + get + { + return this.moduleConfiguration; + } + } + + /// + /// Gets or sets a value indicating whether the container is rendered in edit mode. + /// + public bool EditMode { get; internal set; } + + /// + /// Gets or sets the footer markup for the container. + /// + public string Footer { get; internal set; } + + /// + /// Gets or sets the header markup for the container. + /// + public string Header { get; internal set; } + + /// + /// Gets or sets the CSS class applied to the content pane. + /// + public string ContentPaneCssClass { get; internal set; } + + /// + /// Gets or sets the inline style applied to the content pane. + /// + public string ContentPaneStyle { get; internal set; } + + /// + /// Gets or sets the stylesheets registered by the container. + /// + public List RegisteredStylesheets { get; set; } = new List(); + + private string ModuleName + { + get + { + return this.moduleConfiguration.DesktopModule.ModuleName; + } + } + + private string FileNameWithoutExtension + { + get + { + return Path.GetFileNameWithoutExtension(this.moduleConfiguration.ModuleControl.ControlSrc); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ControlViewModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ControlViewModel.cs new file mode 100644 index 00000000000..4b27ea4e2d1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ControlViewModel.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Models +{ + /// + /// View model used to invoke an MVC module control within a container. + /// + public class ControlViewModel + { + /// + /// Gets or sets the identifier of the module. + /// + public int ModuleId { get; set; } + + /// + /// Gets or sets the identifier of the tab on which the module resides. + /// + public int TabId { get; set; } + + /// + /// Gets or sets the identifier of the module control to render. + /// + public int ModuleControlId { get; set; } + + /// + /// Gets or sets the name of the pane where the module is placed. + /// + public string PanaName { get; set; } + + /// + /// Gets or sets the source path of the container control. + /// + public string ContainerSrc { get; set; } + + /// + /// Gets or sets the container path. + /// + public string ContainerPath { get; set; } + + /// + /// Gets or sets the icon file for the module. + /// + public string IconFile { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleActionsModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleActionsModel.cs new file mode 100644 index 00000000000..040c134ae54 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleActionsModel.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Models +{ + using System.Collections.Generic; + + using DotNetNuke.Entities.Modules; + + /// + /// Represents the actions and UI state associated with an MVC module. + /// + public class ModuleActionsModel + { + // public ModuleInstanceContext ModuleContext { get; internal set; } + + /// + /// Gets or sets the module context for which actions are defined. + /// + public ModuleInfo ModuleContext { get; set; } + + /// + /// Gets or sets a value indicating whether the module supports quick settings. + /// + public bool SupportsQuickSettings { get; set; } + + /// + /// Gets or sets a value indicating whether quick settings should be displayed. + /// + public bool DisplayQuickSettings { get; set; } + + /// + /// Gets or sets the model used to render quick settings. + /// + public object QuickSettingsModel { get; set; } + + /// + /// Gets or sets the JSON describing custom actions for the module. + /// + public string CustomActionsJSON { get; set; } + + /// + /// Gets or sets the JSON describing administrative actions for the module. + /// + public string AdminActionsJSON { get; set; } + + /// + /// Gets or sets the serialized list of panes that can host the module. + /// + public string Panes { get; set; } + + /// + /// Gets or sets the text for the custom actions group. + /// + public string CustomText { get; set; } + + /// + /// Gets or sets the text for the admin actions group. + /// + public string AdminText { get; set; } + + /// + /// Gets or sets the text used for move operations. + /// + public string MoveText { get; set; } + + /// + /// Gets or sets a value indicating whether the module supports move operations. + /// + public bool SupportsMove { get; set; } + + /// + /// Gets or sets a value indicating whether the module is shared across tabs. + /// + public bool IsShared { get; set; } + + /// + /// Gets or sets the module title shown in the actions UI. + /// + public string ModuleTitle { get; set; } + + /// + /// Gets or sets client-side scripts associated with actions. + /// + public Dictionary ActionScripts { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleHostModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleHostModel.cs new file mode 100644 index 00000000000..1a431d2b367 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleHostModel.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Models +{ + using System.Text.RegularExpressions; + + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Host; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Instrumentation; + using DotNetNuke.Security; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Services.Personalization; + using DotNetNuke.UI.Modules; + + /// + /// Hosts a module control (or its cached content) for use within the MVC pipeline. + /// + public sealed class ModuleHostModel + { + private const string DefaultCssProvider = "DnnPageHeaderProvider"; + private const string DefaultJsProvider = "DnnBodyProvider"; + + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(ModuleHostModel)); + + private static readonly Regex CdfMatchRegex = new Regex( + @"<\!--CDF\((?JAVASCRIPT|CSS|JS-LIBRARY)\|(?.+?)(\|(?.+?)\|(?\d+?))?\)-->", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private readonly ModuleInfo moduleConfiguration; + + private IModuleControl control = null; + + /// + /// Initializes a new instance of the class. + /// + /// The module configuration to host. + public ModuleHostModel(ModuleInfo moduleConfiguration) + { + this.moduleConfiguration = moduleConfiguration; + if (Host.EnableCustomModuleCssClass) + { + string moduleName = this.moduleConfiguration.DesktopModule.ModuleName; + if (moduleName != null) + { + moduleName = Globals.CleanName(moduleName); + } + + this.CssClass = string.Format("DNNModuleContent Mod{0}C", moduleName); + } + } + + /// + /// Gets the attached instance. + /// + public IModuleControl ModuleControl + { + get + { + // Make sure the Control tree has been created + // this.EnsureChildControls(); + return this.control as IModuleControl; + } + } + + /// + /// Gets the CSS class applied to the module content. + /// + public string CssClass { get; private set; } + + /// + /// Determines whether the specified module is currently in view mode. + /// + /// The module configuration. + /// The current portal settings. + /// true if the module is in view mode; otherwise, false. + internal static bool IsViewMode(ModuleInfo moduleInfo, PortalSettings settings) + { + bool viewMode; + + if (ModulePermissionController.HasModuleAccess(SecurityAccessLevel.ViewPermissions, Null.NullString, moduleInfo)) + { + viewMode = false; + } + else + { + viewMode = !ModulePermissionController.HasModuleAccess(SecurityAccessLevel.Edit, Null.NullString, moduleInfo); + } + + return viewMode || Personalization.GetUserMode() == PortalSettings.Mode.View; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleModelBase.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleModelBase.cs new file mode 100644 index 00000000000..88d45d3d312 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleModelBase.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Models +{ + /// + /// Base model for MVC module view models, exposing common module context. + /// + public class ModuleModelBase + { + /// + /// Gets or sets the identifier of the module instance. + /// + public int ModuleId { get; set; } + + /// + /// Gets or sets the identifier of the tab on which the module resides. + /// + public int TabId { get; set; } + + /// + /// Gets or sets the path to the local resource file for the module. + /// + public string LocalResourceFile { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PageModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PageModel.cs new file mode 100644 index 00000000000..505aaeed351 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PageModel.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Models +{ + using System.Collections.Generic; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Pages; + + /// + /// Represents the data required to render a DNN page through the MVC pipeline. + /// + public class PageModel + { + /// + /// Gets or sets the identifier of the current tab. + /// + public int? TabId { get; set; } + + /// + /// Gets or sets the language code for the current request. + /// + public string Language { get; set; } + + /// + /// Gets or sets the identifier of the current portal. + /// + public int? PortalId { get; set; } + + /// + /// Gets or sets the skin model used to render the page. + /// + public SkinModel Skin { get; set; } + + /// + /// Gets or sets the anti-forgery token markup to include in the page. + /// + public string AntiForgery { get; set; } + + /// + /// Gets or sets the client-side variables available to scripts. + /// + public Dictionary ClientVariables { get; set; } + + /// + /// Gets or sets the page-level head content. + /// + public string PageHeadText { get; set; } + + /// + /// Gets or sets the portal-level head content. + /// + public string PortalHeadText { get; set; } + + /// + /// Gets or sets the page title. + /// + public string Title { get; set; } + + /// + /// Gets or sets the background image URL for the page. + /// + public string BackgroundUrl { get; set; } + + /// + /// Gets or sets the meta refresh directive, if any. + /// + public string MetaRefresh { get; set; } + + /// + /// Gets or sets the meta description for the page. + /// + public string Description { get; set; } + + /// + /// Gets or sets the meta keywords for the page. + /// + public string KeyWords { get; set; } + + /// + /// Gets or sets the copyright meta information for the page. + /// + public string Copyright { get; set; } + + /// + /// Gets or sets the generator meta information for the page. + /// + public string Generator { get; set; } + + /// + /// Gets or sets the robots meta directive for the page. + /// + public string MetaRobots { get; set; } + + /// + /// Gets or sets the startup scripts registered for the page. + /// + public Dictionary StartupScripts { get; set; } + + /// + /// Gets or sets a value indicating whether the page is in edit mode. + /// + public bool IsEditMode { get; set; } + + /// + /// Gets or sets the favicon link element for the page. + /// + public string FavIconLink { get; set; } + + /// + /// Gets or sets the canonical link URL for the page. + /// + public string CanonicalLinkUrl { get; set; } + + // TODO: CSP - enable when CSP implementation is ready + // public IContentSecurityPolicy ContentSecurityPolicy { get; set; } + + /// + /// Gets or sets the navigation manager for generating navigation URLs. + /// + public INavigationManager NavigationManager { get; set; } + + /// + /// Gets or sets the client resource controller used to register scripts and styles. + /// + public IClientResourceController ClientResourceController { get; set; } + + /// + /// Gets or sets the page service used to interact with page metadata. + /// + public IPageService PageService { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PaneModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PaneModel.cs new file mode 100644 index 00000000000..30e162892fb --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PaneModel.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Models +{ + using System.Collections.Generic; + + /// + /// Represents a pane within a skin, containing one or more module containers. + /// + public class PaneModel + { + private Dictionary containers; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the pane. + public PaneModel(string name) + { + this.Name = name; + } + + /// + /// Gets or sets the CSS class applied to the pane. + /// + public string CssClass { get; set; } + + /// + /// Gets the collection of containers hosted within the pane. + /// + public Dictionary Containers + { + get + { + return this.containers ?? (this.containers = new Dictionary()); + } + } + + /// Gets or sets the name (ID) of the Pane. + public string Name { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredScript.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredScript.cs new file mode 100644 index 00000000000..660af4206a4 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredScript.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +using DotNetNuke.Abstractions.ClientResources; + +namespace DotNetNuke.Web.MvcPipeline.Models +{ + /// + /// Represents a client script registered by the MVC pipeline. + /// + public class RegisteredScript + { + /// + /// Gets or sets the script path or content. + /// + public string Script { get; set; } + + /// + /// Gets or sets the load order for the script. + /// + public FileOrder.Js FileOrder { get; set; } = DotNetNuke.Abstractions.ClientResources.FileOrder.Js.DefaultPriority; + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredStylesheet.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredStylesheet.cs new file mode 100644 index 00000000000..e9a4e4ec070 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredStylesheet.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +using DotNetNuke.Abstractions.ClientResources; + +namespace DotNetNuke.Web.MvcPipeline.Models +{ + /// + /// Represents a stylesheet registered by the MVC pipeline. + /// + public class RegisteredStylesheet + { + /// + /// Gets or sets the stylesheet path. + /// + public string Stylesheet { get; set; } + + /// + /// Gets or sets the load order for the stylesheet. + /// + public FileOrder.Css FileOrder { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/SkinModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/SkinModel.cs new file mode 100644 index 00000000000..0ca3ff75045 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/SkinModel.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Models +{ + using System.Collections.Generic; + using System.IO; + + using DotNetNuke.Common; + using DotNetNuke.Web.MvcPipeline.Controllers; + + /// + /// Represents the skin definition and resources used to render a page. + /// + public class SkinModel + { + private Dictionary panes; + + /// + /// Gets or sets the skin source path. + /// + public string SkinSrc { get; set; } + + /// + /// Gets the collection of panes defined by the skin. + /// + public Dictionary Panes + { + get + { + return this.panes ?? (this.panes = new Dictionary()); + } + } + + /// + /// Gets the path to the Razor view file corresponding to the skin. + /// + public string RazorFile + { + get + { + return Path.GetDirectoryName(this.SkinSrc).Replace("\\", "/") + "/Views/" + Path.GetFileName(this.SkinSrc).Replace(".ascx", ".cshtml"); + } + } + + /// + /// Gets the directory path of the skin. + /// + public string SkinPath + { + get + { + return Path.GetDirectoryName(this.SkinSrc).Replace("\\", "/") + "/"; + } + } + + /// + /// Gets or sets the Razor view used for the control panel. + /// + public string ControlPanelRazor { get; set; } + + /// + /// Gets the CSS class applied to panes rendered by the skin. + /// + public string PaneCssClass + { + get + { + /* + if (Globals.IsEditMode()) + { + return "dnnSortable"; + } + */ + return string.Empty; + } + } + + /// + /// Gets the CSS class applied to the page body when the skin is rendered. + /// + public string BodyCssClass + { + get + { + if (Globals.IsEditMode()) + { + return "dnnEditState"; + } + + return string.Empty; + } + } + + /// + /// Gets or sets an error message associated with loading the skin. + /// + public string SkinError { get; set; } + + /// + /// Gets or sets the list of stylesheets registered by the skin. + /// + public List RegisteredStylesheets { get; set; } = new List(); + + /// + /// Gets or sets the list of scripts registered by the skin. + /// + public List RegisteredScripts { get; set; } = new List(); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/DefaultMvcModuleControlBase.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/DefaultMvcModuleControlBase.cs new file mode 100644 index 00000000000..5cb01b1d594 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/DefaultMvcModuleControlBase.cs @@ -0,0 +1,310 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + using System; + using System.Collections; + using System.IO; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + using System.Web.UI; + + using DotNetNuke.Common; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Instrumentation; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.Client; + using DotNetNuke.Web.Client.ClientResourceManagement; + + /// + /// Base implementation for MVC module controls, exposing common DNN context and services. + /// + public abstract class DefaultMvcModuleControlBase : IMvcModuleControl, IDisposable + { + private readonly Lazy serviceScopeContainer = new Lazy(ServiceScopeContainer.GetRequestOrCreateScope); + private string localResourceFile; + private ModuleInstanceContext moduleContext; + + /// + /// Gets the module configuration associated with this control. + /// + public ModuleInfo ModuleConfiguration + { + get + { + return this.ModuleContext.Configuration; + } + } + + /// + /// Gets the identifier of the current tab. + /// + public int TabId + { + get + { + return this.ModuleContext.TabId; + } + } + + /// + /// Gets the identifier of the current module. + /// + public int ModuleId + { + get + { + return this.ModuleContext.ModuleId; + } + } + + /// + /// Gets the identifier of the current tab-module instance. + /// + public int TabModuleId + { + get + { + return this.ModuleContext.TabModuleId; + } + } + + /// + /// Gets a value indicating whether the current tab is a host (superuser) tab. + /// + public bool IsHostMenu + { + get + { + return Globals.IsHostTab(this.PortalSettings.ActiveTab.TabID); + } + } + + /// + /// Gets the current portal settings. + /// + public PortalSettings PortalSettings + { + get + { + return PortalController.Instance.GetCurrentPortalSettings(); + } + } + + /// + /// Gets the identifier of the current portal. + /// + public int PortalId + { + get + { + return this.ModuleContext.PortalId; + } + } + + /// + /// Gets information about the current user. + /// + public UserInfo UserInfo + { + get + { + return this.PortalSettings.UserInfo; + } + } + + /// + /// Gets the identifier of the current user. + /// + public int UserId + { + get + { + return this.PortalSettings.UserId; + } + } + + /// + /// Gets the current portal alias information. + /// + public PortalAliasInfo PortalAlias + { + get + { + return this.PortalSettings.PortalAlias; + } + } + + /// + /// Gets the settings for the current module instance. + /// + public Hashtable Settings + { + get + { + return this.ModuleContext.Settings; + } + } + + /// Gets or sets the underlying base control for this module control. + /// A instance. + public Control Control { get; set; } + + /// Gets the name for this control. + /// A string representing the control name. + public virtual string ControlName + { + get + { + if (string.IsNullOrEmpty(this.ModuleConfiguration.ModuleControl.ControlKey)) + { + return "View"; + } + else + { + return this.ModuleConfiguration.ModuleControl.ControlKey; + } + } + } + + /// Gets the path for this control (used primarily for user controls). + /// A string representing the control path. + public virtual string ControlPath + { + get + { + if (this.ModuleConfiguration.DesktopModule != null) + { + return "DesktopModules/" + this.ModuleConfiguration.DesktopModule.FolderName; + } + + return string.Empty; + } + } + + /// + /// Gets the default resource file name for this control. + /// + public virtual string ResourceName + { + get + { + return this.ControlName + ".resx"; + } + } + + /// Gets the Module Context for this control. + /// A ModuleInstanceContext. + public ModuleInstanceContext ModuleContext + { + get + { + if (this.moduleContext == null) + { + this.moduleContext = new ModuleInstanceContext(this); + } + + return this.moduleContext; + } + } + + /// + /// Gets or sets the module action collection for this control. + /// + public ModuleActionCollection Actions + { + get + { + return this.ModuleContext.Actions; + } + + set + { + this.ModuleContext.Actions = value; + } + } + + /// + /// Gets or sets the help URL associated with this control. + /// + public string HelpURL + { + get + { + return this.ModuleContext.HelpURL; + } + + set + { + this.ModuleContext.HelpURL = value; + } + } + + /// Gets or sets the local resource file for this control. + /// A string representing the resource file path. + public string LocalResourceFile + { + get + { + string fileRoot; + if (string.IsNullOrEmpty(this.localResourceFile)) + { + fileRoot = "~/" + this.ControlPath + "/" + Localization.LocalResourceDirectory + "/" + this.ResourceName; + } + else + { + fileRoot = this.localResourceFile; + } + + return fileRoot; + } + + set + { + this.localResourceFile = value; + } + } + + /// + /// Gets the Dependency Provider to resolve registered + /// services with the container. + /// + /// + /// The Dependency Service. + /// + protected IServiceProvider DependencyProvider => this.serviceScopeContainer.Value.ServiceScope.ServiceProvider; + + /// + /// Gets the next available action identifier for this module. + /// + /// The next action identifier. + public int GetNextActionID() + { + return this.ModuleContext.GetNextActionID(); + } + + /// + public void Dispose() + { + // base.Dispose(); + if (this.serviceScopeContainer.IsValueCreated) + { + this.serviceScopeContainer.Value.Dispose(); + } + } + + /// + /// Renders the module control as HTML using the specified HTML helper. + /// + /// The MVC HTML helper. + /// The rendered HTML. + public abstract IHtmlString Html(HtmlHelper htmlHelper); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/IMvcModuleControl.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/IMvcModuleControl.cs new file mode 100644 index 00000000000..218e3499d7c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/IMvcModuleControl.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +using System; +using System.Web; +using System.Web.Mvc; +using DotNetNuke.UI.Modules; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + /// + /// Defines the contract for MVC-based module controls in the DNN MVC pipeline. + /// + public interface IMvcModuleControl : IModuleControl + { + /// + /// Renders the module control and returns the resulting HTML. + /// + /// The MVC HTML helper. + /// An HTML string representing the rendered control. + IHtmlString Html(HtmlHelper htmlHelper); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/ModuleControlFactory.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/ModuleControlFactory.cs new file mode 100644 index 00000000000..772142ed3c4 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/ModuleControlFactory.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + using System; + + using DotNetNuke.Common; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Framework; + + /// + /// Factory for creating MVC module control instances for the MVC pipeline. + /// + internal class ModuleControlFactory + { + /// + /// Creates an MVC module control instance for the specified module. + /// + /// The module configuration. + /// An MVC module control instance. + public static IMvcModuleControl CreateModuleControl(ModuleInfo module) + { + return CreateModuleControl(module, module.ModuleControl.ControlSrc); + } + + /// + /// Creates an MVC module control instance for the specified module and control source. + /// + /// The module configuration. + /// The control source. + /// An MVC module control instance. + public static IMvcModuleControl CreateModuleControl(ModuleInfo module, string controlSrc) + { + IMvcModuleControl control; + if (!string.IsNullOrEmpty(module.ModuleControl.MvcControlClass)) + { + var controlClass = module.ModuleControl.MvcControlClass; + try + { + var obj = Reflection.CreateObject(Globals.GetCurrentServiceProvider(), controlClass, controlClass); + if (obj is IMvcModuleControl) + { + control = obj as IMvcModuleControl; + } + else + { + throw new Exception("Mvc Control needs to implement IMvcModuleControl : " + controlClass); + } + } + catch (Exception ex) + { + throw new Exception("Could not create instance of " + controlClass, ex); + } + } + //// else if (controlSrc.EndsWith(".mvc", System.StringComparison.OrdinalIgnoreCase)) + //// { + //// control = new MvcModuleControl(); + //// } + else if (controlSrc.EndsWith(".html", StringComparison.OrdinalIgnoreCase)) + { + control = new SpaModuleControl(); + } + else + { + throw new Exception("The module control dous not support the MVC pipeline : " + module.ModuleTitle + " " + module.ModuleControl.ControlTitle); + } + + control.ModuleContext.Configuration = module; + return control; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/MvcModuleControl.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/MvcModuleControl.cs new file mode 100644 index 00000000000..b174e8f7cce --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/MvcModuleControl.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + using System; + using System.Collections; + using System.IO; + using System.Linq; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.UI; + + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Instrumentation; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Containers; + using DotNetNuke.UI.Modules; + using DotNetNuke.Collections; + using DotNetNuke.Common.Internal; + + /// + /// MVC module control that routes requests to MVC controllers and views based on the control source. + /// + public class MvcModuleControl : DefaultMvcModuleControlBase + { + + private const string ExcludedQueryStringParams = "tabid,mid,ctl,language,popup,action,controller"; + private const string ExcludedRouteValues = "mid,ctl,popup"; + + /// + /// Gets the control name, which maps to the MVC action name. + /// + public override string ControlName + { + get + { + return this.RouteActionName; + } + } + + + /// Gets the path for this control (used primarily for user controls). + /// A string representing the control path. + public override string ControlPath + { + get + { + return string.Format("~/DesktopModules/MVC/{0}", this.ModuleConfiguration.DesktopModule.FolderName); + } + } + + /// Gets the action name for the MVC route. + /// A string representing the action name. + public string RouteActionName + { + get + { + var segments = this.GetSegments(); + if (segments.Length < 2) + { + return string.Empty; + } + if (segments.Length == 3) + { + return segments[2]; + } + else + { + return segments[1]; + } + } + } + + /// Gets the controller name for the MVC route. + /// A string representing the controller name. + public string RouteControllerName + { + get + { + var segments = this.GetSegments(); + if (segments.Length < 2) + { + return string.Empty; + } + return segments[1]; + } + } + + /// + /// Gets the namespace that contains the MVC controllers for this module. + /// + public string RouteNamespace + { + get + { + var segments = this.GetSegments(); + if (segments.Length < 1) + { + return string.Empty; + } + return segments[0]; + } + } + + /// + /// Gets the segments that compose the MVC control source. + /// + /// An array of route segments. + public string[] GetSegments() + { + return this.ModuleConfiguration.ModuleControl.ControlSrc.Replace(".mvc", string.Empty).Split('/'); + } + + /// + /// Renders the MVC module using the appropriate controller and action. + /// + /// The MVC HTML helper. + /// The rendered HTML. + public override IHtmlString Html(HtmlHelper htmlHelper) + { + var module = this.ModuleConfiguration; + var controlSrc = module.ModuleControl.ControlSrc; + var area = module.DesktopModule.FolderName; + if (!controlSrc.EndsWith(".mvc", System.StringComparison.OrdinalIgnoreCase)) + { + throw new Exception("The controlSrc is not a MVC control: " + controlSrc); + } + + var segments = this.GetSegments(); + if (segments.Length < 2) + { + throw new Exception("The controlSrc is not a MVC control: " + controlSrc); + } + + string controllerName = string.Empty; + string actionName = string.Empty; + var controlKey = module.ModuleControl.ControlKey; + + + this.LocalResourceFile = string.Format( + "~/DesktopModules/MVC/{0}/{1}/{2}.resx", + module.DesktopModule.FolderName, + Localization.LocalResourceDirectory, + RouteActionName); + + RouteValueDictionary values = new RouteValueDictionary + { + { "mvcpage", true }, + { "ModuleId", module.ModuleID }, + { "TabId", module.TabID }, + { "ModuleControlId", module.ModuleControlId }, + { "PanaName", module.PaneName }, + { "ContainerSrc", module.ContainerSrc }, + { "ContainerPath", module.ContainerPath }, + { "IconFile", module.IconFile }, + { "area", area } + }; + + var queryString = htmlHelper.ViewContext.HttpContext.Request.QueryString; + + if (string.IsNullOrEmpty(controlKey)) + { + controlKey = queryString.GetValueOrDefault("ctl", string.Empty); + } + + var moduleId = Null.NullInteger; + if (queryString["moduleid"] != null) + { + int.TryParse(queryString["moduleid"], out moduleId); + } + + if (moduleId != module.ModuleID && string.IsNullOrEmpty(controlKey)) + { + // Set default routeData for module that is not the "selected" module + actionName = RouteActionName; + controllerName = RouteControllerName; + + // routeData.Values.Add("controller", controllerName); + // routeData.Values.Add("action", actionName); + + if (!string.IsNullOrEmpty(RouteNamespace)) + { + // routeData.DataTokens.Add("namespaces", new string[] { routeNamespace }); + } + } + else + { + var control = ModuleControlController.GetModuleControlByControlKey(controlKey, module.ModuleDefID); + actionName = queryString.GetValueOrDefault("action", this.RouteActionName); + controllerName = queryString.GetValueOrDefault("controller", this.RouteControllerName); + + // values.Add("controller", controllerName); + // values.Add("action", actionName); + + foreach (var param in queryString.AllKeys) + { + if (!ExcludedQueryStringParams.Split(',').ToList().Contains(param.ToLowerInvariant())) + { + if (!values.ContainsKey(param)) + { + values.Add(param, queryString[param]); + } + } + } + + if (!string.IsNullOrEmpty(this.RouteNamespace)) + { + // routeData.DataTokens.Add("namespaces", new string[] { routeNamespace }); + } + } + + string[] routeValues = + { + $"moduleId={this.ModuleConfiguration.ModuleID}", + $"controller={this.RouteControllerName}", + $"action={this.RouteActionName}", + }; + + var request = htmlHelper.ViewContext.HttpContext.Request; + var req = request.Params; + var isMyRoute = req["MODULEID"] != null && req["CONTROLLER"] != null && int.TryParse(req["MODULEID"], out var modId) && modId == this.ModuleConfiguration.ModuleID; + + var url = isMyRoute + ? request.Url.ToString() + : TestableGlobals.Instance.NavigateURL(this.ModuleConfiguration.TabID, TestableGlobals.Instance.IsHostTab(this.ModuleConfiguration.TabID), this.PortalSettings, string.Empty, routeValues); + + var formTag = new TagBuilder("form"); + formTag.Attributes.Add("action", url); + formTag.Attributes.Add("method", "post"); + formTag.InnerHtml += htmlHelper.AntiForgeryToken().ToHtmlString(); + + formTag.InnerHtml += htmlHelper.Action( + actionName, + controllerName, + values); + return new MvcHtmlString(formTag.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/MvcModuleControlExtensions.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/MvcModuleControlExtensions.cs new file mode 100644 index 00000000000..8381d06da0b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/MvcModuleControlExtensions.cs @@ -0,0 +1,261 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + using System; + using System.Collections.Generic; + using System.Web; + using System.Web.Mvc; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.Client; + using DotNetNuke.Web.Client.ClientResourceManagement; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + + /// + /// Extension methods for IMvcModuleControl interface. + /// + public static class MvcModuleControlExtensions + { + + /// + /// Gets a localized string for the module control. + /// + /// The module control instance. + /// The resource key. + /// The localized string. + public static string LocalizeString(this IMvcModuleControl moduleControl, string key) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + + if (string.IsNullOrEmpty(key)) + { + return string.Empty; + } + + return Localization.GetString(key, moduleControl.LocalResourceFile); + } + + /// + /// Gets a localized string formatted for safe JavaScript usage. + /// + /// The module control instance. + /// The resource key. + /// The JavaScript-safe localized string. + public static string LocalizeSafeJsString(this IMvcModuleControl moduleControl, string key) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + + if (string.IsNullOrEmpty(key)) + { + return string.Empty; + } + + return Localization.GetSafeJSString(key, moduleControl.LocalResourceFile); + } + + /// + /// Gets an edit URL for the module with specific parameters. + /// + /// The module control instance. + /// The parameter key name. + /// The parameter key value. + /// The control key for the edit page. + /// The edit URL with parameters. + public static string EditUrl(this IMvcModuleControl moduleControl) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + return EditUrl(moduleControl, "Edit", string.Empty, string.Empty); + } + + /// + /// Gets an edit URL for the module with specific parameters. + /// + /// The module control instance. + /// The parameter key name. + /// The parameter key value. + /// The control key for the edit page. + /// The edit URL with parameters. + public static string EditUrl(this IMvcModuleControl moduleControl, string controlKey) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + return EditUrl(moduleControl, controlKey, string.Empty, string.Empty); + } + + /// + /// Gets an edit URL for the module with specific parameters. + /// + /// The module control instance. + /// The parameter key name. + /// The parameter key value. + /// The control key for the edit page. + /// The edit URL with parameters. + public static string EditUrl(this IMvcModuleControl moduleControl, string keyName, string keyValue) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + return EditUrl(moduleControl, keyName, keyValue, string.Empty); + } + + /// + /// Gets an edit URL for the module with specific parameters. + /// + /// The module control instance. + /// The parameter key name. + /// The parameter key value. + /// The control key for the edit page. + /// The edit URL with parameters. + public static string EditUrl(this IMvcModuleControl moduleControl, string keyName, string keyValue, string controlKey) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + var parameters = new string[] { }; + return EditUrl(moduleControl, keyName, keyValue, controlKey, parameters); + } + + /// + /// Gets an edit URL for the module with specific parameters. + /// + /// The module control instance. + /// The parameter key name. + /// The parameter key value. + /// The control key for the edit page. + /// The edit URL with parameters. + public static string EditUrl(this IMvcModuleControl moduleControl, string keyName, string keyValue, string controlKey, params string[] additionalParameters) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + var parameters = GetParameters(moduleControl, controlKey, additionalParameters); + return moduleControl.ModuleContext.EditUrl(keyName, keyValue, controlKey, parameters); + } + + + /// + /// Gets an edit URL for the module with specific parameters. + /// + /// The module control instance. + /// The parameter key name. + /// The parameter key value. + /// The control key for the edit page. + /// The edit URL with parameters. + public static string EditUrl(this IMvcModuleControl moduleControl, int tabID, string controlKey, bool pageRedirect, params string[] additionalParameters) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + var parameters = GetParameters(moduleControl, controlKey, additionalParameters); + return moduleControl.ModuleContext.NavigateUrl(tabID, controlKey, pageRedirect, parameters); + } + + /// + /// Gets a module setting value with type conversion. + /// + /// The type to convert the setting to. + /// The module control instance. + /// The setting name. + /// The default value if setting is not found or conversion fails. + /// The setting value converted to the specified type. + public static T GetModuleSetting(this IMvcModuleControl moduleControl, string settingName, T defaultValue = default) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + + if (string.IsNullOrEmpty(settingName)) + { + return defaultValue; + } + + var settings = moduleControl.ModuleContext.Settings; + if (settings == null || !settings.ContainsKey(settingName)) + { + return defaultValue; + } + + try + { + var settingValue = settings[settingName]?.ToString(); + if (string.IsNullOrEmpty(settingValue)) + { + return defaultValue; + } + + return (T)Convert.ChangeType(settingValue, typeof(T)); + } + catch (Exception) + { + return defaultValue; + } + } + + /// + /// Checks if the current user is in edit mode for the module. + /// + /// The module control instance. + /// True if in edit mode; otherwise, false. + public static bool EditMode(this IMvcModuleControl moduleControl) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + + return moduleControl.ModuleContext.EditMode; + } + + /// + /// Checks if the module is editable by the current user. + /// + /// The module control instance. + /// True if editable; otherwise, false. + public static bool IsEditable(this IMvcModuleControl moduleControl) + { + if (moduleControl == null) + { + throw new ArgumentNullException(nameof(moduleControl)); + } + + return moduleControl.ModuleContext.IsEditable; + } + + private static string[] GetParameters(IMvcModuleControl moduleControl, string controlKey, string[] additionalParameters) + { + if (moduleControl.ModuleContext.Configuration.ModuleDefinition.ModuleControls.ContainsKey(controlKey)) + { + var editModuleControl = moduleControl.ModuleContext.Configuration.ModuleDefinition.ModuleControls[controlKey]; + if (!string.IsNullOrEmpty(editModuleControl.MvcControlClass)) + { + var parameters = new string[1 + additionalParameters.Length]; + parameters[0] = "mvcpage=yes"; + Array.Copy(additionalParameters, 0, parameters, 1, additionalParameters.Length); + return parameters; + } + } + + return additionalParameters; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/IPageContributor.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/IPageContributor.cs new file mode 100644 index 00000000000..dac7e61f341 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/IPageContributor.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Page +{ + /// + /// Defines a contract for module controls that need to contribute resources to the page. + /// + public interface IPageContributor + { + /// + /// Configures page-level resources such as scripts and styles for the module. + /// + /// The page configuration context. + void ConfigurePage(PageConfigurationContext context); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/PageConfigurationContext.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/PageConfigurationContext.cs new file mode 100644 index 00000000000..62358c727c3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/PageConfigurationContext.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Page +{ + using System; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Pages; + using DotNetNuke.Framework; + using DotNetNuke.Framework.JavaScriptLibraries; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Provides services and helpers for configuring page-level resources for MVC modules. + /// + public class PageConfigurationContext + { + /// + /// Initializes a new instance of the class. + /// + /// The dependency injection service provider. + public PageConfigurationContext(IServiceProvider serviceProvider) + { + this.ClientResourceController = serviceProvider.GetService(); + this.PageService = serviceProvider.GetService(); + this.ServicesFramework = DotNetNuke.Framework.ServicesFramework.Instance; + this.JavaScriptLibraryHelper = serviceProvider.GetService(); + } + + /// + /// Gets the client resource controller used to register scripts and styles. + /// + public IClientResourceController ClientResourceController { get; private set; } + + /// + /// Gets the page service used to manage page metadata. + /// + public IPageService PageService { get; private set; } + + /// + /// Gets the DNN services framework instance. + /// + public IServicesFramework ServicesFramework { get; private set; } + + /// + /// Gets the JavaScript library helper used to request JavaScript libraries. + /// + public IJavaScriptLibraryHelper JavaScriptLibraryHelper { get; private set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/README.md b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/README.md new file mode 100644 index 00000000000..b7e0405112c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/README.md @@ -0,0 +1,321 @@ +# MVC Module Control Implementation + +## Overview + +The MVC Module Control implementation provides a modern alternative to DNN's traditional WebForms-based module rendering pipeline. This new system enables DNN modules to leverage ASP.NET MVC architecture while maintaining compatibility with the existing DNN framework. + +## Problem Statement + +DNN Platform has historically relied on the WebForms pipeline accessed through `/default.aspx`. As outlined in [GitHub issue #6679](https://github.com/dnnsoftware/Dnn.Platform/issues/6679). + +## Solution: Hybrid Pipeline Architecture + +The MVC Pipeline introduces a dual-rendering mechanism: + +1. **Legacy Pipeline**: Traditional WebForms through `/default.aspx` +2. **New MVC Pipeline**: Modern MVC rendering through `/DesktopModules/Default/Page/{tabId}/{locale}` + +### Module Pipeline Support Matrix + +Based on the GitHub issue specifications, modules can support different pipeline patterns: + +| WebForms Support | MVC Module Support | SPA Module Support | +|------------------|--------------------|--------------------| +| Custom Control + Razor view | Use generic Control + Custom MVC Controller as child controller (shared with WebForms pipeline) | Use generic Control + return directly the html (shared with WebForms pipeline) | | +| Render Razor Partial | The generic control redirects to the controller defined in Control Src |The generic Control renders the html file defined in Control Src | | + +### Module Control Class Configuration + +Modules specify their MVC compatibility through: +- **MVC Control Class**: Defined in module control settings and module manifest +- **Interface Implementation**: Must implement `IMvcModuleControl` +- **Optional Interfaces**: Can implement `IActionable` for unified action handling +- **Pipeline Detection**: System can determine module compatibility and show appropriate messages + +## Class Diagram + +```mermaid +classDiagram + %% Interfaces + class IMvcModuleControl { + <> + +Html(helper) IHtmlString + } + + class IModuleControl { + <> + + +string ControlPath + +string ControlName + +ModuleInstanceContext ModuleContext + +string LocalResourceFile + } + + + %% Concrete Classes + class MvcModuleControl { + +Html(helper) IHtmlString + } + note for MvcModuleControl "MVC controller" + + class SpaModuleControl { + +Html(helper) IHtmlString + } + note for SpaModuleControl "Html with tokens" + + %% Abstract Classes + + class DefaultMvcModuleControlBase { + +Html(helper) IHtmlString + } + + class RazorModuleControlBase { + + +Invoke() IRazorModuleResult + } + note for RazorModuleControlBase "Razor view from model" + + %% Relationships + IMvcModuleControl ..|> IModuleControl : extends + + DefaultMvcModuleControlBase ..|> IMvcModuleControl : implements + + MvcModuleControl --|> DefaultMvcModuleControlBase : extends + RazorModuleControlBase --|> DefaultMvcModuleControlBase : extends + SpaModuleControl --|> DefaultMvcModuleControlBase : extends + + +``` + +## Core Components + +### 1. IMvcModuleControl Interface + +```csharp +public interface IMvcModuleControl : IModuleControl +{ + IHtmlString Html(HtmlHelper htmlHelper); +} +``` + +The base interface that all MVC module controls must implement, extending the standard `IModuleControl` with MVC-specific rendering capabilities. This interface enables: + +- **Pipeline Compatibility Detection**: The system can determine if a module supports the MVC pipeline +- **Unified Rendering**: The `Html()` method provides access to `HtmlHelper` with information about HttpContext, controller, and page model +- **Flexible Rendering Options**: Modules can use HTML helpers to render content (Razor partials, child action controllers, or other helpers) + +### 2. DefaultMvcModuleControlBase + +The abstract base class that provides common functionality for all MVC module controls: + +- **Dependency Injection**: Integrated service provider access +- **Module Context**: Access to DNN module configuration and settings +- **Portal Context**: Portal settings, user information, and localization +- **Resource Management**: Localization helpers and resource file management +- **URL Generation**: Helper methods for creating edit URLs + +**Key Features:** +- Service scoped dependency injection +- Automatic resource file path resolution +- Portal and user context access +- Module settings management +- Edit URL generation with MVC support + +### 3. Module Control Implementations + +#### MvcModuleControl +The standard MVC module control for traditional MVC controllers and actions. + +**Features:** +- Parses `.mvc` control source to extract controller and action names +- Supports routing with namespaces: `{namespace}/{controller}/{action}` +- Automatic query string parameter mapping +- Route value dictionary construction for MVC action execution +- Localization resource file resolution + +**Control Source Format:** +``` +{namespace}/{controller}/{action}.mvc +``` + +#### SpaModuleControl +Specialized control for Single Page Applications. + +**Features:** +- HTML5 file rendering with token replacement +- Automatic CSS and JavaScript file inclusion +- File existence caching for performance +- Support for HTML5 module token system +- Content caching with file dependency tracking + +**Supported Files:** +- `.html` or custom HTML5 files +- Automatic `.css` file inclusion (same name) +- Automatic `.js` file inclusion (same name) + +#### RazorModuleControlBase +Abstract base for modules using Razor view rendering. +This use MVC 5 razor views. +Recomended for Weforms control migrations +Folows the ViewComponent patern of .net Core for easy future trasition to .net Core + +**Features:** +- Direct Razor view rendering +- Model binding support +- Custom view context management +- Flexible view name resolution +- Request/Response context integration + +**Usage Pattern:** +```csharp +public class MyModuleControl : RazorModuleControlBase +{ + public override IRazorModuleResult Invoke() + { + var model = GetMyModel(); + return View("MyView", model); + } +} +``` + +### 4. Extension Methods (MvcModuleControlExtensions) + +Provides convenient extension methods for all MVC module controls: + +- **Localization**: `LocalizeString()`, `LocalizeSafeJsString()` +- **URL Generation**: `EditUrl()` with various overloads +- **Settings Access**: `GetModuleSetting()` with type conversion +- **State Checking**: `EditMode()`, `IsEditable()` + +### 5. Resource Management + +#### IResourcable Interface +Modules can implement this interface to automatically manage CSS and JavaScript resources. + +#### ModuleResources System +- Automatic resource registration +- Priority-based loading +- File existence validation +- Caching for performance +- Independent of the pipeline + +### 6. Utilities + +#### MvcModuleControlRenderer +Provides rendering capabilities for Razor-based module controls outside of the normal MVC pipeline. + +#### MvcViewEngine +A powerful utility class for rendering MVC views to strings outside of the standard MVC request pipeline. This class is essential for the MVC module control system as it enables view rendering in non-controller contexts. + +**Core Methods:** +```csharp +// Render full view with layout +string html = MvcViewEngine.RenderView("~/Views/MyView.cshtml", model); + +// Render partial view without layout +string partial = MvcViewEngine.RenderPartialView("~/Views/_MyPartial.cshtml", model); + +// Render HtmlHelper delegates +string html = MvcViewEngine.RenderHtmlHelperToString(helper => + helper.Action("MyAction", "MyController"), model); +``` + +## Demo Implementation + +The Demo folder includes demonstration classes that show practical implementation examples: + +### WrapperModule.cs +A WebForms-compatible module that bridges to the MVC pipeline, demonstrating how to integrate MVC module controls within the traditional DNN WebForms infrastructure. + +**Key Features:** +- **Hybrid Bridge Pattern**: Inherits from `PortalModuleBase` to maintain WebForms compatibility +- **MVC Integration**: Uses `MvcUtils.CreateModuleControl()` to instantiate MVC module controls +- **MvcViewEngine Integration**: Demonstrates `MvcViewEngine.RenderHtmlHelperToString()` usage +- **Interface Support**: Handles `IActionable` and `IResourcable` interfaces automatically +- **Lifecycle Management**: Proper ASP.NET control lifecycle implementation + +**Implementation Pattern:** +```csharp +public class WrapperModule : PortalModuleBase, IActionable +{ + protected override void OnInit(EventArgs e) + { + // Create MVC module control + var mc = MvcUtils.CreateModuleControl(this.ModuleConfiguration); + + // Render using MvcViewEngine + html = MvcViewEngine.RenderHtmlHelperToString(helper => mc.Html(helper)); + + // Handle optional interfaces + if (mc is IActionable actionable) + this.ModuleActions = actionable.ModuleActions; + + if (mc is IResourcable resourcable) + resourcable.RegisterResources(this.Page); + } +} +``` + +### DemoModuleControl.cs +A concrete implementation of `RazorModuleControlBase` showing how to create custom MVC module controls with dynamic view selection. + +**Key Features:** +- **Dynamic View Routing**: Uses query string parameters to determine which view to render +- **Multiple View Support**: Demonstrates rendering different views based on user input +- **Custom View Paths**: Shows how to specify custom view file locations +- **Model Passing**: Illustrates passing data models to views + +**Implementation Example:** +```csharp +public class DemoModuleControl : RazorModuleControlBase +{ + public override IRazorModuleResult Invoke() + { + // Dynamic view selection based on query parameters + switch (Request.QueryString["view"]) + { + case "Terms": + return View("~/Views/Default/Terms.cshtml", "Terms content"); + case "Privacy": + return View("~/admin/Portal/Views/Privacy.cshtml", "Privacy content"); + default: + return View("~/admin/Portal/Views/Terms.cshtml", "Default content"); + } + } +} +``` + +**Usage Scenarios:** +- **Migration**: Use WrapperModule to run MVC controls within WebForms pages +- **Development Reference**: DemoModuleControl shows best practices for Razor module implementation +- **Integration Patterns**: Demonstrates how to handle multiple interfaces (`IActionable`, `IResourcable`) +- **View Management**: Shows flexible view path configuration and model binding + +**Bridge Pattern Benefits:** +The WrapperModule demonstrates the bridge pattern that allows: +- Gradual migration from WebForms to MVC +- Using MVC controls within existing WebForms infrastructure +- Maintaining compatibility with existing DNN module architecture +- Automatic handling of module actions and resource registration + +## Key Benefits + +This implementation provides several key advantages: + +### 1. Modern Development Experience +- MVC pattern for better separation of concerns +- Dependency injection support +- Testable architecture +- Familiar development patterns for modern .NET developers + +### 2. Gradual Migration Path +- Hybrid architecture allows coexistence of WebForms and MVC +- Module-by-module migration strategy +- Backward compatibility maintained + +### 3. Pipeline Transparency & Compatibility +- **Unified Interface Implementation**: MVC modules can implement `IActionable` in a unified way +- **No Legacy Modifications**: No modifications required to the existing module pipeline +- **Custom Module Patterns**: Open to custom module control patterns + + diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ContentRazorModuleResult.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ContentRazorModuleResult.cs new file mode 100644 index 00000000000..8a7a704efe0 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ContentRazorModuleResult.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + using System.Web; + using System.Web.Mvc; + + /// + /// Razor result that renders a raw HTML content string. + /// + public class ContentRazorModuleResult : IRazorModuleResult + { + /// + /// Initializes a new instance of the class. + /// + /// The raw HTML content to render. + public ContentRazorModuleResult(string content) + { + this.Content = content; + } + + /// + /// Gets the raw HTML content to render. + /// + public string Content { get; private set; } + + /// + public IHtmlString Execute(HtmlHelper htmlHelper) + { + return new MvcHtmlString(this.Content); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ErrorRazorModuleResult.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ErrorRazorModuleResult.cs new file mode 100644 index 00000000000..00c0716ab06 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ErrorRazorModuleResult.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Modules; + + /// + /// Razor result that renders a standardized DNN error message panel. + /// + public class ErrorRazorModuleResult : IRazorModuleResult + { + /// + /// Initializes a new instance of the class. + /// + /// The error heading. + /// The error message. + public ErrorRazorModuleResult(string heading, string message) + { + this.Heading = heading; + this.Message = message; + } + + /// + /// Gets the error heading. + /// + public string Heading { get; private set; } + + /// + /// Gets the error message. + /// + public string Message { get; private set; } + + /// + public IHtmlString Execute(HtmlHelper htmlHelper) + { + return htmlHelper.ModuleMessage(this.Message, ModuleMessageType.Error, this.Heading); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IRazorModuleResult.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IRazorModuleResult.cs new file mode 100644 index 00000000000..96f2be328e7 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IRazorModuleResult.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + using System.Web; + using System.Web.Mvc; + + /// + /// Represents the result of executing a Razor-based module control. + /// + public interface IRazorModuleResult + { + /// + /// Executes the result using the specified HTML helper. + /// + /// The MVC HTML helper. + /// The rendered HTML. + IHtmlString Execute(HtmlHelper htmlHelper); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IViewRenderer.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IViewRenderer.cs new file mode 100644 index 00000000000..5904298c2ed --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IViewRenderer.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + /// + /// Defines a service that can render MVC views to strings. + /// + public interface IViewRenderer + { + /// + /// Renders the specified view to a string. + /// + /// The virtual path or name of the view. + /// The model to pass to the view. + /// The rendered view as a string. + string RenderViewToString(string viewPath, object model = null); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/RazorModuleViewContext.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/RazorModuleViewContext.cs new file mode 100644 index 00000000000..0b39c4b02d4 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/RazorModuleViewContext.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + using System.Web; + using System.Web.Mvc; + + /// + /// Holds contextual information required to render a Razor-based module view. + /// + public class RazorModuleViewContext + { + /// + /// Gets or sets the current HTTP context. + /// + public HttpContextBase HttpContext { get; internal set; } + + /// + /// Gets or sets the used by the view. + /// + public ViewDataDictionary ViewData { get; internal set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ViewRazorModuleResult.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ViewRazorModuleResult.cs new file mode 100644 index 00000000000..af989c7d4e3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ViewRazorModuleResult.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + + /// + /// Razor result that renders a partial view with an optional model and view data. + /// + public class ViewRazorModuleResult : IRazorModuleResult + { + /// + /// Initializes a new instance of the class. + /// + /// The name or path of the view. + /// The model to pass to the view. + /// The view data dictionary. + public ViewRazorModuleResult(string viewName, object model, ViewDataDictionary viewData) + { + this.ViewName = viewName; + this.Model = model; + this.ViewData = viewData; + } + + /// + /// Gets the view name or path. + /// + public string ViewName { get; private set; } + + /// + /// Gets the view model. + /// + public object Model { get; private set; } + + /// + /// Gets the . + /// + public ViewDataDictionary ViewData { get; private set; } + + /// + public IHtmlString Execute(HtmlHelper htmlHelper) + { + return htmlHelper.Partial(this.ViewName, this.Model, this.ViewData); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/RazorModuleControlBase.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/RazorModuleControlBase.cs new file mode 100644 index 00000000000..4ad84e4bc3b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/RazorModuleControlBase.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Claims; + using System.Security.Principal; + using System.Text; + using System.Threading.Tasks; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + using System.Web.Routing; + + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + using DotNetNuke.Web.MvcPipeline.Modules; + + /// + /// Base class for Razor-based MVC module controls. + /// + public abstract class RazorModuleControlBase : DefaultMvcModuleControlBase + { + private RazorModuleViewContext viewContext; + + /// + /// Gets the default view path for this module control. + /// + protected virtual string DefaultViewName + { + get + { + return "~/" + this.ControlPath.Replace('\\', '/').Trim('/') + "/Views/" + this.ControlName + ".cshtml"; + } + } + + /// + /// Gets or sets the Razor module view context. + /// + public RazorModuleViewContext ViewContext + { + get + { + if (this.viewContext == null) + { + this.viewContext = new RazorModuleViewContext + { + HttpContext = new HttpContextWrapper(System.Web.HttpContext.Current), + }; + } + + return this.viewContext; + } + + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + this.viewContext = value; + } + } + + /// + /// Renders the module control using Razor and returns the resulting HTML. + /// + /// The MVC HTML helper. + /// The rendered HTML. + public override IHtmlString Html(HtmlHelper htmlHelper) + { + this.ViewContext.ViewData = new ViewDataDictionary(htmlHelper.ViewData); + this.ViewContext.ViewData["ModuleContext"] = this.ModuleContext; + this.ViewContext.ViewData["ModuleId"] = this.ModuleId; + this.ViewContext.ViewData["LocalResourceFile"] = this.LocalResourceFile; + var res = this.Invoke(); + return res.Execute(htmlHelper); + } + + /// + /// Executes the module and returns a Razor module result to be rendered. + /// + /// The Razor module result. + public abstract IRazorModuleResult Invoke(); + + /// + /// Returns a result which will render HTML encoded text. + /// + /// The content, will be HTML encoded before output. + /// A . + public IRazorModuleResult Content(string content) + { + if (content == null) + { + throw new ArgumentNullException("content"); + } + + return new ContentRazorModuleResult(content); + } + + /// + /// Returns a result which will render an error message. + /// + /// The error heading. + /// The error message. + /// A . + public IRazorModuleResult Error(string heading, string message) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + return new ErrorRazorModuleResult(heading, message); + } + + /// + /// Returns a result that renders the default view. + /// + /// A . + public IRazorModuleResult View() + { + return View(null); + } + + /// + /// Returns a result that renders the specified view. + /// + /// The view name. + /// A . + public IRazorModuleResult View(string viewName) + { + return View(viewName, null); + } + + /// + /// Returns a result that renders the default view with the specified model. + /// + /// The view model. + /// A . + public IRazorModuleResult View(object model) + { + return this.View(null, model); + } + + /// + /// Returns a result that renders the specified view with the specified model. + /// + /// The view name. + /// The view model. + /// A . + public IRazorModuleResult View(string viewName, object model) + { + if (string.IsNullOrEmpty(viewName)) + { + viewName = this.DefaultViewName; + } + + return new ViewRazorModuleResult(viewName, model, this.ViewData); + } + + /// + /// Gets the . + /// + public HttpContextBase HttpContext => this.ViewContext.HttpContext; + + /// + /// Gets the . + /// + public HttpRequestBase Request => this.ViewContext.HttpContext.Request; + + /// + /// Gets the . + /// + public ViewDataDictionary ViewData => this.ViewContext.ViewData; + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/SpaModuleControl.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/SpaModuleControl.cs new file mode 100644 index 00000000000..4e75835bb04 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/SpaModuleControl.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.UI; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Modules; + using DotNetNuke.Collections; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Framework; + using DotNetNuke.Instrumentation; + using DotNetNuke.Services.Cache; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Containers; + using DotNetNuke.UI.Modules; + using DotNetNuke.UI.Modules.Html5; + using DotNetNuke.Web.Client.ClientResourceManagement; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + using Microsoft.Extensions.DependencyInjection; + + /// + /// SPA-style HTML5 module control that renders content from an HTML file and processes DNN tokens. + /// + public class SpaModuleControl : DefaultMvcModuleControlBase, IPageContributor + { + private readonly IBusinessControllerProvider businessControllerProvider; + private readonly IClientResourceController clientResourceController; + + /// + /// Initializes a new instance of the class using the global dependency provider. + /// + public SpaModuleControl() + : base() + { + this.businessControllerProvider = Globals.DependencyProvider.GetRequiredService(); + var serviceProvider = Common.Globals.GetCurrentServiceProvider(); + this.clientResourceController = serviceProvider.GetRequiredService(); + } + + /// + /// Initializes a new instance of the class with explicit dependencies. + /// + /// The business controller provider. + /// The client resource controller. + public SpaModuleControl(IBusinessControllerProvider businessControllerProvider, IClientResourceController clientResourceController) + : base() + { + this.businessControllerProvider = businessControllerProvider; + this.clientResourceController = clientResourceController; + } + + /// + /// Gets the HTML5 control source file for this module. + /// + public string Html5File => this.ModuleConfiguration.ModuleControl.ControlSrc; + + /// + /// Configures page resources required by the SPA module (scripts and styles). + /// + /// The page configuration context. + public void ConfigurePage(PageConfigurationContext context) + { + context.ServicesFramework.RequestAjaxScriptSupport(); + if (!string.IsNullOrEmpty(this.Html5File)) + { + // Check if css file exists + var cssFile = Path.ChangeExtension(this.Html5File, ".css"); + if (this.FileExists(cssFile)) + { + context.ClientResourceController.CreateStylesheet(cssFile).Register(); + } + } + + if (!string.IsNullOrEmpty(this.Html5File)) + { + // Check if js file exists + var jsFile = Path.ChangeExtension(this.Html5File, ".js"); + if (this.FileExists(jsFile)) + { + context.ClientResourceController.CreateScript(jsFile).Register(); + } + } + } + + /// + /// Renders the SPA module by loading and token-replacing the HTML5 file content. + /// + /// The MVC HTML helper. + /// The rendered HTML. + public override IHtmlString Html(HtmlHelper htmlHelper) + { + var fileContent = string.Empty; + if (!string.IsNullOrEmpty(this.Html5File)) + { + fileContent = this.GetFileContent(this.Html5File); + var moduleActions = new ModuleActionCollection(); + var tokenReplace = new Html5ModuleTokenReplace(null, htmlHelper.ViewContext.HttpContext.Request, this.clientResourceController, this.businessControllerProvider, this.Html5File, this.ModuleContext, moduleActions); + fileContent = tokenReplace.ReplaceEnvironmentTokens(fileContent); + } + + return new HtmlString(HttpUtility.HtmlDecode(fileContent)); + } + + private static string GetFileContentInternal(string filepath) + { + using (var reader = new StreamReader(filepath)) + { + return reader.ReadToEnd(); + } + } + + private string GetFileContent(string filepath) + { + var cacheKey = string.Format(DataCache.SpaModulesContentHtmlFileCacheKey, filepath); + var absoluteFilePath = HttpContext.Current.Server.MapPath("/" + filepath); + var cacheItemArgs = new CacheItemArgs(cacheKey, DataCache.SpaModulesHtmlFileTimeOut, DataCache.SpaModulesHtmlFileCachePriority) + { + CacheDependency = new DNNCacheDependency(absoluteFilePath), + }; + return CBO.GetCachedObject(cacheItemArgs, c => GetFileContentInternal(absoluteFilePath)); + } + + private bool FileExists(string filepath) + { + var cacheKey = string.Format(DataCache.SpaModulesFileExistsCacheKey, filepath); + return CBO.GetCachedObject( + new CacheItemArgs( + cacheKey, + DataCache.SpaModulesHtmlFileTimeOut, + DataCache.SpaModulesHtmlFileCachePriority), + c => File.Exists(HttpContext.Current.Server.MapPath("/" + filepath))); + } + } +} \ No newline at end of file diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/WebForms/WrapperModule.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/WebForms/WrapperModule.cs new file mode 100644 index 00000000000..4333595223d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/WebForms/WrapperModule.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.WebForms +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Web; + using System.Web.Mvc.Html; + using System.Web.UI; + using System.Web.UI.WebControls; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Pages; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Framework; + using DotNetNuke.Instrumentation; + using DotNetNuke.Services.Installer.Log; + using DotNetNuke.UI.Skins; + using DotNetNuke.UI.Skins.Controls; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + using DotNetNuke.Web.MvcPipeline.Utils; + using Microsoft.Extensions.DependencyInjection; + + /// + /// WebForms wrapper module that hosts an MVC module control inside a classic DNN module. + /// + public class WrapperModule : PortalModuleBase, IActionable + { + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(WrapperModule)); + + private string html = string.Empty; + + /// + public ModuleActionCollection ModuleActions { get; private set; } = new ModuleActionCollection(); + + /// + /// Ensures child controls are created and wired up for the wrapped MVC output. + /// + protected override void CreateChildControls() + { + this.Controls.Clear(); + this.Controls.Add(new LiteralControl(this.html)); + + // important so ASP.NET tracks the created controls across postbacks + this.ChildControlsCreated = true; + base.CreateChildControls(); + } + + /// + /// Ensures the wrapped MVC module control is created and rendered early in the page lifecycle. + /// + /// The event arguments. + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + try + { + var mc = ModuleControlFactory.CreateModuleControl(this.ModuleConfiguration); + this.html = MvcViewEngine.RenderHtmlHelperToString(helper => mc.Html(helper)); + if (mc is IActionable actionable) + { + this.ModuleActions = actionable.ModuleActions; + } + + if (mc is IPageContributor pageContributor) + { + pageContributor.ConfigurePage(new PageConfigurationContext(this.DependencyProvider)); + } + } + catch (Exception ex) + { + Logger.Error(ex); + Skin.AddModuleMessage(this, "An error occurred while loading the module. Please contact the site administrator.", ModuleMessage.ModuleMessageType.RedError); + this.html = "
" + ex.Message + "
"; + } + + this.EnsureChildControls(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleActionsControl.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleActionsControl.cs new file mode 100644 index 00000000000..065d7a58de2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleActionsControl.cs @@ -0,0 +1,325 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Modules +{ + using System; + using System.Collections.Generic; + using System.Web.Script.Serialization; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Instrumentation; + using DotNetNuke.Security; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Personalization; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + + /// + /// Razor-based control responsible for rendering the module actions (gear) menu. + /// + public class ModuleActionsControl : RazorModuleControlBase, IPageContributor + { + private readonly ILog logger = LoggerSource.Instance.GetLogger(typeof(ModuleActionsControl)); + private readonly List validIDs = new List(); + + private readonly Dictionary actionScripts = new Dictionary(); + private ModuleAction actionRoot; + + /// + /// Gets a value indicating whether the current user is in edit mode. + /// + public bool EditMode + { + get + { + return Personalization.GetUserMode() != PortalSettings.Mode.View; + } + } + + /// + /// Gets the root action used to build the actions tree. + /// + protected ModuleAction ActionRoot + { + get + { + if (this.actionRoot == null) + { + this.actionRoot = new ModuleAction(this.ModuleContext.GetNextActionID(), Localization.GetString("Manage.Text", Localization.GlobalResourceFile), string.Empty, string.Empty, "manage-icn.png"); + } + + return this.actionRoot; + } + } + + /// + /// Gets the localized text for the generic (admin) actions group. + /// + protected string AdminText + { + get { return Localization.GetString("ModuleGenericActions.Action", Localization.GlobalResourceFile); } + } + + /// + /// Gets the localized text for the module-specific actions group. + /// + protected string CustomText + { + get { return Localization.GetString("ModuleSpecificActions.Action", Localization.GlobalResourceFile); } + } + + /// + /// Gets the localized text used for move operations. + /// + protected string MoveText + { + get { return Localization.GetString(ModuleActionType.MoveRoot, Localization.GlobalResourceFile); } + } + + /// + /// Gets or sets the serialized JSON representing administrative actions. + /// + protected string AdminActionsJSON { get; set; } + + /// + /// Gets or sets the serialized JSON representing custom actions. + /// + protected string CustomActionsJSON { get; set; } + + /// + /// Gets or sets a value indicating whether quick settings are displayed. + /// + protected bool DisplayQuickSettings { get; set; } + + /// + /// Gets or sets the serialized JSON list of panes. + /// + protected string Panes { get; set; } + + /// + /// Gets or sets a value indicating whether the module supports moving between panes. + /// + protected bool SupportsMove { get; set; } + + /// + /// Gets or sets a value indicating whether the module supports quick settings. + /// + protected bool SupportsQuickSettings { get; set; } + + /// + /// Gets or sets a value indicating whether the module is shared across tabs or portals. + /// + protected bool IsShared { get; set; } + + /// + /// Gets or sets the module title shown in the actions UI. + /// + protected string ModuleTitle { get; set; } + + /// + /// Gets the module action collection for the current module. + /// + protected ModuleActionCollection Actions + { + get + { + return this.ModuleControl.ModuleContext.Actions; + } + } + + /// + /// Gets or sets the current module control. + /// + public IModuleControl ModuleControl { get; set; } + + /// + /// Executes the Razor module control and returns the result. + /// + /// The Razor module result. + public override IRazorModuleResult Invoke() + { + var moduleInfo = ModuleConfiguration; + this.OnLoad(moduleInfo); + + var viewModel = new Models.ModuleActionsModel + { + ModuleContext = moduleInfo, + SupportsQuickSettings = this.SupportsQuickSettings, + DisplayQuickSettings = this.DisplayQuickSettings, + // QuickSettingsModel = this.qu, + CustomActionsJSON = this.CustomActionsJSON, + AdminActionsJSON = this.AdminActionsJSON, + Panes = this.Panes, + CustomText = this.CustomText, + AdminText = this.AdminText, + MoveText = this.MoveText, + SupportsMove = this.SupportsMove, + IsShared = this.IsShared, + ModuleTitle = moduleInfo.ModuleTitle, + ActionScripts = this.actionScripts, + }; + + return View("ModuleActions", viewModel); + } + + /// + /// Gets a localized string from the global resource file. + /// + /// The resource key. + /// The localized string. + protected string LocalizeString(string key) + { + return Localization.GetString(key, Localization.GlobalResourceFile); + } + + /// + /// Populates the actions model based on the provided module configuration. + /// + /// The module information. + protected void OnLoad(ModuleInfo moduleInfo) + { + this.ActionRoot.Actions.AddRange(this.Actions); + this.AdminActionsJSON = "[]"; + this.CustomActionsJSON = "[]"; + this.Panes = "[]"; + try + { + this.SupportsQuickSettings = false; + this.DisplayQuickSettings = false; + this.ModuleTitle = this.ModuleContext.Configuration.ModuleTitle; + var moduleDefinitionId = this.ModuleContext.Configuration.ModuleDefID; + var quickSettingsControl = ModuleControlController.GetModuleControlByControlKey("QuickSettings", moduleDefinitionId); + + if (quickSettingsControl != null) + { + this.SupportsQuickSettings = true; + /* + var control = ModuleControlFactory.LoadModuleControl(this.Page, this.ModuleContext.Configuration, "QuickSettings", quickSettingsControl.ControlSrc); + control.ID += this.ModuleContext.ModuleId; + this.quickSettings.Controls.Add(control); + + this.DisplayQuickSettings = this.ModuleContext.Configuration.ModuleSettings.GetValueOrDefault("QS_FirstLoad", true); + ModuleController.Instance.UpdateModuleSetting(this.ModuleContext.ModuleId, "QS_FirstLoad", "False"); + + ClientResourceManager.RegisterScript(this.Page, "~/admin/menus/ModuleActions/dnnQuickSettings.js"); + */ + } + + if (this.ActionRoot.Visible && Globals.IsAdminControl() == false) + { + // Add Menu Items + foreach (ModuleAction rootAction in this.ActionRoot.Actions) + { + // Process Children + var actions = new List(); + foreach (ModuleAction action in rootAction.Actions) + { + if (action.Visible) + { + if (this.EditMode && Globals.IsAdminControl() == false || + action.Secure != SecurityAccessLevel.Anonymous && action.Secure != SecurityAccessLevel.View) + { + if (!action.Icon.Contains("://") + && !action.Icon.StartsWith("/") + && !action.Icon.StartsWith("~/")) + { + action.Icon = "~/images/" + action.Icon; + } + + if (action.Icon.StartsWith("~/")) + { + action.Icon = Globals.ResolveUrl(action.Icon); + } + + actions.Add(action); + + if (string.IsNullOrEmpty(action.Url)) + { + this.validIDs.Add(action.ID); + } + } + } + + if (string.IsNullOrEmpty(action.ClientScript) && !string.IsNullOrEmpty(action.Url) && action.Url.StartsWith("javascript:")) + { + if (!DotNetNuke.UI.UIUtilities.IsLegacyUI(this.ModuleContext.ModuleId, action.ControlKey, this.ModuleContext.PortalId)) + { + action.ClientScript = UrlUtils.PopUpUrl(action.Url, null, this.PortalSettings, true, false); + } + } + + if (!string.IsNullOrEmpty(action.ClientScript) && !string.IsNullOrEmpty(action.Url)) + { + this.actionScripts.Add(action.Url, action.ClientScript); + } + } + + var oSerializer = new JavaScriptSerializer(); + if (rootAction.Title == Localization.GetString("ModuleGenericActions.Action", Localization.GlobalResourceFile)) + { + this.AdminActionsJSON = oSerializer.Serialize(actions); + } + else + { + if (rootAction.Title == Localization.GetString("ModuleSpecificActions.Action", Localization.GlobalResourceFile)) + { + this.CustomActionsJSON = oSerializer.Serialize(actions); + } + else + { + this.SupportsMove = actions.Count > 0; + this.Panes = oSerializer.Serialize(this.PortalSettings.ActiveTab.Panes); + } + } + } + + this.IsShared = this.ModuleContext.Configuration.AllTabs + || PortalGroupController.Instance.IsModuleShared(this.ModuleContext.ModuleId, PortalController.Instance.GetPortal(this.PortalSettings.PortalId)) + || TabController.Instance.GetTabsByModuleID(this.ModuleContext.ModuleId).Count > 1; + + this.SupportsMove = true; + } + + } + catch (Exception exc) + { + // Exceptions.ProcessModuleLoadException(this, exc); + throw exc; + } + } + + /// + /// Configures the page by registering resources required by the actions UI. + /// + /// The page configuration context. + public void ConfigurePage(PageConfigurationContext context) + { + context.ClientResourceController + .CreateStylesheet("~/admin/menus/ModuleActions/ModuleActions.css") + .SetPriority(FileOrder.Css.ModuleCss) + .Register(); + context.ClientResourceController + .CreateStylesheet("~/Resources/Shared/stylesheets/dnnicons/css/dnnicon.min.css") + .SetPriority(FileOrder.Css.ModuleCss) + .Register(); + context.ClientResourceController + .CreateScript("~/admin/menus/ModuleActions/ModuleActions.js") + .Register(); + context.JavaScriptLibraryHelper.RequestRegistration(CommonJs.DnnPlugins); + context.ServicesFramework.RequestAjaxAntiForgerySupport(); + } + + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.DnnLabel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.DnnLabel.cs new file mode 100644 index 00000000000..91f897c87d9 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.DnnLabel.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Modules +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + + using DotNetNuke.Services.Localization; + + /// + /// HTML helper extensions for rendering DNN-style labels with help icons. + /// + public static partial class ModuleHelpers + { + /// + /// Renders a DNN label with help icon for the specified model expression, using the given resource file for localization. + /// + /// The model type. + /// The property type. + /// The strongly-typed HTML helper. + /// The expression identifying the field. + /// The localization resource file used to resolve the label text. + /// The rendered label HTML. + public static IHtmlString DnnLabelFor(this HtmlHelper htmlHelper, Expression> expression, string resourceFile) + { + // HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes) + var name = htmlHelper.DisplayNameFor(expression).ToString(); + if (string.IsNullOrEmpty(name)) + { + name = htmlHelper.NameFor(expression).ToString(); + } + + name = Localization.GetString(name, resourceFile); + + var attrs = new Dictionary(); + + var div = new TagBuilder("div"); + div.AddCssClass("dnnLabel"); + div.Attributes["style"] = "position: relative;"; + var aHelp = new TagBuilder("a"); + aHelp.AddCssClass("dnnFormHelp"); + + div.InnerHtml += htmlHelper.LabelFor(expression, name, attrs).ToString(); + div.InnerHtml += aHelp.ToString(); + + /* + < a id = "dnn_ctr385_ModuleSettings_plTitle_cmdHelp" tabindex = "-1" class="dnnFormHelp" aria-label="Help" href="javascript:__doPostBack('dnn$ctr385$ModuleSettings$plTitle$cmdHelp','')"> +
+
+ Saisissez un titre pour ce module.Il apparaitra dans la barre de titre du container utilisé pour ce module, si cette fonction est supportée par le container. + +
+
+ */ + + return MvcHtmlString.Create(div.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.ModuleMessage.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.ModuleMessage.cs new file mode 100644 index 00000000000..65021ef797e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.ModuleMessage.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Modules +{ + using System; + using System.Collections.Generic; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + + /// + /// HTML helpers for rendering standardized DNN module messages. + /// + public static partial class ModuleHelpers + { + /// + /// Creates a DNN skin message panel with optional heading and automatic scrolling. + /// + /// The HTML helper instance. + /// The message text to display. + /// The type of message (affects CSS classes). + /// Optional heading text. + /// Whether to automatically scroll to the message. + /// Additional HTML attributes for the panel. + /// HTML string for the skin message panel. + public static IHtmlString ModuleMessage( + this HtmlHelper htmlHelper, + string message, + ModuleMessageType messageType = ModuleMessageType.Info, + string heading = null, + bool autoScroll = true, + object htmlAttributes = null) + { + if (string.IsNullOrEmpty(message)) + { + return MvcHtmlString.Empty; + } + + var cssClass = GetMessageCssClass(messageType); + + // Create the main panel + var panelBuilder = new TagBuilder("div"); + panelBuilder.GenerateId("dnnSkinMessage"); + panelBuilder.AddCssClass("dnnFormMessage"); + panelBuilder.AddCssClass(cssClass); + + // Merge additional HTML attributes + if (htmlAttributes != null) + { + var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); + panelBuilder.MergeAttributes(attributes); + } + + var innerHtml = string.Empty; + + // Add heading if provided + if (!string.IsNullOrEmpty(heading)) + { + var headingBuilder = new TagBuilder("h6"); + headingBuilder.SetInnerText(heading); + innerHtml += headingBuilder.ToString(); + } + + // Add message + var messageBuilder = new TagBuilder("div"); + messageBuilder.SetInnerText(message); + innerHtml += messageBuilder.ToString(); + + panelBuilder.InnerHtml = innerHtml; + + var result = panelBuilder.ToString(); + + // Add auto-scroll script if requested + if (autoScroll) + { + MvcClientAPI.RegisterScript("scrollScript", GenerateScrollScript()); + } + + return MvcHtmlString.Create(result); + } + + /// + /// Convenience method for creating an info message. + /// + public static IHtmlString ModuleInfoMessage(this HtmlHelper htmlHelper, string message, string heading = null, bool autoScroll = true) + { + return htmlHelper.ModuleMessage(message, ModuleMessageType.Info, heading, autoScroll); + } + + /// + /// Convenience method for creating a success message. + /// + public static IHtmlString ModuleSuccessMessage(this HtmlHelper htmlHelper, string message, string heading = null, bool autoScroll = true) + { + return htmlHelper.ModuleMessage(message, ModuleMessageType.Success, heading, autoScroll); + } + + /// + /// Convenience method for creating a warning message. + /// + public static IHtmlString ModuleWarningMessage(this HtmlHelper htmlHelper, string message, string heading = null, bool autoScroll = true) + { + return htmlHelper.ModuleMessage(message, ModuleMessageType.Warning, heading, autoScroll); + } + + /// + /// Convenience method for creating an error message. + /// + public static IHtmlString ModuleErrorMessage(this HtmlHelper htmlHelper, string message, string heading = null, bool autoScroll = true) + { + return htmlHelper.ModuleMessage(message, ModuleMessageType.Error, heading, autoScroll); + } + + private static string GetMessageCssClass(ModuleMessageType messageType) + { + switch (messageType) + { + case ModuleMessageType.Success: + return "dnnFormSuccess"; + case ModuleMessageType.Warning: + return "dnnFormWarning"; + case ModuleMessageType.Error: + return "dnnFormError"; + case ModuleMessageType.Info: + default: + return "dnnFormInfo"; + } + } + + private static string GenerateScrollScript() + { + return $@" + jQuery(document).ready(function ($) {{ + var $body = window.opera ? (document.compatMode == ""CSS1Compat"" ? $('html') : $('body')) : $('html,body'); + var $message = $('.dnnFormMessage'); + if ($message.length > 0) {{ + var scrollTop = $message.offset().top - parseInt($(document.body).css(""margin-top"")); + $body.animate({{ scrollTop: scrollTop }}, 'fast'); + }} + }});"; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.cs new file mode 100644 index 00000000000..38cccf07e6b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Modules +{ + using System; + using System.Collections; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.MvcPipeline.Controllers; + using Microsoft.Extensions.DependencyInjection; + + /// + /// HTML helpers for working with DNN MVC modules (localization, edit URLs, and partial views). + /// + public static partial class ModuleHelpers + { + /// + /// Gets a localized string for the specified key and resource file. + /// + /// The HTML helper. + /// The localization key. + /// The resource file path. + /// The localized string as an HTML string. + public static IHtmlString LocalizeString(this HtmlHelper htmlHelper, string key, string localResourceFile) + { + return MvcHtmlString.Create(Localization.GetString(key, localResourceFile)); + } + + /// + /// Gets a localized string for the specified key using the LocalResourceFile in . + /// + /// The HTML helper. + /// The localization key. + /// The localized string as an HTML string. + public static IHtmlString LocalizeString(this HtmlHelper htmlHelper, string key) + { + if (htmlHelper.ViewContext.ViewData["LocalResourceFile"] == null) + { + throw new InvalidOperationException("The LocalResourceFile must be set in the ViewData to use this helper."); + } + var localResourceFile = (string)htmlHelper.ViewContext.ViewData["LocalResourceFile"]; + return MvcHtmlString.Create(Localization.GetString(key, localResourceFile)); + } + + /// + /// Builds an edit URL for the current module. + /// + /// The HTML helper. + /// The edit URL as an HTML string. + public static IHtmlString EditUrl(this HtmlHelper htmlHelper) + { + if (htmlHelper.ViewContext.ViewData["ModuleContext"] == null) + { + throw new InvalidOperationException("The ModuleContext must be set in the ViewData to use this helper."); + } + var moduleContext = (ModuleInstanceContext)htmlHelper.ViewContext.ViewData["ModuleContext"]; + return MvcHtmlString.Create(moduleContext.EditUrl()); + } + + /// + /// Builds an edit URL for the current module and specified control key. + /// + /// The HTML helper. + /// The control key. + /// The edit URL as an HTML string. + public static IHtmlString EditUrl(this HtmlHelper htmlHelper, string controlKey) + { + if (htmlHelper.ViewContext.ViewData["ModuleContext"] == null) + { + throw new InvalidOperationException("The ModuleContext must be set in the ViewData to use this helper."); + } + var moduleContext = (ModuleInstanceContext)htmlHelper.ViewContext.ViewData["ModuleContext"]; + return MvcHtmlString.Create(moduleContext.EditUrl(controlKey)); + } + + /// + /// Builds an edit URL for the current module with a single key/value route parameter. + /// + /// The HTML helper. + /// The query string key. + /// The query string value. + /// The edit URL as an HTML string. + public static IHtmlString EditUrl(this HtmlHelper htmlHelper, string keyName, string keyValue) + { + if (htmlHelper.ViewContext.ViewData["ModuleContext"] == null) + { + throw new InvalidOperationException("The ModuleContext must be set in the ViewData to use this helper."); + } + var moduleContext = (ModuleInstanceContext)htmlHelper.ViewContext.ViewData["ModuleContext"]; + return MvcHtmlString.Create(moduleContext.EditUrl(keyName, keyValue)); + } + + /// + /// Builds an edit URL for the current module with a key/value parameter and control key. + /// + /// The HTML helper. + /// The query string key. + /// The query string value. + /// The control key. + /// The edit URL as an HTML string. + public static IHtmlString EditUrl(this HtmlHelper htmlHelper, string keyName, string keyValue, string controlKey) + { + if (htmlHelper.ViewContext.ViewData["ModuleContext"] == null) + { + throw new InvalidOperationException("The ModuleContext must be set in the ViewData to use this helper."); + } + var moduleContext = (ModuleInstanceContext)htmlHelper.ViewContext.ViewData["ModuleContext"]; + return MvcHtmlString.Create(moduleContext.EditUrl(keyName, keyValue, controlKey)); + } + + /// + /// Builds an edit URL for the current module with a key/value parameter, control key, and additional route parameters. + /// + /// The HTML helper. + /// The query string key. + /// The query string value. + /// The control key. + /// Additional route parameters. + /// The edit URL as an HTML string. + public static IHtmlString EditUrl(this HtmlHelper htmlHelper, string keyName, string keyValue, string controlKey, params string[] additionalParameters) + { + if (htmlHelper.ViewContext.ViewData["ModuleContext"] == null) + { + throw new InvalidOperationException("The ModuleContext must be set in the ViewData to use this helper."); + } + var moduleContext = (ModuleInstanceContext)htmlHelper.ViewContext.ViewData["ModuleContext"]; + return MvcHtmlString.Create(moduleContext.EditUrl(keyName, keyValue, controlKey, additionalParameters)); + } + + /// + /// Renders a partial view for the current module using the module's folder path. + /// + /// The HTML helper. + /// The name of the partial view. + /// The model to pass to the partial view. + /// The partial view HTML. + public static MvcHtmlString ModulePartial(this HtmlHelper htmlHelper, string partialViewName, object model = null) + { + if (htmlHelper.ViewContext.ViewData["ModuleContext"] == null) + { + throw new InvalidOperationException("The ModuleContext must be set in the ViewData to use this helper."); + } + var moduleContext = (ModuleInstanceContext)htmlHelper.ViewContext.ViewData["ModuleContext"]; + + var viewPath = string.Format("~/DesktopModules/{0}/Views/{1}.cshtml", moduleContext.Configuration.DesktopModule.FolderName, partialViewName); + + return htmlHelper.Partial(viewPath, model); + } + + /// + /// Renders a partial view for the current module using the module's folder path with explicit view data. + /// + /// The HTML helper. + /// The name of the partial view. + /// The model to pass to the partial view. + /// The view data dictionary. + /// The partial view HTML. + public static MvcHtmlString ModulePartial(this HtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary dic) + { + if (htmlHelper.ViewContext.ViewData["ModuleContext"] == null) + { + throw new InvalidOperationException("The ModuleContext must be set in the ViewData to use this helper."); + } + var moduleContext = (ModuleInstanceContext)htmlHelper.ViewContext.ViewData["ModuleContext"]; + + var viewPath = string.Format("~/DesktopModules/{0}/Views/{1}.cshtml", moduleContext.Configuration.DesktopModule.FolderName, partialViewName); + + return htmlHelper.Partial(viewPath, model, dic); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleMessageType.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleMessageType.cs new file mode 100644 index 00000000000..39311481279 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleMessageType.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Modules +{ + /// + /// Enum for different types of skin messages. + /// + public enum ModuleMessageType + { + /// + /// Information message. + /// + Info, + + /// + /// Success message. + /// + Success, + + /// + /// Warning message. + /// + Warning, + + /// + /// Error message. + /// + Error, + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/MoluleHelpers.Actions.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/MoluleHelpers.Actions.cs new file mode 100644 index 00000000000..ad63b2766ab --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/MoluleHelpers.Actions.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Modules +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.UI.Modules; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + using DotNetNuke.Web.MvcPipeline.Utils; + + /// + /// HTML helpers for rendering module action menus. + /// + public static partial class ModuleHelpers + { + /// + /// Renders the module actions UI for the specified MVC module control. + /// + /// The HTML helper. + /// The MVC module control. + /// The rendered module actions HTML. + public static IHtmlString ModuleActions(this HtmlHelper htmlHelper, IMvcModuleControl moduleControl) + { + var actionsControl = new ModuleActionsControl(); + actionsControl.ConfigurePage(new PageConfigurationContext(Common.Globals.GetCurrentServiceProvider())); + actionsControl.ModuleContext.Configuration = moduleControl.ModuleContext.Configuration; + + try + { + actionsControl.ModuleControl = moduleControl; + return actionsControl.Html(htmlHelper); + } + catch (Exception) + { + return new MvcHtmlString(string.Empty); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/PermissionTriStateHelper.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/PermissionTriStateHelper.cs new file mode 100644 index 00000000000..0a85d4e5d7b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/PermissionTriStateHelper.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Modules +{ + using System; + using System.Collections; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Entities.Icons; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + using Microsoft.Extensions.DependencyInjection; + + /// + /// HTML helpers and utilities for rendering and initializing tri-state permission controls. + /// + public static class PermissionTriStateHelper + { + /// + /// Renders a tri-state permission control for a given permission value. + /// + /// The HTML helper. + /// The name/id for the hidden input. + /// The current permission value (True, False, or Null). + /// Whether this permission represents full control. + /// Whether this is a view permission. + /// Whether the permission is locked. + /// Whether deny mode is supported for this permission. + /// An optional permission key used to add a CSS class. + /// The HTML markup for the tri-state control. + public static MvcHtmlString PermissionTriState( + this HtmlHelper helper, + string name, + string value, + bool isFullControl = false, + bool isView = false, + bool locked = false, + bool supportsDenyMode = true, + string permissionKey = "") + { + const string scriptKey = "initTriState"; + JavaScript.RequestRegistration(CommonJs.jQuery); + var controller = GetClientResourcesController(); + controller.RegisterScript("/js/dnn.permissiontristate.js"); + MvcClientAPI.RegisterStartupScript(scriptKey, GetInitScript()); + + var grantImagePath = IconController.IconURL("Grant"); + var denyImagePath = IconController.IconURL("Deny"); + var nullImagePath = IconController.IconURL("Unchecked"); + var lockImagePath = IconController.IconURL("Lock"); + + var grantAltText = Localization.GetString("PermissionTypeGrant"); + var denyAltText = Localization.GetString("PermissionTypeDeny"); + var nullAltText = Localization.GetString("PermissionTypeNull"); + + string imagePath; + string altText; + switch (value) + { + case "True": + imagePath = grantImagePath; + altText = grantAltText; + break; + case "False": + imagePath = denyImagePath; + altText = denyAltText; + break; + default: + imagePath = nullImagePath; + altText = nullAltText; + break; + } + + var cssClass = "tristate"; + if (locked) + { + imagePath = lockImagePath; + cssClass += " lockedPerm"; + } + + if (!supportsDenyMode) + { + cssClass += " noDenyPerm"; + } + + if (isFullControl) + { + cssClass += " fullControl"; + } + + if (isView && !locked) + { + cssClass += " view"; + } + + if (!string.IsNullOrEmpty(permissionKey) && !isView && !isFullControl) + { + cssClass += " " + permissionKey.ToLowerInvariant(); + } + + var img = new TagBuilder("img"); + img.MergeAttribute("src", imagePath); + img.MergeAttribute("alt", altText); + + var hidden = new TagBuilder("input"); + hidden.MergeAttribute("type", "hidden"); + hidden.MergeAttribute("name", name); + hidden.MergeAttribute("value", value); + hidden.MergeAttribute("class", cssClass); + hidden.MergeAttribute("id", name); + + return MvcHtmlString.Create(img.ToString(TagRenderMode.SelfClosing) + hidden.ToString(TagRenderMode.SelfClosing)); + } + + /// + /// Gets the JavaScript used to initialize tri-state permission controls on the page. + /// + /// The initialization script. + public static string GetInitScript() + { + string grantImagePath, denyImagePath, nullImagePath, lockImagePath, grantAltText, denyAltText, nullAltText; + + LookupScriptValues(out grantImagePath, out denyImagePath, out nullImagePath, out lockImagePath, out grantAltText, out denyAltText, out nullAltText); + + var script = + string.Format( + @"jQuery(document).ready( + function() {{ + var images = {{ 'True': '{0}', 'False': '{1}', 'Null': '{2}' }}; + var toolTips = {{ 'True': '{3}', 'False': '{4}', 'Null': '{5}' }}; + var tsm = dnn.controls.triStateManager(images, toolTips); + jQuery('.tristate').each( function(i, elem) {{ + tsm.initControl( elem ); + }}); + }});", + grantImagePath, + denyImagePath, + nullImagePath, + grantAltText, + denyAltText, + nullAltText); + + return script; + } + + private static void LookupScriptValues(out string grantImagePath, out string denyImagePath, out string nullImagePath, out string lockImagePath, out string grantAltText, out string denyAltText, out string nullAltText) + { + grantImagePath = IconController.IconURL("Grant"); + denyImagePath = IconController.IconURL("Deny"); + nullImagePath = IconController.IconURL("Unchecked"); + lockImagePath = IconController.IconURL("Lock"); + + grantAltText = Localization.GetString("PermissionTypeGrant"); + denyAltText = Localization.GetString("PermissionTypeDeny"); + nullAltText = Localization.GetString("PermissionTypeNull"); + } + + private static IClientResourceController GetClientResourcesController() + { + var serviceProvider = DotNetNuke.Common.Globals.GetCurrentServiceProvider(); + return serviceProvider.GetRequiredService(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Pages/HtmlHelpers.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Pages/HtmlHelpers.cs new file mode 100644 index 00000000000..2b939bec00c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Pages/HtmlHelpers.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Pages +{ + using System; + using System.IO; + using System.Linq; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.Pages; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Framework; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.UI.Skins; + using DotNetNuke.Web.Client.ClientResourceManagement; + using DotNetNuke.Web.MvcPipeline.Framework; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.Models; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + using DotNetNuke.Web.MvcPipeline.Utils; + + /// + /// HTML helper extensions for rendering MVC pipeline page-level elements. + /// + public static partial class HtmlHelpers + { + + /// + /// Renders all head tags (scripts, styles, meta tags) registered for the current page. + /// + /// The HTML helper for the current . + /// An HTML string containing the rendered head tags. + public static IHtmlString RenderHeadTags(this HtmlHelper helper) + { + var pageService = helper.ViewData.Model.PageService; + var headTags = new StringBuilder(); + foreach (var item in pageService.GetHeadTags()) + { + headTags.Append(item.Value); + } + foreach (var item in pageService.GetMetaTags()) + { + var tag = new TagBuilder("meta"); + tag.Attributes.Add("name", item.Name); + tag.Attributes.Add("content", item.Content); + headTags.Append(tag.ToString()); + } + return new MvcHtmlString(headTags.ToString()); + } + + /// + /// Renders page-level messages (errors, warnings, info, success) using DNN message styles. + /// + /// The HTML helper for the current . + /// An HTML string containing the rendered messages, or an empty string if there are no messages. + public static IHtmlString RenderPageMessages(this HtmlHelper helper) + { + var pageService = helper.ViewData.Model.PageService; + var messages = pageService.GetMessages(); + if (messages.Any()) + { + var outer = new TagBuilder("div"); + outer.Attributes["id"] = "dnnSkinMessage"; + + foreach (var msg in pageService.GetMessages()) + { + var wrapper = new TagBuilder("div"); + + var panel = new TagBuilder("div"); + + switch (msg.MessageType) + { + case PageMessageType.Error: + panel.AddCssClass("dnnFormError"); + break; + case PageMessageType.Warning: + panel.AddCssClass("dnnFormWarning"); + break; + case PageMessageType.Success: + panel.AddCssClass("dnnFormSuccess"); + break; + case PageMessageType.Info: + panel.AddCssClass("dnnFormInfo"); + break; + } + panel.AddCssClass("dnnFormMessage"); + if (!string.IsNullOrEmpty(msg.Heading)) + { + var headingSpan = new TagBuilder("span"); + headingSpan.SetInnerText(msg.Heading); + headingSpan.AddCssClass("dnnModMessageHeading"); + panel.InnerHtml += headingSpan.ToString(); + } + + var messageDiv = new TagBuilder("span"); + messageDiv.InnerHtml = msg.Message; + panel.InnerHtml += messageDiv.ToString(); + + wrapper.InnerHtml = panel.ToString(); + outer.InnerHtml += wrapper.ToString(); + } + + var script = + "jQuery(document).ready(function ($) {" + + "var $body = window.opera ? (document.compatMode == \"CSS1Compat\" ? $('html') : $('body')) : $('html,body');" + + "var scrollTop = $('#dnnSkinMessage').offset().top - parseInt($(document.body).css(\"margin-top\"));" + + "$body.animate({ scrollTop: scrollTop }, 'fast');" + + "});"; + + MvcClientAPI.RegisterScript("dnnSkinMessage", script); + + return new MvcHtmlString(outer.ToString()); + } + else + { + return new MvcHtmlString(string.Empty); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ConstraintValidation.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ConstraintValidation.cs new file mode 100644 index 00000000000..9d6ee500616 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ConstraintValidation.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Web.Routing; + + /// Validates that the constraints on a Route are of a type that can be processed by . + /// + /// This validation is only applicable when the is one that we created. A user-defined + /// type that is derived from may have different semantics. + /// + /// The logic here is duplicated from System.Web, but we need it to validate correctness of routes on startup. + /// Since we can't change System.Web, this just lives in a static class for MVC. + /// + /// + internal static class ConstraintValidation + { + /// + /// Validates that all route constraints are either strings or implement . + /// + /// The route to validate. + /// + /// Thrown when a constraint value is neither a string nor an instance. + /// + public static void Validate(Route route) + { + Contract.Assert(route != null); + Contract.Assert(route.Url != null); + + if (route.Constraints == null) + { + return; + } + + foreach (var kvp in route.Constraints.Where(kvp => !(kvp.Value is string)).Where(kvp => !(kvp.Value is IRouteConstraint))) + { + throw new InvalidOperationException("Invalid Constraint", new Exception(typeof(IRouteConstraint).FullName)); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageHandler.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageHandler.cs new file mode 100644 index 00000000000..f7f406dccdc --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageHandler.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Web; + using System.Web.Mvc; + using System.Web.Routing; + using System.Web.SessionState; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.HttpModules.Membership; + using DotNetNuke.Services.Localization; + + /// + /// MVC handler used by the DNN MVC pipeline to process page requests with proper culture and authentication. + /// + public class DnnMvcPageHandler : MvcHandler, IHttpHandler, IRequiresSessionState + { + /// + /// Initializes a new instance of the class. + /// + /// The current request context. + public DnnMvcPageHandler(RequestContext requestContext) + : base(requestContext) + { + } + + /// + protected override void ProcessRequest(HttpContext httpContext) + { + this.SetThreadCulture(); + MembershipModule.AuthenticateRequest(this.RequestContext.HttpContext, allowUnknownExtensions: true); + base.ProcessRequest(httpContext); + } + + /// + protected override IAsyncResult BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, object state) + { + this.SetThreadCulture(); + MembershipModule.AuthenticateRequest(this.RequestContext.HttpContext, allowUnknownExtensions: true); + return base.BeginProcessRequest(httpContext, callback, state); + } + + /// + /// Sets the current thread culture based on portal and page locale settings. + /// + private void SetThreadCulture() + { + var portalSettings = PortalController.Instance.GetCurrentSettings(); + if (portalSettings is null) + { + return; + } + + var pageLocale = Localization.GetPageLocale(portalSettings); + if (pageLocale is null) + { + return; + } + + Localization.SetThreadCultures(pageLocale, portalSettings); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageRouteHandler.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageRouteHandler.cs new file mode 100644 index 00000000000..864a3874fef --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageRouteHandler.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Web; + using System.Web.Mvc; + using System.Web.Routing; + using System.Web.SessionState; + + /// + /// Route handler that creates instances for MVC page routes. + /// + public class DnnMvcPageRouteHandler : IRouteHandler + { + private readonly IControllerFactory controllerFactory; + + /// Initializes a new instance of the class. + public DnnMvcPageRouteHandler() + { + } + + /// Initializes a new instance of the class. + /// The controller factory. + public DnnMvcPageRouteHandler(IControllerFactory controllerFactory) + { + this.controllerFactory = controllerFactory; + } + + /// + IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) + { + return this.GetHttpHandler(requestContext); + } + + /// + /// Creates the HTTP handler for the given request context. + /// + /// The current request context. + /// The HTTP handler. + protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) + { + requestContext.HttpContext.SetSessionStateBehavior(this.GetSessionStateBehavior(requestContext)); + return new DnnMvcPageHandler(requestContext); + } + + /// + /// Gets the session state behavior for the controller handling the request. + /// + /// The current request context. + /// The desired session state behavior. + protected virtual SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext) + { + string controllerName = (string)requestContext.RouteData.Values["controller"]; + if (string.IsNullOrWhiteSpace(controllerName)) + { + throw new InvalidOperationException("No Controller"); + } + + IControllerFactory controllerFactory = this.controllerFactory ?? ControllerBuilder.Current.GetControllerFactory(); + return controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpConfigurationExtensions.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpConfigurationExtensions.cs new file mode 100644 index 00000000000..ce93a89a2cf --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpConfigurationExtensions.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Web.Http; + + using DotNetNuke.Common; + + /// + /// Provides extension methods for registering MVC pipeline providers on . + /// + public static class HttpConfigurationExtensions + { + private const string Key = "MvcPipelineTabAndModuleInfoProvider"; + + /// + /// Adds a tab and module information provider to the HTTP configuration. + /// + /// The HTTP configuration. + /// The provider to add. + public static void AddTabAndModuleInfoProvider(this HttpConfiguration configuration, ITabAndModuleInfoProvider tabAndModuleInfoProvider) + { + Requires.NotNull("configuration", configuration); + Requires.NotNull("tabAndModuleInfoProvider", tabAndModuleInfoProvider); + + var providers = configuration.Properties.GetOrAdd(Key, InitValue) as ConcurrentQueue; + + if (providers == null) + { + providers = new ConcurrentQueue(); + configuration.Properties[Key] = providers; + } + + providers.Enqueue(tabAndModuleInfoProvider); + } + + /// + /// Gets the collection of registered tab and module information providers. + /// + /// The HTTP configuration. + /// The registered providers. + public static IEnumerable GetTabAndModuleInfoProviders(this HttpConfiguration configuration) + { + Requires.NotNull("configuration", configuration); + + var providers = configuration.Properties.GetOrAdd(Key, InitValue) as ConcurrentQueue; + + if (providers == null) + { + // shouldn't ever happen outside of unit tests + return new ITabAndModuleInfoProvider[] { }; + } + + return providers.ToArray(); + } + + private static object InitValue(object o) + { + return new ConcurrentQueue(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpRequestExtensions.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpRequestExtensions.cs new file mode 100644 index 00000000000..d7e3209cf93 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpRequestExtensions.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Linq; + using System.Net; + using System.Net.NetworkInformation; + using System.Net.Sockets; + using System.Text; + using System.Web; + using System.Web.Http; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.Services.UserRequest; + + /// + /// Extension methods for to find DNN tab and module information. + /// + internal static class HttpRequestExtensions + { + private delegate bool TryMethod(ITabAndModuleInfoProvider provider, HttpRequestBase request, out T output); + + /// + /// Finds the tab identifier associated with the current request. + /// + /// The HTTP request. + /// The tab identifier, or -1 if not found. + public static int FindTabId(this HttpRequestBase request) + { + return IterateTabAndModuleInfoProviders(request, TryFindTabId, -1); + } + + /// + /// Finds the module information associated with the current request. + /// + /// The HTTP request. + /// The instance, or null if not found. + public static ModuleInfo FindModuleInfo(this HttpRequestBase request) + { + return IterateTabAndModuleInfoProviders(request, TryFindModuleInfo, null); + } + + /// + /// Finds the module identifier associated with the current request. + /// + /// The HTTP request. + /// The module identifier, or -1 if not found. + public static int FindModuleId(this HttpRequestBase request) + { + return IterateTabAndModuleInfoProviders(request, TryFindModuleId, -1); + } + + /// + /// Gets the client IP address for the current request. + /// + /// The HTTP request. + /// The client IP address. + public static string GetIPAddress(HttpRequestBase request) + { + return UserRequestIPAddressController.Instance.GetUserRequestIPAddress(request); + } + + private static bool TryFindTabId(ITabAndModuleInfoProvider provider, HttpRequestBase request, out int output) + { + return provider.TryFindTabId(request, out output); + } + + private static bool TryFindModuleInfo(ITabAndModuleInfoProvider provider, HttpRequestBase request, out ModuleInfo output) + { + return provider.TryFindModuleInfo(request, out output); + } + + private static bool TryFindModuleId(ITabAndModuleInfoProvider provider, HttpRequestBase request, out int output) + { + return provider.TryFindModuleId(request, out output); + } + + /// + /// Iterates over the registered instances to locate tab or module information. + /// + /// The type of value to locate. + /// The HTTP request. + /// The delegate used to attempt to find the value. + /// The fallback value if nothing is found. + /// The located value or the fallback. + private static T IterateTabAndModuleInfoProviders(HttpRequestBase request, TryMethod func, T fallback) + { + var providers = GlobalConfiguration.Configuration.GetTabAndModuleInfoProviders(); + + foreach (var provider in providers) + { + T output; + if (func(provider, request, out output)) + { + return output; + } + } + + return fallback; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMapRoute.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMapRoute.cs new file mode 100644 index 00000000000..9dfae1be767 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMapRoute.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Collections.Generic; + using System.Web.Routing; + + /// + /// Defines an API for mapping MVC routes for DNN modules. + /// + public interface IMapRoute + { + /// Sets up the route(s) for DotNetNuke MVC Controls. + /// The name of the folder under DesktopModules in which your module resides. + /// A unique name for the route. + /// The parameterized portion of the route. + /// The namespace(s) in which to search for the controllers for this route. + /// A list of all routes that were registered. + /// The combination of moduleFolderName and routeName must be unique for each route. + Route MapRoute(string moduleFolderName, string routeName, string url, string[] namespaces); + + /// Sets up the route(s) for DotNetNuke MVC Controls. + /// The name of the folder under DesktopModules in which your module resides. + /// A unique name for the route. + /// The parameterized portion of the route. + /// Default values for the route parameters. + /// The namespace(s) in which to search for the controllers for this route. + /// A list of all routes that were registered. + /// The combination of moduleFolderName and routeName must be unique for each route. + Route MapRoute(string moduleFolderName, string routeName, string url, object defaults, string[] namespaces); + + /// Sets up the route(s) for DotNetNuke MVC Controls. + /// The name of the folder under DesktopModules in which your module resides. + /// A unique name for the route. + /// The parameterized portion of the route. + /// Default values for the route parameters. + /// The constraints. + /// The namespace(s) in which to search for the controllers for this route. + /// A list of all routes that were registered. + /// The combination of moduleFolderName and routeName must be unique for each route. + Route MapRoute(string moduleFolderName, string routeName, string url, object defaults, object constraints, string[] namespaces); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMvcRouteMapper.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMvcRouteMapper.cs new file mode 100644 index 00000000000..299da533a09 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMvcRouteMapper.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + public interface IMvcRouteMapper + { + /// + /// Registers MVC routes using the provided route manager. + /// + /// The route manager used to map routes. + void RegisterRoutes(IMapRoute mapRouteManager); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IPortalAliasMvcRouteManager.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IPortalAliasMvcRouteManager.cs new file mode 100644 index 00000000000..984ad3338bf --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IPortalAliasMvcRouteManager.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Web.Routing; + + using DotNetNuke.Abstractions.Portals; + using DotNetNuke.Entities.Portals; + + /// + /// Provides methods to manage MVC route mappings that depend on portal aliases. + /// + internal interface IPortalAliasMvcRouteManager + { + /// + /// Gets all distinct route prefix counts for the current portals and aliases. + /// + /// A collection of prefix segment counts. + IEnumerable GetRoutePrefixCounts(); + + /// + /// Gets a route name for the specified module folder, route name, and prefix count. + /// + /// The module folder name. + /// The base route name. + /// The prefix count. + /// The generated route name. + string GetRouteName(string moduleFolderName, string routeName, int count); + + /// + /// Gets all route values for a specific portal alias by expanding its prefix segments. + /// + /// The portal alias information. + /// The base route values. + /// A route value dictionary including prefix segments. + RouteValueDictionary GetAllRouteValues(IPortalAliasInfo portalAliasInfo, object routeValues); + + /// + /// Gets the full route URL for the specified module folder, relative URL, and prefix count. + /// + /// The module folder name. + /// The relative URL. + /// The prefix count. + /// The full route URL. + string GetRouteUrl(string moduleFolderName, string url, int count); + + /// + /// Clears any cached data used for route generation. + /// + void ClearCachedData(); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ITabAndModuleInfoProvider.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ITabAndModuleInfoProvider.cs new file mode 100644 index 00000000000..fc98a5fc32a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ITabAndModuleInfoProvider.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System.Web; + + using DotNetNuke.Entities.Modules; + + /// + /// Provides methods to resolve tab and module information from an HTTP request. + /// + public interface ITabAndModuleInfoProvider + { + /// + /// Attempts to find the tab identifier for the specified request. + /// + /// The current HTTP request. + /// When this method returns, contains the resolved tab identifier if found; otherwise, a default value. + /// true if the tab identifier was found; otherwise, false. + bool TryFindTabId(HttpRequestBase request, out int tabId); + + /// + /// Attempts to find the module identifier for the specified request. + /// + /// The current HTTP request. + /// When this method returns, contains the resolved module identifier if found; otherwise, a default value. + /// true if the module identifier was found; otherwise, false. + bool TryFindModuleId(HttpRequestBase request, out int moduleId); + + /// + /// Attempts to find module information for the specified request. + /// + /// The current HTTP request. + /// When this method returns, contains the resolved module information if found; otherwise, null. + /// true if the module information was found; otherwise, false. + bool TryFindModuleInfo(HttpRequestBase request, out ModuleInfo moduleInfo); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRouteExtensions.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRouteExtensions.cs new file mode 100644 index 00000000000..5194ca07981 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRouteExtensions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System.Web.Routing; + + /// + /// Extension methods for working with MVC instances. + /// + public static class MvcRouteExtensions + { + private const string NamespaceKey = "namespaces"; + private const string NameKey = "name"; + + /// Gets the name of the route. + /// The route instance. + /// The route name. + public static string GetName(this Route route) + { + return (string)route.DataTokens[NameKey]; + } + + /// + /// Sets the namespaces that are searched for controllers for this route. + /// + /// The route instance. + /// The controller namespaces. + internal static void SetNameSpaces(this Route route, string[] namespaces) + { + route.DataTokens[NamespaceKey] = namespaces; + } + + /// Gets namespaces that are searched for controllers for this route. + /// The route instance. + /// The controller namespaces. + internal static string[] GetNameSpaces(this Route route) + { + return (string[])route.DataTokens[NamespaceKey]; + } + + /// + /// Sets the name of the route. + /// + /// The route instance. + /// The route name. + internal static void SetName(this Route route, string name) + { + route.DataTokens[NameKey] = name; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRoutingManager.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRoutingManager.cs new file mode 100644 index 00000000000..1e39a3990b1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRoutingManager.cs @@ -0,0 +1,251 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Collections.Generic; + using System.Web.Configuration; + using System.Web.Http; + using System.Web.Mvc; + using System.Web.Routing; + using DotNetNuke.Common; + using DotNetNuke.Common.Internal; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Framework.Reflections; + using DotNetNuke.Instrumentation; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Commons; + + /// + /// Provides routing registration and mapping for DNN MVC modules and pages. + /// + public sealed class MvcRoutingManager : IRoutingManager, IMapRoute + { + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(MvcRoutingManager)); + private readonly Dictionary moduleUsage = new Dictionary(); + private readonly RouteCollection routes; + private readonly PortalAliasMvcRouteManager portalAliasMvcRouteManager; + + /// + /// Initializes a new instance of the class. + /// + public MvcRoutingManager() + : this(RouteTable.Routes) + { + } + + /// + /// Initializes a new instance of the class for testing. + /// + /// The route collection to register routes in. + internal MvcRoutingManager(RouteCollection routes) + { + this.routes = routes; + this.portalAliasMvcRouteManager = new PortalAliasMvcRouteManager(); + this.TypeLocator = new TypeLocator(); + } + + /// + /// Gets or sets the type locator used to discover MVC route mappers. + /// + internal ITypeLocator TypeLocator { get; set; } + + /// + /// Maps a route for a module folder using the specified name, URL, and namespaces. + /// + /// The module folder name (area). + /// The route name. + /// The route URL pattern. + /// The controller namespaces associated with the route. + /// The created . + public Route MapRoute(string moduleFolderName, string routeName, string url, string[] namespaces) + { + return this.MapRoute(moduleFolderName, routeName, url, null /* defaults */, null /* constraints */, namespaces); + } + + /// + public Route MapRoute(string moduleFolderName, string routeName, string url, object defaults, string[] namespaces) + { + return this.MapRoute(moduleFolderName, routeName, url, defaults, null /* constraints */, namespaces); + } + + /// + public Route MapRoute(string moduleFolderName, string routeName, string url, object defaults, object constraints, string[] namespaces) + { + if (namespaces == null || namespaces.Length == 0 || string.IsNullOrEmpty(namespaces[0])) + { + throw new ArgumentException(Localization.GetExceptionMessage( + "ArgumentCannotBeNullOrEmpty", + "The argument '{0}' cannot be null or empty.", + "namespaces")); + } + + Requires.NotNullOrEmpty("moduleFolderName", moduleFolderName); + + url = url.Trim('/', '\\'); + + var prefixCounts = this.portalAliasMvcRouteManager.GetRoutePrefixCounts(); + Route route = null; + + if (url == null) + { + throw new ArgumentNullException(nameof(url)); + } + + foreach (var count in prefixCounts) + { + var fullRouteName = this.portalAliasMvcRouteManager.GetRouteName(moduleFolderName, routeName, count); + var routeUrl = this.portalAliasMvcRouteManager.GetRouteUrl(moduleFolderName, url, count); + route = MapRouteWithNamespace(fullRouteName, moduleFolderName, routeUrl, defaults, constraints, namespaces); + this.routes.Add(route); + Logger.Trace("Mapping route: " + fullRouteName + " Area="+moduleFolderName + " @ " + routeUrl); + } + + return route; + } + + + public void RegisterRoutes() + { + // add standard tab and module id provider + GlobalConfiguration.Configuration.AddTabAndModuleInfoProvider(new StandardTabAndModuleInfoProvider()); + using (this.routes.GetWriteLock()) + { + // routes.Clear(); -- don't use; it will remove original WEP API maps + this.LocateServicesAndMapRoutes(); + // routes.MapMvcAttributeRoutes(); + } + + // AreaRegistration.RegisterAllAreas(); + + Logger.TraceFormat("Registered a total of {0} routes", this.routes.Count); + } + + private static bool IsTracingEnabled() + { + var configValue = Config.GetSetting("EnableServicesFrameworkTracing"); + + return !string.IsNullOrEmpty(configValue) && Convert.ToBoolean(configValue); + } + + internal static bool IsValidServiceRouteMapper(Type t) + { + return t != null && t.IsClass && !t.IsAbstract && t.IsVisible && typeof(IMvcRouteMapper).IsAssignableFrom(t); + } + + private void RegisterSystemRoutes() + { + var dataTokens = new RouteValueDictionary(); + var ns = new string[] { "DotNetNuke.Web.MvcWebsite.Controllers" }; + dataTokens["Namespaces"] = ns; + + var route = new Route( + "DesktopModules/{controller}/{action}/{tabid}/{language}", + new RouteValueDictionary(new { action = "Index", tabid = UrlParameter.Optional, language = UrlParameter.Optional }), + null, // No constraints + dataTokens, + new DnnMvcPageRouteHandler() + ); + this.routes.Add(route); + /* + dataTokens = new RouteValueDictionary(); + ns = new string[] { "DotNetNuke.Modules.Html.Controllers" }; + dataTokens["Namespaces"] = ns; + dataTokens["area"] = "Html"; + + + route = new Route( + "DesktopModules/{controller}/{action}", + new RouteValueDictionary(new { action = "Index" }), + null, // No constraints + dataTokens, + new DnnMvcPageRouteHandler() + ); + + this.routes.Add(route); + */ + } + + private static Route MapRouteWithNamespace(string name, string area, string url, object defaults, object constraints, string[] namespaces) + { + var route = new Route(url, new DnnMvcPageRouteHandler()) + { + Defaults = CreateRouteValueDictionaryUncached(defaults), + Constraints = CreateRouteValueDictionaryUncached(constraints), + }; + if (route.DataTokens == null) + { + route.DataTokens = new RouteValueDictionary(); + } + route.DataTokens.Add("area", area); + ConstraintValidation.Validate(route); + if ((namespaces != null) && (namespaces.Length > 0)) + { + route.SetNameSpaces(namespaces); + } + + route.SetName(name); + return route; + } + + private static RouteValueDictionary CreateRouteValueDictionaryUncached(object values) + { + var dictionary = values as IDictionary; + return dictionary != null ? new RouteValueDictionary(dictionary) : TypeHelper.ObjectToDictionary(values); + } + + private void LocateServicesAndMapRoutes() + { + RegisterSystemRoutes(); + this.ClearCachedRouteData(); + + this.moduleUsage.Clear(); + foreach (var routeMapper in this.GetServiceRouteMappers()) + { + try + { + routeMapper.RegisterRoutes(this); + } + catch (Exception e) + { + Logger.ErrorFormat("{0}.RegisterRoutes threw an exception. {1}\r\n{2}", routeMapper.GetType().FullName, e.Message, e.StackTrace); + } + } + } + + private void ClearCachedRouteData() + { + this.portalAliasMvcRouteManager.ClearCachedData(); + } + + private IEnumerable GetServiceRouteMappers() + { + IEnumerable types = this.GetAllServiceRouteMapperTypes(); + + foreach (var routeMapperType in types) + { + IMvcRouteMapper routeMapper; + try + { + routeMapper = Activator.CreateInstance(routeMapperType) as IMvcRouteMapper; + } + catch (Exception e) + { + Logger.ErrorFormat("Unable to create {0} while registering service routes. {1}", routeMapperType.FullName, e.Message); + routeMapper = null; + } + + if (routeMapper != null) + { + yield return routeMapper; + } + } + } + + private IEnumerable GetAllServiceRouteMapperTypes() + { + return this.TypeLocator.GetAllMatchingTypes(IsValidServiceRouteMapper); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/PortalAliasMvcRouteManager.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/PortalAliasMvcRouteManager.cs new file mode 100644 index 00000000000..ae8a5350db1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/PortalAliasMvcRouteManager.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Web.Routing; + + using DotNetNuke.Abstractions.Portals; + using DotNetNuke.Common; + using DotNetNuke.Common.Internal; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Internal.SourceGenerators; + + /// + /// Manages MVC route names, URLs, and prefix counts based on portal aliases. + /// + internal partial class PortalAliasMvcRouteManager : IPortalAliasMvcRouteManager + { + private List prefixCounts; + + /// + public string GetRouteName(string moduleFolderName, string routeName, int count) + { + Requires.NotNullOrEmpty("moduleFolderName", moduleFolderName); + Requires.NotNegative("count", count); + + return moduleFolderName + "-" + routeName + "-" + count.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Gets a route name for the specified module folder and route name based on a portal alias. + /// + /// The module folder name. + /// The base route name. + /// The portal alias information. + /// The generated route name. + public string GetRouteName(string moduleFolderName, string routeName, IPortalAliasInfo portalAlias) + { + var alias = portalAlias.HttpAlias; + string appPath = TestableGlobals.Instance.ApplicationPath; + if (!string.IsNullOrEmpty(appPath)) + { + int i = alias.IndexOf(appPath, StringComparison.OrdinalIgnoreCase); + if (i > 0) + { + alias = alias.Remove(i, appPath.Length); + } + } + + return this.GetRouteName(moduleFolderName, routeName, CalcAliasPrefixCount(alias)); + } + + /// + public RouteValueDictionary GetAllRouteValues(IPortalAliasInfo portalAliasInfo, object routeValues) + { + var allRouteValues = new RouteValueDictionary(routeValues); + + var segments = portalAliasInfo.HttpAlias.Split('/'); + + if (segments.Length <= 1) + { + return allRouteValues; + } + + for (var i = 1; i < segments.Length; i++) + { + var key = "prefix" + (i - 1).ToString(CultureInfo.InvariantCulture); + var value = segments[i]; + allRouteValues.Add(key, value); + } + + return allRouteValues; + } + + /// + public string GetRouteUrl(string moduleFolderName, string url, int count) + { + Requires.NotNegative("count", count); + Requires.NotNullOrEmpty("moduleFolderName", moduleFolderName); + + return $"{GeneratePrefixString(count)}DesktopModules/{url}"; + } + + /// + public void ClearCachedData() + { + this.prefixCounts = null; + } + + /// + public IEnumerable GetRoutePrefixCounts() + { + if (this.prefixCounts != null) + { + return this.prefixCounts; + } + + // prefixCounts are required for each route that is mapped but they only change + // when a new portal is added so cache them until that time + var portals = PortalController.Instance.GetPortals(); + var segmentCounts1 = new List(); + + foreach ( + var count in + portals.Cast() + .Select( + portal => + PortalAliasController.Instance.GetPortalAliasesByPortalId(portal.PortalId) + .Cast() + .Select(x => x.HttpAlias)) + .Select(this.StripApplicationPath) + .SelectMany( + aliases => + aliases.Select(CalcAliasPrefixCount).Where(count => !segmentCounts1.Contains(count)))) + { + segmentCounts1.Add(count); + } + + IEnumerable segmentCounts = segmentCounts1; + this.prefixCounts = segmentCounts.OrderByDescending(x => x).ToList(); + + return this.prefixCounts; + } + + private static string GeneratePrefixString(int count) + { + if (count == 0) + { + return string.Empty; + } + + var prefix = string.Empty; + + for (var i = count - 1; i >= 0; i--) + { + prefix = "{prefix" + i + "}/" + prefix; + } + + return prefix; + } + + private static int CalcAliasPrefixCount(string alias) + { + return alias.Count(c => c == '/'); + } + + private static IEnumerable StripApplicationPathIterable(IEnumerable aliases, string appPath) + { + foreach (var alias in aliases) + { + var i = alias.IndexOf(appPath, StringComparison.OrdinalIgnoreCase); + + if (i > 0) + { + yield return alias.Remove(i, appPath.Length); + } + else + { + yield return alias; + } + } + } + + private IEnumerable StripApplicationPath(IEnumerable aliases) + { + var appPath = TestableGlobals.Instance.ApplicationPath; + + return string.IsNullOrEmpty(appPath) ? aliases : StripApplicationPathIterable(aliases, appPath); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/StandardTabAndModuleInfoProvider.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/StandardTabAndModuleInfoProvider.cs new file mode 100644 index 00000000000..1492db03678 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/StandardTabAndModuleInfoProvider.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Routing +{ + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Web; + + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Instrumentation; + + /// + /// Default implementation for resolving tab and module information from an HTTP request. + /// + public sealed class StandardTabAndModuleInfoProvider : ITabAndModuleInfoProvider + { + private const string ModuleIdKey = "ModuleId"; + private const string TabIdKey = "TabId"; + private const string MonikerQueryKey = "Moniker"; + private const string MonikerHeaderKey = "X-DNN-MONIKER"; + private const string MonikerSettingsKey = "Moniker"; + private static readonly ILog Logger = LoggerSource.Instance.GetLogger(typeof(StandardTabAndModuleInfoProvider)); + + /// + public bool TryFindTabId(HttpRequestBase request, out int tabId) + { + return TryFindTabId(request, out tabId, true); + } + + /// + public bool TryFindModuleId(HttpRequestBase request, out int moduleId) + { + return TryFindModuleId(request, out moduleId, true); + } + + /// + public bool TryFindModuleInfo(HttpRequestBase request, out ModuleInfo moduleInfo) + { + int tabId, moduleId; + if (TryFindTabId(request, out tabId, false) && TryFindModuleId(request, out moduleId, false)) + { + moduleInfo = ModuleController.Instance.GetModule(moduleId, tabId, false); + return moduleInfo != null; + } + + return TryFindByMoniker(request, out moduleInfo); + } + + private static bool TryFindTabId(HttpRequestBase request, out int tabId, bool tryMoniker) + { + tabId = FindInt(request, TabIdKey); + if (tabId > Null.NullInteger) + { + return true; + } + + if (tryMoniker) + { + ModuleInfo moduleInfo; + if (TryFindByMoniker(request, out moduleInfo)) + { + tabId = moduleInfo.TabID; + return true; + } + } + + return false; + } + + private static bool TryFindModuleId(HttpRequestBase request, out int moduleId, bool tryMoniker) + { + moduleId = FindInt(request, ModuleIdKey); + if (moduleId > Null.NullInteger) + { + return true; + } + + if (tryMoniker) + { + ModuleInfo moduleInfo; + if (TryFindByMoniker(request, out moduleInfo)) + { + moduleId = moduleInfo.ModuleID; + return true; + } + } + + return false; + } + + private static int FindInt(HttpRequestBase requestBase, string key) + { + string value = null; + if (requestBase.Headers[key] != null) + { + value = requestBase.Headers[key]; + } + + if (requestBase.Form[key] != null) + { + value = requestBase.Form[key]; + } + + if (string.IsNullOrEmpty(value) && requestBase.Url != null) + { + var queryString = HttpUtility.ParseQueryString(requestBase.Url.Query); + value = queryString[key]; + } + + int id; + return int.TryParse(value, out id) ? id : Null.NullInteger; + } + + private static bool TryFindByMoniker(HttpRequestBase requestBase, out ModuleInfo moduleInfo) + { + var id = FindIntInHeader(requestBase, MonikerHeaderKey); + if (id <= Null.NullInteger) + { + id = FindIntInQueryString(requestBase, MonikerQueryKey); + } + + moduleInfo = id > Null.NullInteger ? ModuleController.Instance.GetTabModule(id) : null; + return moduleInfo != null; + } + + private static int FindIntInHeader(HttpRequestBase requestBase, string key) + { + string value = null; + if (requestBase.Headers[key] != null) + { + value = requestBase.Form[key]; + } + + return GetTabModuleInfoFromMoniker(value); + } + + private static int FindIntInQueryString(HttpRequestBase requestBase, string key) + { + string value = null; + if (requestBase.Url != null) + { + var queryString = HttpUtility.ParseQueryString(requestBase.Url.Query); + value = queryString[key]; + } + + return GetTabModuleInfoFromMoniker(value); + } + + private static int GetTabModuleInfoFromMoniker(string monikerValue) + { + monikerValue = (monikerValue ?? string.Empty).Trim(); + if (monikerValue.Length > 0) + { + var ids = TabModulesController.Instance.GetTabModuleIdsBySetting(MonikerSettingsKey, monikerValue); + if (ids != null && ids.Any()) + { + return ids.First(); + } + + if (Logger.IsWarnEnabled) + { + Logger.WarnFormat("The specified moniker ({0}) is not defined in the system", monikerValue); + } + } + + return Null.NullInteger; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.BreadCrumb.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.BreadCrumb.cs new file mode 100644 index 00000000000..170263429ef --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.BreadCrumb.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System.Text; + using System.Text.RegularExpressions; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for rendering breadcrumb navigation. + /// + public static partial class SkinHelpers + { + private const string UrlRegex = "(href|src)=(\\\"|'|)(.[^\\\"']*)(\\\"|'|)"; + + /// + /// Renders a breadcrumb trail for the current page using the MVC pipeline navigation model. + /// + /// The HTML helper for the current . + /// The CSS class applied to breadcrumb links. + /// The HTML used to separate breadcrumb items. + /// The starting breadcrumb level; negative values show the portal root. + /// If set to true, uses the tab title instead of the tab name. + /// If set to true, hides the control when only a single breadcrumb exists. + /// If set to true, renders simplified markup for the current tab. + /// An HTML string representing the breadcrumb trail. + public static IHtmlString BreadCrumb(this HtmlHelper helper, string cssClass = "SkinObject", string separator = "\"breadcrumb", int rootLevel = 0, bool useTitle = false, bool hideWithNoBreadCrumb = false, bool cleanerMarkup = false) + { + var portalSettings = PortalSettings.Current; + var navigationManager = helper.ViewData.Model.NavigationManager; + var breadcrumb = new StringBuilder(""); + var position = 1; + var showRoot = rootLevel < 0; + var homeUrl = string.Empty; + var homeTabName = "Root"; + + // Resolve separator paths + separator = ResolveSeparatorPaths(separator, portalSettings); + + // Get UserId and GroupId from request + var request = helper.ViewContext.HttpContext.Request; + int profileUserId = Null.NullInteger; + if (!string.IsNullOrEmpty(request.Params["UserId"])) + { + int.TryParse(request.Params["UserId"], out profileUserId); + } + + int groupId = Null.NullInteger; + if (!string.IsNullOrEmpty(request.Params["GroupId"])) + { + int.TryParse(request.Params["GroupId"], out groupId); + } + + if (showRoot) + { + rootLevel = 0; + } + + if (hideWithNoBreadCrumb && portalSettings.ActiveTab.BreadCrumbs.Count == (rootLevel + 1)) + { + return MvcHtmlString.Empty; + } + + if (showRoot && portalSettings.ActiveTab.TabID != portalSettings.HomeTabId) + { + homeUrl = Globals.AddHTTP(portalSettings.PortalAlias.HTTPAlias); + + if (portalSettings.HomeTabId != -1) + { + homeUrl = navigationManager.NavigateURL(portalSettings.HomeTabId); + + var tc = new TabController(); + var homeTab = tc.GetTab(portalSettings.HomeTabId, portalSettings.PortalId, false); + homeTabName = homeTab.LocalizedTabName; + + if (useTitle && !string.IsNullOrEmpty(homeTab.Title)) + { + homeTabName = homeTab.Title; + } + } + + breadcrumb.Append(""); + breadcrumb.Append("" + homeTabName + ""); + breadcrumb.Append(""); + breadcrumb.Append(""); + breadcrumb.Append(separator); + } + + for (var i = rootLevel; i < portalSettings.ActiveTab.BreadCrumbs.Count; ++i) + { + if (i > rootLevel) + { + breadcrumb.Append(separator); + } + + var tab = (TabInfo)portalSettings.ActiveTab.BreadCrumbs[i]; + var tabName = tab.LocalizedTabName; + + if (useTitle && !string.IsNullOrEmpty(tab.Title)) + { + tabName = tab.Title; + } + + var tabUrl = tab.FullUrl; + + if (profileUserId > -1) + { + tabUrl = navigationManager.NavigateURL(tab.TabID, string.Empty, "UserId=" + profileUserId); + } + + if (groupId > -1) + { + tabUrl = navigationManager.NavigateURL(tab.TabID, string.Empty, "GroupId=" + groupId); + } + + if (tab.DisableLink) + { + if (cleanerMarkup) + { + breadcrumb.Append("" + tabName + ""); + } + else + { + breadcrumb.Append("" + tabName + ""); + } + } + else + { + breadcrumb.Append(""); + breadcrumb.Append("" + tabName + ""); + breadcrumb.Append(""); + breadcrumb.Append(""); + } + } + + breadcrumb.Append(""); + + // Wrap in the outer span to match the original .ascx structure. + var outerHtml = new StringBuilder(); + outerHtml.Append(""); + outerHtml.Append(breadcrumb.ToString()); + outerHtml.Append(""); + + return new MvcHtmlString(outerHtml.ToString()); + } + + /// + /// Resolves relative or application-rooted URLs inside the separator HTML to fully qualified paths. + /// + /// The separator HTML string. + /// The current portal settings. + /// The separator HTML with resolved URLs. + private static string ResolveSeparatorPaths(string separator, PortalSettings portalSettings) + { + if (string.IsNullOrEmpty(separator)) + { + return separator; + } + + var urlMatches = Regex.Matches(separator, UrlRegex, RegexOptions.IgnoreCase); + if (urlMatches.Count > 0) + { + foreach (Match match in urlMatches) + { + var url = match.Groups[3].Value; + var changed = false; + + if (url.StartsWith("/")) + { + if (!string.IsNullOrEmpty(Globals.ApplicationPath)) + { + url = string.Format("{0}{1}", Globals.ApplicationPath, url); + changed = true; + } + } + else if (url.StartsWith("~/")) + { + url = Globals.ResolveUrl(url); + changed = true; + } + else + { + url = string.Format("{0}{1}", portalSettings.ActiveTab.SkinPath, url); + changed = true; + } + + if (changed) + { + var newMatch = string.Format( + "{0}={1}{2}{3}", + match.Groups[1].Value, + match.Groups[2].Value, + url, + match.Groups[4].Value); + + separator = separator.Replace(match.Value, newMatch); + } + } + } + + return separator; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Copyright.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Copyright.cs new file mode 100644 index 00000000000..cd547407bbe --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Copyright.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering portal copyright text. + /// + public static partial class SkinHelpers + { + /// + /// Renders the portal's configured footer text or a localized copyright label. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the span. + /// An HTML string representing the copyright text. + public static IHtmlString Copyright(this HtmlHelper helper, string cssClass = "SkinObject") + { + var portalSettings = PortalSettings.Current; + var lblCopyright = new TagBuilder("span"); + + if (!string.IsNullOrEmpty(cssClass)) + { + lblCopyright.AddCssClass(cssClass); + } + + if (!string.IsNullOrEmpty(portalSettings.FooterText)) + { + lblCopyright.SetInnerText(portalSettings.FooterText.Replace("[year]", DateTime.Now.ToString("yyyy"))); + } + else + { + lblCopyright.SetInnerText(string.Format(Localization.GetString("Copyright", GetSkinsResourceFile("Copyright.ascx")), DateTime.Now.Year, portalSettings.PortalName)); + } + + return new MvcHtmlString(lblCopyright.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.CurrentDate.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.CurrentDate.cs new file mode 100644 index 00000000000..61548586e0e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.CurrentDate.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + using DotNetNuke.Entities.Users; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering the current date in the user's local time. + /// + public static partial class SkinHelpers + { + /// + /// Renders the current date for the logged-in user, using an optional custom format. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the span. + /// Optional date format string; when empty, uses the long date pattern. + /// An HTML string representing the current date. + public static IHtmlString CurrentDate(this HtmlHelper helper, string cssClass = "SkinObject", string dateFormat = "") + { + var lblDate = new TagBuilder("span"); + + if (!string.IsNullOrEmpty(cssClass)) + { + lblDate.AddCssClass(cssClass); + } + + var user = UserController.Instance.GetCurrentUserInfo(); + lblDate.SetInnerText(!string.IsNullOrEmpty(dateFormat) ? user.LocalTime().ToString(dateFormat) : user.LocalTime().ToLongDateString()); + + return new MvcHtmlString(lblDate.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssExclude.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssExclude.cs new file mode 100644 index 00000000000..d8e9ec182ba --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssExclude.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for excluding previously registered CSS resources. + /// + public static partial class SkinHelpers + { + /// + /// Excludes a previously registered stylesheet by its logical name. + /// + /// The HTML helper for the current . + /// The logical stylesheet name to remove. + /// An empty HTML string. + public static IHtmlString DnnCssExclude(this HtmlHelper helper, string name) + { + HtmlHelpers.GetClientResourcesController(helper) + .RemoveStylesheetByName(name); + return new MvcHtmlString(string.Empty); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssInclude.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssInclude.cs new file mode 100644 index 00000000000..12dff9f0d6b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssInclude.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections; + using System.Web; + using System.Web.Mvc; + + using ClientDependency.Core; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for including CSS files via the DNN Client Dependency Framework. + /// + public static partial class SkinHelpers + { + /// + /// Registers a stylesheet with the DNN Client Dependency Framework. + /// Optionally emits a CDF debug comment when running in debug mode or when is true. + /// + /// The HTML helper for the current . + /// The stylesheet file path. + /// The path alias (for example, Skin). + /// The client dependency priority. + /// If set to true, emits a CDF debug comment. + /// Optional logical name for the stylesheet. + /// Optional version associated with the stylesheet. + /// If set to true, forces the specified version. + /// Optional provider name to force. + /// Unused parameter retained for API compatibility. + /// Optional media attribute (for example, print or screen). + /// An empty HTML string or a CDF debug comment when requested. + public static IHtmlString DnnCssInclude(this HtmlHelper helper, string filePath, string pathNameAlias = "", int priority = 100, bool addTag = false, string name = "", string version = "", bool forceVersion = false, string forceProvider = "", bool forceBundle = false, string cssMedia = "") + { + var ss = HtmlHelpers.GetClientResourcesController(helper) + .CreateStylesheet(filePath, pathNameAlias) + .SetPriority(priority); + if (!string.IsNullOrEmpty(forceProvider)) + { + ss.SetProvider(forceProvider); + } + if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(version)) + { + ss.SetNameAndVersion(name, version, forceVersion); + } + if (!string.IsNullOrEmpty(cssMedia)) + { + ss.SetMedia(cssMedia); + } + ss.Register(); + + if (addTag || helper.ViewContext.HttpContext.IsDebuggingEnabled) + { + return new MvcHtmlString(string.Format("", ClientDependencyType.Css, filePath, forceProvider, priority)); + } + + return new MvcHtmlString(string.Empty); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsExclude.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsExclude.cs new file mode 100644 index 00000000000..796bef742d5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsExclude.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for excluding previously registered JavaScript resources. + /// + public static partial class SkinHelpers + { + /// + /// Excludes a previously registered JavaScript resource by its logical name. + /// + /// The HTML helper for the current . + /// The logical script name to remove. + /// An empty HTML string. + public static IHtmlString DnnJsExclude(this HtmlHelper helper, string name) + { + HtmlHelpers.GetClientResourcesController(helper) + .RemoveScriptByName(name); + return new MvcHtmlString(string.Empty); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsInclude.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsInclude.cs new file mode 100644 index 00000000000..97e93863948 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsInclude.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections.Generic; + using System.Security.Policy; + using System.Web; + using System.Web.Mvc; + + using ClientDependency.Core; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for including JavaScript files via the DNN Client Dependency Framework. + /// + public static partial class SkinHelpers + { + /// + /// Registers a JavaScript file with the DNN Client Dependency Framework. + /// Optionally emits a CDF debug comment when running in debug mode or when is true. + /// + /// The HTML helper for the current . + /// The JavaScript file path. + /// The path alias (for example, SharedScripts). + /// The client dependency priority. + /// If set to true, emits a CDF debug comment. + /// Optional logical name for the script. + /// Optional version associated with the script. + /// If set to true, forces the specified version. + /// Optional provider name to force. + /// Unused parameter retained for API compatibility. + /// If set to true, marks the script as deferred. + /// An empty HTML string or a CDF debug comment when requested. + public static IHtmlString DnnJsInclude(this HtmlHelper helper, string filePath, string pathNameAlias = "", int priority = 100, bool addTag = false, string name = "", string version = "", bool forceVersion = false, string forceProvider = "", bool forceBundle = false, bool defer = false) + { + // var htmlAttibs = new { nonce = helper.ViewContext.HttpContext.Items["CSP-NONCE"].ToString(), defer = defer ? "defer" : string.Empty }; + //todo CSP - implement nonce support + // htmlAttibs.Add("nonce", helper.ViewContext.HttpContext.Items["CSP-NONCE"].ToString()); + + var script = HtmlHelpers.GetClientResourcesController(helper) + .CreateScript(filePath, pathNameAlias) + .SetPriority(priority); + if (!string.IsNullOrEmpty(forceProvider)) + { + script.SetProvider(forceProvider); + } + if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(version)) + { + script.SetNameAndVersion(name, version, forceVersion); + } + if (defer) + { + script.SetDefer(); + } + script.Register(); + + if (addTag || helper.ViewContext.HttpContext.IsDebuggingEnabled) + { + return new MvcHtmlString(string.Format("", ClientDependencyType.Javascript, filePath, forceProvider, priority)); + } + + return new MvcHtmlString(string.Empty); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnLink.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnLink.cs new file mode 100644 index 00000000000..3a3f6556c37 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnLink.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering the "CMS by DNN" attribution link. + /// + public static partial class SkinHelpers + { + /// + /// Renders an attribution link to the DNN community site, with text varied by the current host name. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the link. + /// Optional link target attribute (for example, _blank). + /// An HTML string representing the attribution link. + public static IHtmlString DnnLink(this HtmlHelper helper, string cssClass = "", string target = "") + { + var link = new TagBuilder("a"); + if (!string.IsNullOrEmpty(cssClass)) + { + link.AddCssClass(cssClass); + } + + if (!string.IsNullOrEmpty(target)) + { + link.Attributes.Add("target", target); + } + + // set home page link to community URL + string url = "http://www.dnnsoftware.com/community?utm_source=dnn-install&utm_medium=web-link&utm_content=gravity-skin-link&utm_campaign=dnn-install"; + string utmTerm = "&utm_term=cms-by-dnn"; + string hostName = helper.ViewContext.HttpContext.Request.Url.Host.ToLowerInvariant().Replace("www.", string.Empty); + int charPos = 0; + string linkText = "CMS by DNN"; + if (hostName.Length > 0) + { + // convert first letter of hostname to int pos in alphabet + charPos = char.ToUpper(hostName[0]) - 64; + } + + // vary link by first letter of host name + if (charPos <= 5) + { + linkText = "Open Source ASP.NET CMS by DNN"; + utmTerm = "&utm_term=open+source+asp.net+by+dnn"; + } + + if (charPos > 5 && charPos <= 10) + { + linkText = "DNN - .NET Open Source CMS"; + utmTerm = "&utm_term=dnn+.net+open+source+cms"; + } + + if (charPos > 10 && charPos <= 15) + { + linkText = "Web Content Management by DNN"; + utmTerm = "&utm_term=web+content+management+by+dnn"; + } + + if (charPos > 15 && charPos <= 20) + { + linkText = "DNN .NET CMS"; + utmTerm = "&utm_term=dnn+.net+cms"; + } + + if (charPos > 20 && charPos <= 25) + { + linkText = "WCM by DNN"; + utmTerm = "&utm_term=wcm+by+dnn"; + } + + link.SetInnerText(linkText); + link.Attributes.Add("href", HttpUtility.HtmlEncode(url + utmTerm)); + + return new MvcHtmlString(link.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DotNetNuke.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DotNetNuke.cs new file mode 100644 index 00000000000..4208d6b7b4e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DotNetNuke.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.Application; + using DotNetNuke.Application; + using DotNetNuke.Common; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for rendering DNN application branding. + /// + public static partial class SkinHelpers + { + /// + /// Renders a link to the DNN application site using the legal copyright text. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the anchor. + /// An HTML string for the DNN copyright link, or empty when disabled. + public static IHtmlString DotNetNuke(this HtmlHelper helper, string cssClass = "Normal") + { + var hostSettingsService = Globals.GetCurrentServiceProvider().GetRequiredService(); + if (!hostSettingsService.GetBoolean("Copyright", true)) + { + return MvcHtmlString.Empty; + } + + var link = new TagBuilder("a"); + link.Attributes.Add("href", DotNetNukeContext.Current.Application.Url); + if (!string.IsNullOrEmpty(cssClass)) + { + link.AddCssClass(cssClass); + } + + link.SetInnerText(DotNetNukeContext.Current.Application.LegalCopyright); + + return new MvcHtmlString(link.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Help.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Help.cs new file mode 100644 index 00000000000..9ce7b0bffb2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Help.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.Application; + using DotNetNuke.Common; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for rendering help links to host or portal contacts. + /// + public static partial class SkinHelpers + { + /// + /// Renders a mailto link for requesting support, directing to either host or portal email. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the anchor. + /// An HTML string representing the help link, or empty if the user is not authenticated. + public static IHtmlString Help(this HtmlHelper helper, string cssClass = "") + { + if (!helper.ViewContext.HttpContext.Request.IsAuthenticated) + { + return MvcHtmlString.Empty; + } + + var portalSettings = PortalSettings.Current; + var hostSettings = Globals.GetCurrentServiceProvider().GetRequiredService(); + + var link = new TagBuilder("a"); + if (!string.IsNullOrEmpty(cssClass)) + { + link.AddCssClass(cssClass); + } + + string email; + if (TabPermissionController.CanAdminPage()) + { + email = hostSettings.HostEmail; + } + else + { + email = portalSettings.Email; + } + + link.Attributes.Add("href", "mailto:" + email + "?subject=" + portalSettings.PortalName + " Support Request"); + link.SetInnerText(Localization.GetString("Help")); + + return new MvcHtmlString(link.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.HostName.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.HostName.cs new file mode 100644 index 00000000000..a80c54d8e13 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.HostName.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.Application; + using DotNetNuke.Common; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for rendering links to the host site. + /// + public static partial class SkinHelpers + { + /// + /// Renders a link to the host site using host settings. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the anchor. + /// An HTML string for the host name link. + public static IHtmlString HostName(this HtmlHelper helper, string cssClass = "") + { + var hostSettings = Globals.GetCurrentServiceProvider().GetRequiredService(); + + var link = new TagBuilder("a"); + link.Attributes.Add("href", Globals.AddHTTP(hostSettings.HostUrl)); + + if (!string.IsNullOrEmpty(cssClass)) + { + link.AddCssClass(cssClass); + } + + link.SetInnerText(hostSettings.HostTitle); + + return new MvcHtmlString(link.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.JavaScriptLibraryInclude.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.JavaScriptLibraryInclude.cs new file mode 100644 index 00000000000..dfcdef78efb --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.JavaScriptLibraryInclude.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + using DotNetNuke.Common; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for including registered JavaScript libraries by name and version. + /// + public static partial class SkinHelpers + { + /// + /// Registers a JavaScript library by name and optional version using the DNN JavaScript library helper. + /// + /// The HTML helper for the current . + /// The logical name of the JavaScript library. + /// Optional version string (for example, "1.2.3"). + /// Optional specific version selector (for example, "Latest", "Exact"). + /// An empty HTML string; the library is registered via the JavaScript library helper. + public static IHtmlString JavaScriptLibraryInclude(this HtmlHelper helper, string name, string version = null, string specificVersion = null) + { + var javaScript = HtmlHelpers.GetDependencyProvider(helper).GetRequiredService(); + SpecificVersion specificVer; + if (version == null) + { + javaScript.RequestRegistration(name); + } + else if (!Enum.TryParse(specificVersion, true, out specificVer)) + { + javaScript.RequestRegistration(name, new Version(version)); + } + else + { + javaScript.RequestRegistration(name, new Version(version), specificVer); + } + + return new MvcHtmlString(string.Empty); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Language.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Language.cs new file mode 100644 index 00000000000..1e048163302 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Language.cs @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Web; + using System.Web.Mvc; + using System.Web.UI.WebControls; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Security; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Services.Exceptions; + using DotNetNuke.Services.Localization; + using DotNetNuke.UI.Skins.Controls; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering language and culture selection UI. + /// + public static partial class SkinHelpers + { + /// + /// Renders the language selector and optional list of language links for the current portal. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the selector control. + /// Optional item template for language links. + /// Optional template for the currently selected language. + /// Optional header template for the language list. + /// Optional footer template for the language list. + /// Optional alternate item template for language links. + /// Optional separator template between language items. + /// Optional common header template wrapping the control. + /// Optional common footer template wrapping the control. + /// If set to true, shows the language drop-down menu. + /// If set to true, renders language links in addition to the drop-down. + /// If set to true, uses the current culture to resolve templates; otherwise, uses en-US. + /// An HTML string representing the language selector UI. + public static IHtmlString Language( + this HtmlHelper helper, + string cssClass = "", + string itemTemplate = "", + string selectedItemTemplate = "", + string headerTemplate = "", + string footerTemplate = "", + string alternateTemplate = "", + string separatorTemplate = "", + string commonHeaderTemplate = "", + string commonFooterTemplate = "", + bool showMenu = true, + bool showLinks = false, + bool useCurrentCultureForTemplate = false) + { + var portalSettings = PortalSettings.Current; + var currentCulture = CultureInfo.CurrentCulture.ToString(); + var templateCulture = useCurrentCultureForTemplate ? currentCulture : "en-US"; + var localResourceFile = GetSkinsResourceFile("Language.ascx"); + var localTokenReplace = new LanguageTokenReplace { resourceFile = localResourceFile }; + + var locales = new Dictionary(); + IEnumerable cultureListItems = Localization.LoadCultureInListItems(CultureDropDownTypes.NativeName, currentCulture, string.Empty, false); + foreach (Locale loc in LocaleController.Instance.GetLocales(portalSettings.PortalId).Values) + { + string defaultRoles = PortalController.GetPortalSetting(string.Format("DefaultTranslatorRoles-{0}", loc.Code), portalSettings.PortalId, "Administrators"); + if (!portalSettings.ContentLocalizationEnabled || + (LocaleIsAvailable(loc, portalSettings) && + (PortalSecurity.IsInRoles(portalSettings.AdministratorRoleName) || loc.IsPublished || PortalSecurity.IsInRoles(defaultRoles)))) + { + locales.Add(loc.Code, loc); + } + } + + int cultureCount = 0; + + var selectCulture = new TagBuilder("select"); + selectCulture.Attributes.Add("id", "selectCulture"); + selectCulture.Attributes.Add("name", "selectCulture"); + selectCulture.AddCssClass("NormalTextBox"); + selectCulture.Attributes.Add("onchange", "var url = this.options[this.selectedIndex].getAttribute('data-link'); if(url) window.location.href = url;"); + + if (!string.IsNullOrEmpty(cssClass)) + { + selectCulture.AddCssClass(cssClass); + } + + // TimoBreumelhof: This is not ideal but it works for now.. should really be a UL IMO + foreach (var cultureItem in cultureListItems) + { + cultureCount++; + if (locales.ContainsKey(cultureItem.Value)) + { + var option = new TagBuilder("option"); + option.Attributes.Add("value", cultureItem.Value); + option.Attributes.Add("data-link", ParseTemplate("[URL]", cultureItem.Value, localTokenReplace, currentCulture)); + option.SetInnerText(cultureItem.Text); + if (cultureItem.Value == currentCulture) + { + option.Attributes.Add("selected", "selected"); + } + + selectCulture.InnerHtml += option.ToString(); + } + } + + if (string.IsNullOrEmpty(commonHeaderTemplate)) + { + commonHeaderTemplate = Localization.GetString("CommonHeaderTemplate.Default", localResourceFile, templateCulture); + } + + if (string.IsNullOrEmpty(commonFooterTemplate)) + { + commonFooterTemplate = Localization.GetString("CommonFooterTemplate.Default", localResourceFile, templateCulture); + } + + string languageContainer = string.Empty; + languageContainer += commonHeaderTemplate; + + if (showMenu) + { + languageContainer += selectCulture.ToString(); + } + + if (showLinks) + { + if (string.IsNullOrEmpty(itemTemplate)) + { + itemTemplate = Localization.GetString("ItemTemplate.Default", localResourceFile, templateCulture); + } + + if (string.IsNullOrEmpty(alternateTemplate)) + { + alternateTemplate = Localization.GetString("AlternateTemplate.Default", localResourceFile, templateCulture); + } + + if (string.IsNullOrEmpty(selectedItemTemplate)) + { + selectedItemTemplate = Localization.GetString("SelectedItemTemplate.Default", localResourceFile, templateCulture); + } + + if (string.IsNullOrEmpty(headerTemplate)) + { + headerTemplate = Localization.GetString("HeaderTemplate.Default", localResourceFile, templateCulture); + } + + if (string.IsNullOrEmpty(footerTemplate)) + { + footerTemplate = Localization.GetString("FooterTemplate.Default", localResourceFile, templateCulture); + } + + if (string.IsNullOrEmpty(separatorTemplate)) + { + separatorTemplate = Localization.GetString("SeparatorTemplate.Default", localResourceFile, templateCulture); + } + + languageContainer += headerTemplate; + + string listItems = string.Empty; + + bool alt = false; + int i = 0; + foreach (var locale in locales.Values) + { + if (i > 0 && !string.IsNullOrEmpty(separatorTemplate)) + { + listItems += ParseTemplate(separatorTemplate, string.Empty, localTokenReplace, currentCulture); + } + i++; + + string listItem = string.Empty; + if (locale.Code == currentCulture && !string.IsNullOrEmpty(selectedItemTemplate)) + { + listItem += ParseTemplate(selectedItemTemplate, locale.Code, localTokenReplace, currentCulture); + } + else + { + if (alt) + { + listItem += ParseTemplate(alternateTemplate, locale.Code, localTokenReplace, currentCulture); + } + else + { + listItem += ParseTemplate(itemTemplate, locale.Code, localTokenReplace, currentCulture); + } + + alt = !alt; + } + + listItems += listItem; + } + + languageContainer += listItems; + + languageContainer += footerTemplate; + } + + languageContainer += ParseTemplate(commonFooterTemplate, currentCulture, localTokenReplace, currentCulture); + + if (cultureCount <= 1) + { + languageContainer = string.Empty; + } + + return new MvcHtmlString(languageContainer); + } + + private static string ParseTemplate(string template, string locale, LanguageTokenReplace localTokenReplace, string currentCulture) + { + string strReturnValue = template; + try + { + if (!string.IsNullOrEmpty(locale)) + { + localTokenReplace.Language = locale; + } + else + { + localTokenReplace.Language = currentCulture; + } + + strReturnValue = localTokenReplace.ReplaceEnvironmentTokens(strReturnValue); + } + catch (Exception ex) + { + Exceptions.ProcessPageLoadException(ex, HttpContext.Current.Request.RawUrl); + } + + return strReturnValue; + } + + private static bool LocaleIsAvailable(Locale locale, PortalSettings portalSettings) + { + var tab = portalSettings.ActiveTab; + if (tab.DefaultLanguageTab != null) + { + tab = tab.DefaultLanguageTab; + } + + var localizedTab = TabController.Instance.GetTabByCulture(tab.TabID, tab.PortalID, locale); + + return localizedTab != null && !localizedTab.IsDeleted && TabPermissionController.CanViewPage(localizedTab); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.LeftMenu.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.LeftMenu.cs new file mode 100644 index 00000000000..9c13e3e06c0 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.LeftMenu.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for legacy left menu functionality. + /// + public static partial class SkinHelpers + { + /// + /// Returns an empty string because the legacy left menu skin object is deprecated. + /// + /// The HTML helper for the current . + /// An empty HTML string. + public static IHtmlString LeftMenu(this HtmlHelper helper) + { + return new MvcHtmlString(string.Empty); // LeftMenu is deprecated and should return an empty string. + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Links.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Links.cs new file mode 100644 index 00000000000..089252612e3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Links.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.Text.RegularExpressions; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.UI; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering navigation link lists. + /// + public static partial class SkinHelpers + { + private static readonly Regex SrcRegex = new Regex("src=[']?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + /// + /// Renders a list of navigation links for tabs at the specified hierarchy level. + /// + /// The HTML helper for the current . + /// CSS class applied to each link. + /// Separator between links; may contain an image tag. + /// The tab level to render: same, child, parent, or root. + /// Layout alignment (for example, vertical for stacked links). + /// If set to true, includes disabled tabs. + /// If set to true, falls back to all tabs when no links are found at the requested level. + /// If set to true, includes the active tab in the list. + /// An HTML string containing the rendered links. + public static IHtmlString Links(this HtmlHelper helper, string cssClass = "SkinObject", string separator = " ", string level = "same", string alignment = "", bool showDisabled = false, bool forceLinks = true, bool includeActiveTab = true) + { + var portalSettings = PortalSettings.Current; + + // Separator processing + if (!string.IsNullOrEmpty(separator) && separator != " ") + { + if (separator.IndexOf("src=", StringComparison.Ordinal) != -1) + { + separator = SrcRegex.Replace(separator, "$&" + portalSettings.ActiveTab.SkinPath); + } + separator = string.Format("{1}", cssClass, separator); + } + else + { + separator = " "; + } + + string strLinks = BuildLinks(portalSettings, level, separator, cssClass, alignment, showDisabled, includeActiveTab); + + if (string.IsNullOrEmpty(strLinks) && forceLinks) + { + strLinks = BuildLinks(portalSettings, string.Empty, separator, cssClass, alignment, showDisabled, includeActiveTab); + } + + return new MvcHtmlString(strLinks); + } + + private static string BuildLinks(PortalSettings portalSettings, string level, string separator, string cssClass, string alignment, bool showDisabled, bool includeActiveTab) + { + var sbLinks = new StringBuilder(); + var portalTabs = TabController.GetTabsBySortOrder(portalSettings.PortalId); + var hostTabs = TabController.GetTabsBySortOrder(Null.NullInteger); + + foreach (TabInfo objTab in portalTabs) + { + sbLinks.Append(ProcessLink(ProcessTab(objTab, portalSettings, level, cssClass, includeActiveTab, showDisabled), sbLinks.Length, separator, alignment)); + } + + foreach (TabInfo objTab in hostTabs) + { + sbLinks.Append(ProcessLink(ProcessTab(objTab, portalSettings, level, cssClass, includeActiveTab, showDisabled), sbLinks.Length, separator, alignment)); + } + + return sbLinks.ToString(); + } + + private static string ProcessTab(TabInfo objTab, PortalSettings portalSettings, string level, string cssClass, bool includeActiveTab, bool showDisabled) + { + if (Navigation.CanShowTab(objTab, false, showDisabled)) // Assuming AdminMode is false for now as it wasn't passed, or check permissions + { + switch (level) + { + case "same": + case "": + if (objTab.ParentId == portalSettings.ActiveTab.ParentId) + { + if (includeActiveTab || objTab.TabID != portalSettings.ActiveTab.TabID) + { + return AddLink(objTab.TabName, objTab.FullUrl, cssClass); + } + } + break; + case "child": + if (objTab.ParentId == portalSettings.ActiveTab.TabID) + { + return AddLink(objTab.TabName, objTab.FullUrl, cssClass); + } + break; + case "parent": + if (objTab.TabID == portalSettings.ActiveTab.ParentId) + { + return AddLink(objTab.TabName, objTab.FullUrl, cssClass); + } + break; + case "root": + if (objTab.Level == 0) + { + return AddLink(objTab.TabName, objTab.FullUrl, cssClass); + } + break; + } + } + return string.Empty; + } + + private static string ProcessLink(string sLink, int currentLength, string separator, string alignment) + { + if (string.IsNullOrEmpty(sLink)) + { + return string.Empty; + } + + if (alignment == "vertical") + { + return string.Concat("
", separator, sLink, "
"); + } + else if (!string.IsNullOrEmpty(separator) && currentLength > 0) + { + return string.Concat(separator, sLink); + } + + return sLink; + } + + private static string AddLink(string strTabName, string strURL, string strCssClass) + { + return string.Format("{2}", strCssClass, strURL, strTabName); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Login.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Login.cs new file mode 100644 index 00000000000..69cb41f108d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Login.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Runtime.CompilerServices; + using System.Security.Policy; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Authentication; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering login/logout links. + /// + public static partial class SkinHelpers + { + private const string LoginFileName = "Login.ascx"; + + /// + /// Renders a login or logout link depending on the current authentication state. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the link. + /// Optional custom login text or HTML. + /// Optional custom logout text or HTML. + /// If set to true, renders the classic login markup; otherwise, renders enhanced markup. + /// If set to true, shows the control even on error pages. + /// An HTML string representing the login or logout UI. + public static IHtmlString Login(this HtmlHelper helper, string cssClass = "SkinObject", string text = "", string logoffText = "", bool legacyMode = true, bool showInErrorPage = false) + { + var navigationManager = helper.ViewData.Model.NavigationManager; + //TODO: CSP - enable when CSP implementation is ready + var nonce = string.Empty; //helper.ViewData.Model.ContentSecurityPolicy.Nonce; + var portalSettings = PortalSettings.Current; + var request = HttpContext.Current.Request; + + var isVisible = (!portalSettings.HideLoginControl || request.IsAuthenticated) + && (!portalSettings.InErrorPageRequest() || showInErrorPage); + + if (!isVisible) + { + return MvcHtmlString.Empty; + } + + if (legacyMode) + { + return BuildLegacyLogin(text, cssClass, logoffText, nonce, navigationManager); + } + + return BuildEnhancedLogin(text, cssClass, logoffText, nonce, navigationManager); + } + + private static MvcHtmlString BuildLegacyLogin(string text, string cssClass, string logoffText, string nonce, INavigationManager navigationManager) + { + var link = new TagBuilder("a"); + ConfigureLoginLink(link, text, cssClass, logoffText, out string loginScript, nonce, navigationManager); + return new MvcHtmlString(link.ToString() + loginScript); + } + + private static MvcHtmlString BuildEnhancedLogin(string text, string cssClass, string logoffText, string nonce, INavigationManager navigationManager) + { + var container = new TagBuilder("div"); + container.AddCssClass("loginGroup"); + + var link = new TagBuilder("a"); + link.AddCssClass("secondaryActionsList"); + ConfigureLoginLink(link, text, cssClass, logoffText, out string loginScript, nonce, navigationManager); + + container.InnerHtml = link.ToString(); + return new MvcHtmlString(container.ToString() + loginScript); + } + + private static void ConfigureLoginLink(TagBuilder link, string text, string cssClass, string logoffText, out string loginScript, string nonce, INavigationManager navigationManager) + { + var portalSettings = PortalSettings.Current; + var request = HttpContext.Current.Request; + + loginScript = string.Empty; + + if (!string.IsNullOrEmpty(cssClass)) + { + link.AddCssClass(cssClass); + } + else + { + link.AddCssClass("SkinObject"); + } + + link.Attributes["rel"] = "nofollow"; + + if (request.IsAuthenticated) + { + var displayText = !string.IsNullOrEmpty(logoffText) + ? logoffText.Replace("src=\"", "src=\"" + portalSettings.ActiveTab.SkinPath) + : Localization.GetString("Logout", GetSkinsResourceFile(LoginFileName)); + + link.SetInnerText(displayText); + link.Attributes["title"] = displayText; + link.Attributes["href"] = navigationManager.NavigateURL(portalSettings.ActiveTab.TabID, "Logoff"); + } + else + { + var displayText = !string.IsNullOrEmpty(text) + ? text.Replace("src=\"", "src=\"" + portalSettings.ActiveTab.SkinPath) + : Localization.GetString("Login", GetSkinsResourceFile(LoginFileName)); + + link.SetInnerText(displayText); + link.Attributes["title"] = displayText; + + string returnUrl = request.RawUrl; + if (returnUrl.IndexOf("?returnurl=", StringComparison.OrdinalIgnoreCase) != -1) + { + returnUrl = returnUrl.Substring(0, returnUrl.IndexOf("?returnurl=", StringComparison.OrdinalIgnoreCase)); + } + + returnUrl = HttpUtility.UrlEncode(returnUrl); + + var loginUrl = Globals.LoginURL(returnUrl, request.QueryString["override"] != null); + link.Attributes["href"] = loginUrl; + + // link.Attributes["data-url"] = loginUrl; + link.Attributes["class"] += " dnnLoginLink"; + + loginScript = GetLoginScript(loginUrl, nonce); + } + } + + private static string GetLoginScript(string loginUrl, string nonce) + { + var portalSettings = PortalSettings.Current; + var request = HttpContext.Current.Request; + + if (!request.IsAuthenticated) + { + var nonceAttribute = string.Empty; + if (!string.IsNullOrEmpty(nonce)) + { + nonceAttribute = $"nonce=\"{nonce}\""; + } + var script = string.Format( + @" + "; + + return script; + } + + return string.Empty; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Logo.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Logo.cs new file mode 100644 index 00000000000..23e641e2148 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Logo.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Linq; + using System.Web; + using System.Web.Mvc; + using System.Xml; + using System.Xml.Linq; + + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.FileSystem; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering the portal logo. + /// + public static partial class SkinHelpers + { + /// + /// Renders the portal logo as a link to the home page, optionally injecting SVG content. + /// + /// The HTML helper for the current . + /// Optional CSS border width to apply to the logo image. + /// Optional CSS class applied to the logo image element. + /// Optional CSS class applied to the anchor element. + /// If set to true, inlines SVG logo content instead of using an <img> tag. + /// An HTML string representing the logo link. + public static IHtmlString Logo(this HtmlHelper helper, string borderWidth = "", string cssClass = "", string linkCssClass = "", bool injectSvg = false) + { + var portalSettings = PortalSettings.Current; + var navigationManager = helper.ViewData.Model.NavigationManager; + + TagBuilder tbImage = new TagBuilder("img"); + if (!string.IsNullOrEmpty(borderWidth)) + { + tbImage.Attributes.Add("style", $"border-width:{borderWidth};"); + } + + if (!string.IsNullOrEmpty(cssClass)) + { + tbImage.AddCssClass(cssClass); + } + + tbImage.Attributes.Add("alt", portalSettings.PortalName); + + TagBuilder tbLink = new TagBuilder("a"); + tbLink.GenerateId("dnn_dnnLOGO_"); + if (!string.IsNullOrEmpty(linkCssClass)) + { + tbLink.AddCssClass(linkCssClass); + } + + if (!string.IsNullOrEmpty(portalSettings.LogoFile)) + { + var fileInfo = GetLogoFileInfo(portalSettings); + if (fileInfo != null) + { + if (injectSvg && "svg".Equals(fileInfo.Extension, StringComparison.OrdinalIgnoreCase)) + { + string svgContent = GetSvgContent(fileInfo, portalSettings, cssClass); + if (!string.IsNullOrEmpty(svgContent)) + { + tbLink.InnerHtml = svgContent; + } + } + else + { + string imageUrl = FileManager.Instance.GetUrl(fileInfo); + if (!string.IsNullOrEmpty(imageUrl)) + { + tbImage.Attributes.Add("src", imageUrl); + tbLink.InnerHtml = tbImage.ToString(); + } + } + } + } + + tbLink.Attributes.Add("title", portalSettings.PortalName); + tbLink.Attributes.Add("aria-label", portalSettings.PortalName); + + if (portalSettings.HomeTabId != -1) + { + tbLink.Attributes.Add("href", navigationManager.NavigateURL(portalSettings.HomeTabId)); + } + else + { + tbLink.Attributes.Add("href", Globals.AddHTTP(portalSettings.PortalAlias.HTTPAlias)); + } + + return new MvcHtmlString(tbLink.ToString()); + } + + private static IFileInfo GetLogoFileInfo(PortalSettings portalSettings) + { + string cacheKey = string.Format(DataCache.PortalCacheKey, portalSettings.PortalId, portalSettings.CultureCode) + "LogoFile"; + var file = CBO.GetCachedObject( + new CacheItemArgs(cacheKey, DataCache.PortalCacheTimeOut, DataCache.PortalCachePriority), + (CacheItemArgs itemArgs) => + { + return FileManager.Instance.GetFile(portalSettings.PortalId, portalSettings.LogoFile); + }); + + return file; + } + + private static string GetSvgContent(IFileInfo svgFile, PortalSettings portalSettings, string cssClass) + { + var cacheKey = string.Format(DataCache.PortalCacheKey, portalSettings.PortalId, portalSettings.CultureCode) + "LogoSvg"; + return CBO.GetCachedObject( + new CacheItemArgs(cacheKey, DataCache.PortalCacheTimeOut, DataCache.PortalCachePriority, svgFile), + (_) => + { + try + { + XDocument svgDocument; + using (var fileContent = FileManager.Instance.GetFileContent(svgFile)) + { + svgDocument = XDocument.Load(fileContent); + } + + var svgXmlNode = svgDocument.Descendants() + .SingleOrDefault(x => x.Name.LocalName.Equals("svg", StringComparison.Ordinal)); + if (svgXmlNode == null) + { + throw new InvalidFileContentException("The svg file has no svg node."); + } + + if (!string.IsNullOrEmpty(cssClass)) + { + // Append the css class. + var classes = svgXmlNode.Attribute("class")?.Value; + svgXmlNode.SetAttributeValue("class", string.IsNullOrEmpty(classes) ? cssClass : $"{classes} {cssClass}"); + } + + if (svgXmlNode.Descendants().FirstOrDefault(x => x.Name.LocalName.Equals("title", StringComparison.Ordinal)) == null) + { + // Add the title for ADA compliance. + var ns = svgXmlNode.GetDefaultNamespace(); + var titleNode = new XElement( + ns + "title", + new XAttribute("id", "logoTitle"), + portalSettings.PortalName); + + svgXmlNode.AddFirst(titleNode); + + // Link the title to the svg node. + svgXmlNode.SetAttributeValue("aria-labelledby", "logoTitle"); + } + + // Ensure we have the image role for ADA Compliance + svgXmlNode.SetAttributeValue("role", "img"); + + return svgDocument.ToString(); + } + catch (XmlException ex) + { + throw new InvalidFileContentException("Invalid SVG file: " + ex.Message); + } + }); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Meta.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Meta.cs new file mode 100644 index 00000000000..7db91961736 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Meta.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering meta tags into the page head. + /// + public static partial class SkinHelpers + { + /// + /// Renders a meta tag with the specified attributes. + /// + /// The HTML helper for the current . + /// The meta name attribute. + /// The meta content attribute. + /// The meta http-equiv attribute. + /// Unused for MVC rendering; kept for API parity. + /// An HTML string representing the meta tag. + public static IHtmlString Meta(this HtmlHelper helper, string name = "", string content = "", string httpEquiv = "", bool insertFirst = false) + { + var metaTag = new TagBuilder("meta"); + + if (!string.IsNullOrEmpty(name)) + { + metaTag.Attributes.Add("name", name); + } + + if (!string.IsNullOrEmpty(content)) + { + metaTag.Attributes.Add("content", content); + } + + if (!string.IsNullOrEmpty(httpEquiv)) + { + metaTag.Attributes.Add("http-equiv", httpEquiv); + } + + return new MvcHtmlString(metaTag.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Pane.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Pane.cs new file mode 100644 index 00000000000..20157bd3b44 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Pane.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering pane containers and their modules. + /// + public static partial class SkinExtensions + { + /// + /// Renders a pane and its contained modules for the current skin. + /// + /// The HTML helper for the current . + /// The pane identifier. + /// Optional CSS class applied to the pane container. + /// An HTML string representing the rendered pane. + public static IHtmlString Pane(this HtmlHelper htmlHelper, string id, string cssClass = "") + { + var model = htmlHelper.ViewData.Model; + if (model == null) + { + throw new InvalidOperationException("The model need to be present."); + } + + var editDiv = new TagBuilder("div"); + + // editDiv.GenerateId("dnn_" + id + "_SyncPanel"); + var paneDiv = new TagBuilder("div"); + paneDiv.AddCssClass("dnnPane"); + paneDiv.GenerateId("dnn_" + id); + if (model.IsEditMode) + { + editDiv.AddCssClass(cssClass); + paneDiv.AddCssClass(model.Skin.PaneCssClass); + paneDiv.Attributes["data-name"] = id; + } + else + { + paneDiv.AddCssClass(cssClass); + } + id = id.ToLower(); + + if (model.Skin.Panes.ContainsKey(id)) + { + var pane = model.Skin.Panes[id]; + paneDiv.AddCssClass(pane.CssClass); + foreach (var container in pane.Containers) + { + string sanitizedModuleName = Null.NullString; + if (!string.IsNullOrEmpty(container.Value.ModuleConfiguration.DesktopModule.ModuleName)) + { + sanitizedModuleName = Globals.CreateValidClass(container.Value.ModuleConfiguration.DesktopModule.ModuleName, false); + } + + var moduleDiv = new TagBuilder("div"); + moduleDiv.AddCssClass("DnnModule-" + container.Value.ModuleConfiguration.ModuleID); + moduleDiv.AddCssClass("DnnModule-" + sanitizedModuleName); + moduleDiv.AddCssClass("DnnModule"); + if (model.IsEditMode) + { + moduleDiv.Attributes["data-module-title"] = container.Value.ModuleConfiguration.ModuleTitle; + } + + if (Globals.IsAdminControl()) + { + moduleDiv.AddCssClass("DnnModule-Admin"); + } + + var anchor = new TagBuilder("a"); + anchor.Attributes["name"] = container.Value.ModuleConfiguration.ModuleID.ToString(); + moduleDiv.InnerHtml += anchor.ToString(); + + moduleDiv.InnerHtml += htmlHelper.Partial(container.Value.ContainerRazorFile, container.Value).ToHtmlString(); + paneDiv.InnerHtml += moduleDiv.ToString(); + } + } + else + { + paneDiv.AddCssClass("DNNEmptyPane"); + if (model.IsEditMode) + { + paneDiv.AddCssClass("EditBarEmptyPane"); + } + } + + if (model.IsEditMode) + { + // Add support for drag and drop + paneDiv.AddCssClass(" dnnSortable"); + editDiv.InnerHtml += paneDiv.ToString(); + return MvcHtmlString.Create(editDiv.ToString()); + } + else + { + return MvcHtmlString.Create(paneDiv.ToString()); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Privacy.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Privacy.cs new file mode 100644 index 00000000000..ac1a7e456ff --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Privacy.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering links to the portal privacy page. + /// + public static partial class SkinHelpers + { + /// + /// Renders a link to the portal privacy page, using a localized default label when none is provided. + /// + /// The HTML helper for the current . + /// Optional link text; when empty, a localized default is used. + /// Optional CSS class applied to the anchor. + /// Optional rel attribute value (for example, nofollow). + /// An HTML string for the privacy link. + public static IHtmlString Privacy(this HtmlHelper helper, string text = "", string cssClass = "SkinObject", string rel = "nofollow") + { + var navigationManager = helper.ViewData.Model.NavigationManager; + var portalSettings = PortalSettings.Current; + var link = new TagBuilder("a"); + + // Add Css Class + link.Attributes.Add("class", cssClass); + + // Add Text + if (string.IsNullOrWhiteSpace(text)) + { + text = Localization.GetString("Privacy.Text", GetSkinsResourceFile("Privacy.ascx")); + } + + link.SetInnerText(text); + + // Add Link + var href = portalSettings.PrivacyTabId == Null.NullInteger ? navigationManager.NavigateURL(portalSettings.ActiveTab.TabID, "Privacy") : navigationManager.NavigateURL(portalSettings.PrivacyTabId); + link.Attributes.Add("href", href); + + // Add Rel + if (!string.IsNullOrWhiteSpace(rel)) + { + link.Attributes.Add("rel", rel); + } + + return new MvcHtmlString(link.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Search.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Search.cs new file mode 100644 index 00000000000..5aad43eea54 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Search.cs @@ -0,0 +1,427 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System.Collections; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Host; + using DotNetNuke.Entities.Icons; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Framework; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Services.Localization; + //using DotNetNuke.Web.Client; + using DotNetNuke.Web.Client.ResourceManager; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering the search skin object and related scripts. + /// + public static partial class SkinHelpers + { + private const string SearchFileName = "Search.ascx"; + + /// + /// Renders the DNN search skin object for the current page. + /// + /// The HTML helper for the current . + /// The unique identifier used to generate element IDs. + /// If set to true, renders the dropdown-style search UI; otherwise, renders the classic UI. + /// If set to true, shows the web search option in classic mode. + /// If set to true, shows the site search option in classic mode. + /// Optional CSS class applied to the search button. + /// Optional submit button text; defaults to a localized "Search" label. + /// Optional icon URL for web search. + /// Optional label for web search. + /// Optional tooltip for web search. + /// Optional external web search URL. + /// Optional label for site search. + /// If set to true, uses the web search URL for site searches. + /// If set to true, enables wildcard search behavior. + /// Minimum characters required to trigger auto-search. + /// Delay in milliseconds before auto-search is triggered. + /// An HTML string representing the rendered search UI. + public static MvcHtmlString Search( + this HtmlHelper helper, + string id, + bool useDropDownList = false, + bool showWeb = true, + bool showSite = true, + string cssClass = "", + string submit = null, + string webIconURL = null, + string webText = null, + string webToolTip = null, + string webUrl = null, + string siteText = null, + bool useWebForSite = false, + bool enableWildSearch = true, + int minCharRequired = 2, + int autoSearchDelayInMilliSecond = 400) + { + var navigationManager = helper.ViewData.Model.NavigationManager; + //TODO: CSP - enable when CSP implementation is ready + var nonce = string.Empty; // helper.ViewData.Model.ContentSecurityPolicy.Nonce; + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + var controller = HtmlHelpers.GetClientResourcesController(helper); + controller.RegisterStylesheet("~/Resources/Search/SearchSkinObjectPreview.css", FileOrder.Css.ModuleCss); + controller.CreateScript("~/Resources/Search/SearchSkinObjectPreview.js") + .SetDefer() + .Register(); + + var searchType = "S"; + /* + if (this.WebRadioButton.Visible) + { + if (this.WebRadioButton.Checked) + { + this.SearchType = "W"; + } + } + */ + if (string.IsNullOrEmpty(webIconURL)) + { + webIconURL = IconController.IconURL("GoogleSearch"); + } + + if (string.IsNullOrEmpty(webText)) + { + webUrl = Localization.GetString("Web", SkinHelpers.GetSkinsResourceFile(SearchFileName)); + } + + if (string.IsNullOrEmpty(webToolTip)) + { + webUrl = Localization.GetString("Web.ToolTip", SkinHelpers.GetSkinsResourceFile(SearchFileName)); + } + + if (string.IsNullOrEmpty(webUrl)) + { + webUrl = Localization.GetString("URL", SkinHelpers.GetSkinsResourceFile(SearchFileName)); + } + + var searchUrl = SkinHelpers.ExecuteSearchUrl(string.Empty, searchType, useWebForSite, webUrl, navigationManager); + if (!useDropDownList) + { + return BuildClassicSearch(id, showWeb, showSite, cssClass, submit, webText, siteText, enableWildSearch, minCharRequired, autoSearchDelayInMilliSecond, searchUrl, nonce); + } + + return BuildDropDownSearch(id, cssClass, submit, webText, siteText, enableWildSearch, minCharRequired, autoSearchDelayInMilliSecond, searchUrl, nonce); + } + + private static MvcHtmlString BuildClassicSearch( + string id, + bool showWeb, + bool showSite, + string cssClass, + string submit, + string webText, + string siteText, + bool enableWildSearch, + int minCharRequired, + int autoSearchDelayInMilliSecond, + string searchUrl, + string nonce) + { + var container = new TagBuilder("span"); + container.GenerateId("dnn_" + id + "_ClassicSearch"); + var containerId = container.Attributes["id"]; + + if (showWeb) + { + var radio = new TagBuilder("input"); + radio.Attributes["type"] = "radio"; + radio.Attributes["name"] = "SearchType"; + radio.Attributes["value"] = "W"; + radio.Attributes["id"] = "dnn_" + id + "WebRadioButton"; + radio.Attributes["class"] = cssClass; + radio.Attributes["checked"] = "checked"; + container.InnerHtml += radio.ToString(TagRenderMode.SelfClosing); + + var label = new TagBuilder("label"); + label.Attributes["for"] = "WebRadioButton"; + label.SetInnerText(webText ?? Localization.GetString("Web", GetSkinsResourceFile(SearchFileName))); + container.InnerHtml += label.ToString(); + } + + if (showSite) + { + var radio = new TagBuilder("input"); + radio.Attributes["type"] = "radio"; + radio.Attributes["name"] = "SearchType"; + radio.Attributes["value"] = "S"; + radio.Attributes["id"] = "dnn_" + id + "SiteRadioButton"; + radio.Attributes["class"] = cssClass; + container.InnerHtml += radio.ToString(TagRenderMode.SelfClosing); + + var label = new TagBuilder("label"); + label.Attributes["for"] = "SiteRadioButton"; + label.SetInnerText(siteText ?? Localization.GetString("Site", GetSkinsResourceFile(SearchFileName))); + container.InnerHtml += label.ToString(); + } + + container.InnerHtml += BuildSearchInput(id, "txtSearch", "NormalTextBox"); + container.InnerHtml += BuildSearchButton(cssClass, submit, searchUrl); + + return new MvcHtmlString(container.ToString() + GetInitScript(false, enableWildSearch, minCharRequired, autoSearchDelayInMilliSecond, containerId, nonce)); + } + + private static MvcHtmlString BuildDropDownSearch( + string id, + string cssClass, + string submit, + string webText, + string siteText, + bool enableWildSearch, + int minCharRequired, + int autoSearchDelayInMilliSecond, + string searchUrl, + string nonce) + { + var container = new TagBuilder("div"); + container.GenerateId("dnn" + id + "DropDownSearch"); + container.AddCssClass("SearchContainer"); + var containerId = container.Attributes["id"]; + + var searchBorder = new TagBuilder("div"); + searchBorder.AddCssClass("SearchBorder"); + + var searchIcon = new TagBuilder("div"); + searchIcon.GenerateId("dnn" + id + "SearchIcon"); + searchIcon.AddCssClass("SearchIcon"); + + var img = new TagBuilder("img"); + img.Attributes["src"] = IconController.IconURL("Action"); + img.Attributes["alt"] = Localization.GetString("DropDownGlyph.AltText", GetSkinsResourceFile(SearchFileName)); + searchIcon.InnerHtml = img.ToString(TagRenderMode.SelfClosing); + + searchBorder.InnerHtml += searchIcon.ToString(); + searchBorder.InnerHtml += BuildSearchInput(id, "txtSearchNew", "SearchTextBox"); + + var choices = new TagBuilder("ul"); + choices.GenerateId("SearchChoices"); + + var siteLi = new TagBuilder("li"); + siteLi.GenerateId("dnn" + id + "_SearchIconSite"); + siteLi.SetInnerText(siteText ?? Localization.GetString("Site", GetSkinsResourceFile(SearchFileName))); + choices.InnerHtml += siteLi.ToString(); + + var webLi = new TagBuilder("li"); + webLi.GenerateId("SearchIconWeb"); + webLi.SetInnerText(webText ?? Localization.GetString("Web", GetSkinsResourceFile(SearchFileName))); + choices.InnerHtml += webLi.ToString(); + + searchBorder.InnerHtml += choices.ToString(); + container.InnerHtml = searchBorder.ToString() + BuildSearchButton(cssClass, submit, searchUrl); + + return new MvcHtmlString(container.ToString() + GetInitScript(true, enableWildSearch, minCharRequired, autoSearchDelayInMilliSecond, containerId, nonce)); + } + + private static string BuildSearchInput(string id, string inputId, string cssClass) + { + var container = new TagBuilder("span"); + container.AddCssClass("searchInputContainer"); + container.Attributes["data-moreresults"] = GetSeeMoreText(); + container.Attributes["data-noresult"] = GetNoResultText(); + + var input = new TagBuilder("input"); + input.Attributes["type"] = "text"; + input.Attributes["id"] = "dnn_" + id + "_" + inputId; + input.Attributes["class"] = cssClass; + input.Attributes["maxlength"] = "255"; + input.Attributes["autocomplete"] = "off"; + input.Attributes["placeholder"] = GetPlaceholderText(); + input.Attributes["aria-label"] = "Search"; + + var clear = new TagBuilder("a"); + clear.AddCssClass("dnnSearchBoxClearText"); + clear.Attributes["title"] = GetClearQueryText(); + clear.Attributes["href"] = "#"; + + container.InnerHtml = input.ToString(TagRenderMode.SelfClosing) + clear.ToString(); + return container.ToString(); + } + + private static string BuildSearchButton(string cssClass, string submit, string searchUrl) + { + var button = new TagBuilder("a"); + if (string.IsNullOrEmpty(cssClass)) + { + button.AddCssClass("SkinObject"); + } + else + { + button.AddCssClass(cssClass); + } + + button.Attributes["href"] = searchUrl; // "#"; + button.InnerHtml = submit ?? Localization.GetString("Search", GetSkinsResourceFile(SearchFileName)); + return button.ToString(); + } + + private static string GetInitScript(bool useDropDownList, bool enableWildSearch, int minCharRequired, int autoSearchDelayInMilliSecond, string id, string nonce) + { + var nonceAttribute = string.Empty; + if (!string.IsNullOrEmpty(nonce)) + { + nonceAttribute = $"nonce=\"{nonce}\""; + } + return string.Format( + @" + ", + autoSearchDelayInMilliSecond, + minCharRequired, + "S", + enableWildSearch.ToString().ToLowerInvariant(), + System.Threading.Thread.CurrentThread.CurrentCulture.ToString(), + PortalSettings.Current.PortalId, + useDropDownList ? "if (typeof dnn.initDropdownSearch != 'undefined') { dnn.initDropdownSearch(searchSkinObject); }" : string.Empty, + id, + nonceAttribute); + } + + private static string GetSeeMoreText() + { + return Localization.GetSafeJSString("SeeMoreResults", GetSkinsResourceFile(SearchFileName)); + } + + private static string GetNoResultText() + { + return Localization.GetSafeJSString("NoResult", GetSkinsResourceFile(SearchFileName)); + } + + private static string GetClearQueryText() + { + return Localization.GetSafeJSString("SearchClearQuery", GetSkinsResourceFile(SearchFileName)); + } + + private static string GetPlaceholderText() + { + return Localization.GetSafeJSString("Placeholder", GetSkinsResourceFile(SearchFileName)); + } + + private static string ExecuteSearchUrl(string searchText, string searchType, bool useWebForSite, string webURL, INavigationManager navigationManager) + { + PortalSettings portalSettings = PortalSettings.Current; + + int searchTabId = SkinHelpers.GetSearchTabId(portalSettings); + + if (searchTabId == Null.NullInteger) + { + return string.Empty; + } + + string strURL; + if (!string.IsNullOrEmpty(searchText)) + { + switch (searchType) + { + case "S": + // site + if (useWebForSite) + { + /* + strURL = this.SiteURL; + if (!string.IsNullOrEmpty(strURL)) + { + strURL = strURL.Replace("[TEXT]", this.Server.UrlEncode(searchText)); + strURL = strURL.Replace("[DOMAIN]", this.Request.Url.Host); + UrlUtils.OpenNewWindow(this.Page, this.GetType(), strURL); + } + */ + return string.Empty; + } + else + { + if (Host.UseFriendlyUrls) + { + return navigationManager.NavigateURL(searchTabId) /* + "?Search=" + this.Server.UrlEncode(searchText)*/; + } + else + { + return navigationManager.NavigateURL(searchTabId) /* + "&Search=" + this.Server.UrlEncode(searchText)*/; + } + } + + case "W": + // web + strURL = webURL; + if (!string.IsNullOrEmpty(strURL)) + { + /* + strURL = strURL.Replace("[TEXT]", this.Server.UrlEncode(searchText)); + strURL = strURL.Replace("[DOMAIN]", string.Empty); + UrlUtils.OpenNewWindow(this.Page, this.GetType(), strURL); + */ + } + + return string.Empty; + } + } + else + { + if (Host.UseFriendlyUrls) + { + return navigationManager.NavigateURL(searchTabId); + } + else + { + return navigationManager.NavigateURL(searchTabId); + } + } + + return string.Empty; + } + + private static int GetSearchTabId(PortalSettings portalSettings) + { + int searchTabId = portalSettings.SearchTabId; + if (searchTabId == Null.NullInteger) + { + ArrayList arrModules = ModuleController.Instance.GetModulesByDefinition(portalSettings.PortalId, "Search Results"); + if (arrModules.Count > 1) + { + foreach (ModuleInfo searchModule in arrModules) + { + if (searchModule.CultureCode == portalSettings.CultureCode) + { + searchTabId = searchModule.TabID; + } + } + } + else if (arrModules.Count == 1) + { + searchTabId = ((ModuleInfo)arrModules[0]).TabID; + } + } + + return searchTabId; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.SkinPartial.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.SkinPartial.cs new file mode 100644 index 00000000000..4734b692cd4 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.SkinPartial.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.IO; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + using System.Web.WebPages.Html; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering reusable Razor skin partials. + /// + public static partial class SkinHelpers + { + /// + /// Renders a Razor partial view located under the current skin's Views folder. + /// + /// The HTML helper for the current . + /// The partial view name (without path or extension). + /// An HTML string containing the rendered partial view. + /// Thrown when the page model is not available. + public static IHtmlString SkinPartial(this HtmlHelper helper, string name = "") + { + var model = helper.ViewData.Model; + if (model == null) + { + throw new InvalidOperationException("The model need to be present."); + } + + var skinPath = Path.GetDirectoryName(model.Skin.SkinSrc); + return helper.Partial("~" + skinPath + "/Views/" + name + ".cshtml"); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Styles.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Styles.cs new file mode 100644 index 00000000000..e6d2784df18 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Styles.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.UI.Skins; + + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for including additional stylesheet links. + /// + public static partial class SkinHelpers + { + public static IHtmlString Styles(this HtmlHelper helper, string styleSheet, string condition = "", bool isFirst = false, bool useSkinPath = true, string media = "", string name = "") + { + var skinPath = useSkinPath ? helper.ViewData.Model.Skin.SkinPath : string.Empty; + var link = new TagBuilder("link"); + + if (!string.IsNullOrEmpty(name)) + { + link.GenerateId(name); + } + + link.Attributes.Add("rel", "stylesheet"); + link.Attributes.Add("type", "text/css"); + link.Attributes.Add("href", skinPath + styleSheet); + if (!string.IsNullOrEmpty(media)) + { + link.Attributes.Add("media", media); + } + + if (string.IsNullOrEmpty(condition)) + { + return new MvcHtmlString(link.ToString()); + } + else + { + return new MvcHtmlString($""); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Tags.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Tags.cs new file mode 100644 index 00000000000..6f5b2a06758 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Tags.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering the legacy DNN tags control. + /// + public static partial class SkinHelpers + { + /// + /// Renders the legacy DNN tags control for a given object type. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the control. + /// Image URL for the add button. + /// Image URL for the cancel button. + /// Image URL for the save button. + /// If set to true, allows adding new tags. + /// If set to true, shows categories. + /// If set to true, shows tags. + /// Separator between tags. + /// The DNN object type the tags apply to (for example, "Page"). + /// The repeat direction (for example, "Horizontal"). + /// An HTML string representing the tags control. + public static IHtmlString Tags(this HtmlHelper helper, string cssClass = "", string addImageUrl = "", string cancelImageUrl = "", string saveImageUrl = "", bool allowTagging = true, bool showCategories = true, bool showTags = true, string separator = ", ", string objectType = "Page", string repeatDirection = "Horizontal") + { + var portalSettings = PortalSettings.Current; + var tagsControl = new TagBuilder("dnn:tags"); + tagsControl.Attributes.Add("id", "tagsControl"); + tagsControl.Attributes.Add("runat", "server"); + tagsControl.Attributes.Add("CssClass", cssClass); + tagsControl.Attributes.Add("AddImageUrl", addImageUrl); + tagsControl.Attributes.Add("CancelImageUrl", cancelImageUrl); + tagsControl.Attributes.Add("SaveImageUrl", saveImageUrl); + tagsControl.Attributes.Add("AllowTagging", allowTagging.ToString()); + tagsControl.Attributes.Add("ShowCategories", showCategories.ToString()); + tagsControl.Attributes.Add("ShowTags", showTags.ToString()); + tagsControl.Attributes.Add("Separator", separator); + tagsControl.Attributes.Add("ObjectType", objectType); + tagsControl.Attributes.Add("RepeatDirection", repeatDirection); + + return new MvcHtmlString(tagsControl.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Terms.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Terms.cs new file mode 100644 index 00000000000..de8225c86d2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Terms.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering links to the portal terms page. + /// + public static partial class SkinHelpers + { + /// + /// Renders a link to the portal terms page, using a localized default label when none is provided. + /// + /// The HTML helper for the current . + /// Optional link text; when empty, a localized default is used. + /// Optional CSS class applied to the anchor. + /// Optional rel attribute value (for example, nofollow). + /// An HTML string for the terms link. + public static IHtmlString Terms(this HtmlHelper helper, string text = "", string cssClass = "SkinObject", string rel = "nofollow") + { + var navigationManager = helper.ViewData.Model.NavigationManager; + var portalSettings = PortalSettings.Current; + var link = new TagBuilder("a"); + + // Add Css Class + link.Attributes.Add("class", cssClass); + + // Add Text + if (string.IsNullOrWhiteSpace(text)) + { + text = Localization.GetString("Terms.Text", GetSkinsResourceFile("Terms.ascx")); + } + + link.SetInnerText(text); + + // Add Link + var href = portalSettings.TermsTabId == Null.NullInteger ? navigationManager.NavigateURL(portalSettings.ActiveTab.TabID, "Terms") : navigationManager.NavigateURL(portalSettings.TermsTabId); + link.Attributes.Add("href", href); + + // Add Rel + if (!string.IsNullOrWhiteSpace(rel)) + { + link.Attributes.Add("rel", rel); + } + + return new MvcHtmlString(link.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Text.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Text.cs new file mode 100644 index 00000000000..47eb275c3e6 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Text.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.IO; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Tokens; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering localized or tokenized text. + /// + public static partial class SkinHelpers + { + /// + /// Renders a text span, optionally localized via a resource key and with token replacement. + /// + /// The HTML helper for the current . + /// The default text to show. + /// Optional CSS class applied to the span. + /// Optional resource key used to look up localized text. + /// If set to true, runs DNN token replacement on the final text. + /// An HTML string representing the rendered text. + public static IHtmlString Text(this HtmlHelper helper, string showText = "", string cssClass = "", string resourceKey = "", bool replaceTokens = false) + { + var portalSettings = PortalSettings.Current; + var text = showText; + + if (!string.IsNullOrEmpty(resourceKey)) + { + var file = Path.GetFileName(helper.ViewContext.HttpContext.Server.MapPath(portalSettings.ActiveTab.SkinSrc)); + file = portalSettings.ActiveTab.SkinPath + Localization.LocalResourceDirectory + "/" + file; + var localization = Localization.GetString(resourceKey, file); + if (!string.IsNullOrEmpty(localization)) + { + text = localization; + } + } + + if (replaceTokens) + { + var tr = new TokenReplace { AccessingUser = portalSettings.UserInfo }; + text = tr.ReplaceEnvironmentTokens(text); + } + + var label = new TagBuilder("span"); + label.SetInnerText(text); + if (!string.IsNullOrEmpty(cssClass)) + { + label.AddCssClass(cssClass); + } + + return new MvcHtmlString(label.ToString()); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Toast.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Toast.cs new file mode 100644 index 00000000000..7a454b3feba --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Toast.cs @@ -0,0 +1,211 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Web; + using System.Web.Mvc; + using System.Xml; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Entities.Users; + using DotNetNuke.Framework; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for rendering toast notification scripts. + /// + public static partial class SkinHelpers + { + private static readonly string ToastCacheKey = "DNN_Toast_Config"; + + /// + /// Renders the client-side script required to show toast notifications for the current user. + /// + /// The HTML helper for the current . + /// Unused parameter preserved for API compatibility. + /// An HTML string containing the toast initialization script, or empty if the user is offline. + public static IHtmlString Toast(this HtmlHelper helper, string cssClass = "SkinObject") + { + if (!IsOnline()) + { + return MvcHtmlString.Empty; + } + + var portalSettings = PortalSettings.Current; + + // Register Resources + var javaScript = Globals.GetCurrentServiceProvider().GetRequiredService(); + javaScript.RequestRegistration(CommonJs.jQueryUI); + + var clientResourceController = Globals.GetCurrentServiceProvider().GetRequiredService(); + clientResourceController.RegisterScript("~/Resources/Shared/components/Toast/jquery.toastmessage.js", FileOrder.Js.jQuery); + clientResourceController.RegisterStylesheet("~/Resources/Shared/components/Toast/jquery.toastmessage.css", FileOrder.Css.DefaultCss); + + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + + var config = InitializeToastConfig(); + string serviceModuleName = config["ServiceModuleName"]; + string serviceAction = config["ServiceAction"]; + string additionalScripts = config.ContainsKey("AddtionalScripts") ? config["AddtionalScripts"] : ""; + + string notificationLink = GetNotificationLink(portalSettings); + string notificationLabel = GetNotificationLabel(); + + var script = $@" + +{additionalScripts}"; + + return new MvcHtmlString(script); + } + + private static bool IsOnline() + { + var userInfo = UserController.Instance.GetCurrentUserInfo(); + return userInfo.UserID != -1; + } + + private static string GetNotificationLink(PortalSettings portalSettings) + { + return GetMessageLink(portalSettings) + "?view=notifications&action=notifications"; + } + + private static string GetMessageLink(PortalSettings portalSettings) + { + var navigationManager = Globals.GetCurrentServiceProvider().GetRequiredService(); + return navigationManager.NavigateURL(GetMessageTab(portalSettings), string.Empty, string.Format("userId={0}", portalSettings.UserId)); + } + + private static string GetNotificationLabel() + { + return Localization.GetString("SeeAllNotification", GetSkinsResourceFile("Toast.ascx")); + } + + private static IDictionary InitializeToastConfig() + { + var config = new Dictionary + { + { "ServiceModuleName", "InternalServices" }, + { "ServiceAction", "NotificationsService/GetToasts" } + }; + + try + { + var toastConfig = DataCache.GetCache>(ToastCacheKey); + if (toastConfig != null) + { + return toastConfig; + } + + // Try to find Toast.config in admin/skins + var configFile = HttpContext.Current.Server.MapPath("~/admin/Skins/Toast.config"); + + if (File.Exists(configFile)) + { + var xmlDocument = new XmlDocument { XmlResolver = null }; + xmlDocument.Load(configFile); + var moduleNameNode = xmlDocument.DocumentElement?.SelectSingleNode("moduleName"); + var actionNode = xmlDocument.DocumentElement?.SelectSingleNode("action"); + var scriptsNode = xmlDocument.DocumentElement?.SelectSingleNode("scripts"); + + if (moduleNameNode != null && !string.IsNullOrEmpty(moduleNameNode.InnerText)) + { + config["ServiceModuleName"] = moduleNameNode.InnerText; + } + + if (actionNode != null && !string.IsNullOrEmpty(actionNode.InnerText)) + { + config["ServiceAction"] = actionNode.InnerText; + } + + if (scriptsNode != null && !string.IsNullOrEmpty(scriptsNode.InnerText)) + { + config["AddtionalScripts"] = scriptsNode.InnerText; + } + } + + DataCache.SetCache(ToastCacheKey, config); + } + catch (Exception ex) + { + //DotNetNuke.Instrumentation.LoggerSource.Instance.GetLogger(typeof(SkinHelpers)).Error(ex); + } + + return config; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.User.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.User.cs new file mode 100644 index 00000000000..4679226ed81 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.User.cs @@ -0,0 +1,372 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Controllers; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Entities.Users; + using DotNetNuke.Services.Authentication; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Social.Messaging.Internal; + using DotNetNuke.Services.Social.Notifications; + using DotNetNuke.Web.MvcPipeline.Models; + + /// + /// Skin helper methods for rendering user/register/profile links and counts. + /// + public static partial class SkinHelpers + { + private static string userResourceFile = GetSkinsResourceFile("User.ascx"); + + /// + /// Renders user-related UI, including register/login links or profile, messages, and notifications for authenticated users. + /// + /// The HTML helper for the current . + /// Optional CSS class applied to the rendered elements. + /// Optional custom registration text or HTML. + /// Optional registration URL override. + /// If set to true, shows unread message/notification counts. + /// If set to true, shows the user avatar. + /// If set to true, renders the classic user skin object; otherwise, renders enhanced markup. + /// If set to true, shows the control even on error pages. + /// An HTML string representing the user UI or an empty string when not visible. + public static IHtmlString User(this HtmlHelper helper, string cssClass = "SkinObject", string text = "", string url = "", bool showUnreadMessages = true, bool showAvatar = true, bool legacyMode = true, bool showInErrorPage = false) + { + // TODO: CSP - enable when CSP implementation is ready + var nonce = string.Empty; // helper.ViewData.Model.ContentSecurityPolicy.Nonce; + var portalSettings = PortalSettings.Current; + var navigationManager = helper.ViewData.Model.NavigationManager; + + if (portalSettings.InErrorPageRequest() && !showInErrorPage) + { + return MvcHtmlString.Empty; + } + + var registerText = Localization.GetString("Register", userResourceFile); + if (!string.IsNullOrEmpty(text)) + { + registerText = text; + if (text.IndexOf("src=") != -1) + { + registerText = text.Replace("src=\"", "src=\"" + portalSettings.ActiveTab.SkinPath); + } + } + + if (legacyMode) + { + if (helper.ViewContext.HttpContext.Request.IsAuthenticated == false) + { + if (portalSettings.UserRegistration == (int)Globals.PortalRegistrationType.NoRegistration || + (portalSettings.Users > portalSettings.UserQuota && portalSettings.UserQuota != 0)) + { + return MvcHtmlString.Empty; + } + + var registerLink = new TagBuilder("a"); + registerLink.AddCssClass("dnnRegisterLink"); + if (!string.IsNullOrEmpty(cssClass)) + { + registerLink.AddCssClass(cssClass); + } + + registerLink.Attributes.Add("rel", "nofollow"); + registerLink.InnerHtml = registerText; + registerLink.Attributes.Add("href", !string.IsNullOrEmpty(url) ? url : Globals.RegisterURL(HttpUtility.UrlEncode(navigationManager.NavigateURL()), Null.NullString)); + + string registerScript = string.Empty; + if (portalSettings.EnablePopUps && portalSettings.RegisterTabId == Null.NullInteger && !AuthenticationController.HasSocialAuthenticationEnabled(null)) + { + // var clickEvent = "return " + UrlUtils.PopUpUrl(registerLink.Attributes["href"], portalSettings, true, false, 600, 950); + // registerLink.Attributes.Add("onclick", clickEvent); + registerScript = GetRegisterScript(registerLink.Attributes["href"], nonce); + } + return new MvcHtmlString(registerLink.ToString() + registerScript); + } + else + { + var userInfo = UserController.Instance.GetCurrentUserInfo(); + if (userInfo.UserID != -1) + { + var userDisplayText = userInfo.DisplayName; + var userDisplayTextUrl = Globals.UserProfileURL(userInfo.UserID); + var userDisplayTextToolTip = Localization.GetString("VisitMyProfile", userResourceFile); + var userLink = new TagBuilder("a"); + userLink.AddCssClass("dnnUserLink"); + if (!string.IsNullOrEmpty(cssClass)) + { + userLink.AddCssClass(cssClass); + } + + userLink.Attributes.Add("href", userDisplayTextUrl); + userLink.Attributes.Add("title", userDisplayTextToolTip); + userLink.InnerHtml = userDisplayText; + return new MvcHtmlString(userLink.ToString()); + } + + return MvcHtmlString.Empty; + } + } + else + { + string registerScript = string.Empty; + var userWrapperDiv = new TagBuilder("div"); + userWrapperDiv.AddCssClass("registerGroup"); + var ul = new TagBuilder("ul"); + ul.AddCssClass("buttonGroup"); + if (!HttpContext.Current.Request.IsAuthenticated) + { + // Unauthenticated User Logic + if (portalSettings.UserRegistration != (int)Globals.PortalRegistrationType.NoRegistration && + (portalSettings.Users < portalSettings.UserQuota || portalSettings.UserQuota == 0)) + { + // User Register + var registerLi = new TagBuilder("li"); + registerLi.AddCssClass("userRegister"); + + var registerLink = new TagBuilder("a"); + registerLink.AddCssClass("dnnRegisterLink"); + registerLink.AddCssClass(cssClass); + registerLink.Attributes.Add("rel", "nofollow"); + registerLink.InnerHtml = !string.IsNullOrEmpty(text) ? text.Replace("src=\"", "src=\"" + portalSettings.ActiveTab.SkinPath) : Localization.GetString("Register", userResourceFile); + registerLink.Attributes.Add("href", !string.IsNullOrEmpty(url) ? url : Globals.RegisterURL(HttpUtility.UrlEncode(navigationManager.NavigateURL()), Null.NullString)); + + if (portalSettings.EnablePopUps && portalSettings.RegisterTabId == Null.NullInteger/*&& !AuthenticationController.HasSocialAuthenticationEnabled(portalSettings)*/) + { + // var clickEvent = "return " + UrlUtils.PopUpUrl(registerLink.Attributes["href"], portalSettings, true, false, 600, 950); + // registerLink.Attributes.Add("onclick", clickEvent); + registerScript = GetRegisterScript(registerLink.Attributes["href"], nonce); + } + + registerLi.InnerHtml = registerLink.ToString(); + ul.InnerHtml += registerLi.ToString(); + } + } + else + { + var userInfo = UserController.Instance.GetCurrentUserInfo(); + if (userInfo.UserID != -1) + { + // Add menu-items (viewProfile, userMessages, userNotifications, etc.) + if (showUnreadMessages) + { + // Create Messages + var unreadMessages = InternalMessagingController.Instance.CountUnreadMessages(userInfo.UserID, PortalController.GetEffectivePortalId(userInfo.PortalID)); + + var messageLinkText = unreadMessages > 0 ? string.Format(Localization.GetString("Messages", userResourceFile), unreadMessages) : string.Format(Localization.GetString("NoMessages", userResourceFile)); + ul.InnerHtml += CreateMenuItem(messageLinkText, "userMessages", navigationManager.NavigateURL(GetMessageTab(portalSettings))); + + // Create Notifications + var unreadAlerts = NotificationsController.Instance.CountNotifications(userInfo.UserID, PortalController.GetEffectivePortalId(userInfo.PortalID)); + var alertLink = navigationManager.NavigateURL(GetMessageTab(portalSettings), string.Empty, string.Format("userId={0}", userInfo.UserID), "view=notifications", "action=notifications"); + var alertLinkText = unreadAlerts > 0 ? string.Format(Localization.GetString("Notifications", userResourceFile), unreadAlerts) : string.Format(Localization.GetString("NoNotifications", userResourceFile)); + + ul.InnerHtml += CreateMenuItem(alertLinkText, "userNotifications", alertLink); + } + + // Create User Display Name Link + var userDisplayText = userInfo.DisplayName; + var userDisplayTextUrl = Globals.UserProfileURL(userInfo.UserID); + var userDisplayTextToolTip = Localization.GetString("VisitMyProfile", userResourceFile); + + ul.InnerHtml += CreateMenuItem(userDisplayText, "userDisplayName", userDisplayTextUrl); + + if (showAvatar) + { + var userProfileLi = new TagBuilder("li"); + userProfileLi.AddCssClass("userProfile"); + + // Get the Profile Image + var profileImg = new TagBuilder("img"); + profileImg.Attributes.Add("src", UserController.Instance.GetUserProfilePictureUrl(userInfo.UserID, 32, 32)); + profileImg.Attributes.Add("alt", Localization.GetString("ProfilePicture", userResourceFile)); + + ul.InnerHtml += CreateMenuItem(profileImg.ToString(), "userProfileImg", userDisplayTextUrl); + } + } + } + + userWrapperDiv.InnerHtml = ul.ToString(); + return new MvcHtmlString(userWrapperDiv.ToString() + registerScript); + } + } + + private static string CreateMenuItem(string cssClass, string href, string resourceKey, bool isStrong = false) + { + var li = new TagBuilder("li"); + li.AddCssClass(cssClass); + + var a = new TagBuilder("a"); + a.Attributes.Add("href", href); + var text = Localization.GetString(resourceKey, userResourceFile); + a.InnerHtml = isStrong ? $"{text}" : text; + + li.InnerHtml = a.ToString(); + return li.ToString(); + } + + private static string CreateMenuItem(string text, string cssClass, string href) + { + var li = new TagBuilder("li"); + li.AddCssClass(cssClass); + + var a = new TagBuilder("a"); + a.Attributes.Add("href", href); + + a.InnerHtml += text; + + li.InnerHtml = a.ToString(); + return li.ToString(); + } + + private static string CreateMessageMenuItem(string cssClass, string href, string resourceKey, int count) + { + var li = new TagBuilder("li"); + li.AddCssClass(cssClass); + + var a = new TagBuilder("a"); + a.Attributes.Add("href", href); + + if (count > 0 || AlwaysShowCount(PortalSettings.Current)) + { + var span = new TagBuilder("span"); + span.AddCssClass(cssClass == "userMessages" ? "messageCount" : "notificationCount"); + span.InnerHtml = count.ToString(); + a.InnerHtml = span.ToString(); + } + + var innerText = Localization.GetString(resourceKey, userResourceFile); + innerText = string.Format(innerText, count.ToString()); + a.InnerHtml += innerText; + + li.InnerHtml = a.ToString(); + return li.ToString(); + } + + private static int GetMessageTab(PortalSettings portalSettings) + { + var cacheKey = string.Format("MessageCenterTab:{0}:{1}", portalSettings.PortalId, portalSettings.CultureCode); + var messageTabId = DataCache.GetCache(cacheKey); + if (messageTabId > 0) + { + return messageTabId; + } + + // Find the Message Tab + messageTabId = FindMessageTab(portalSettings); + + // save in cache + DataCache.SetCache(cacheKey, messageTabId, TimeSpan.FromMinutes(20)); + + return messageTabId; + } + + private static int FindMessageTab(PortalSettings portalSettings) + { + var profileTab = TabController.Instance.GetTab(portalSettings.UserTabId, portalSettings.PortalId, false); + if (profileTab != null) + { + var childTabs = TabController.Instance.GetTabsByPortal(profileTab.PortalID).DescendentsOf(profileTab.TabID); + foreach (TabInfo tab in childTabs) + { + foreach (KeyValuePair kvp in ModuleController.Instance.GetTabModules(tab.TabID)) + { + var module = kvp.Value; + if (module.DesktopModule.FriendlyName == "Message Center" && !module.IsDeleted) + { + return tab.TabID; + } + } + } + } + + // default to User Profile Page + return portalSettings.UserTabId; + } + + private static bool AlwaysShowCount(PortalSettings portalSettings) + { + const string SettingKey = "UserAndLogin_AlwaysShowCount"; + var alwaysShowCount = false; + + var portalSetting = PortalController.GetPortalSetting(SettingKey, portalSettings.PortalId, string.Empty); + if (!string.IsNullOrEmpty(portalSetting) && bool.TryParse(portalSetting, out alwaysShowCount)) + { + return alwaysShowCount; + } + + var hostSetting = HostController.Instance.GetString(SettingKey, string.Empty); + if (!string.IsNullOrEmpty(hostSetting) && bool.TryParse(hostSetting, out alwaysShowCount)) + { + return alwaysShowCount; + } + + return false; + } + + private static string GetRegisterScript(string loginUrl, string nonce) + { + var portalSettings = PortalSettings.Current; + var request = HttpContext.Current.Request; + + if (!request.IsAuthenticated) + { + var nonceAttribute = string.Empty; + if (!string.IsNullOrEmpty(nonce)) + { + nonceAttribute = $"nonce=\"{nonce}\""; + } + var script = string.Format( + @" + "; + + return script; + } + + return string.Empty; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.UserAndLogin.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.UserAndLogin.cs new file mode 100644 index 00000000000..a65b90f51aa --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.UserAndLogin.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Text; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.Application; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Controllers; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Entities.Users; + using DotNetNuke.Services.Authentication; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Social.Messaging.Internal; + using DotNetNuke.Services.Social.Notifications; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for rendering combined user and login UI elements. + /// + public static partial class SkinHelpers + { + /// + /// Renders the combined user/login menu used by legacy DNN skins. + /// + /// The HTML helper for the current . + /// If set to true, shows the control even on error pages. + /// An HTML string representing the user/login UI. + public static IHtmlString UserAndLogin(this HtmlHelper helper, bool showInErrorPage = false) + { + var portalSettings = PortalSettings.Current; + var navigationManager = helper.ViewData.Model.NavigationManager; + + if (!showInErrorPage && portalSettings.InErrorPageRequest()) + { + return MvcHtmlString.Empty; + } + + var sb = new StringBuilder(); + sb.Append("
"); + + var result = sb.ToString(); + + if (UsePopUp(portalSettings)) + { + result = result.Replace("id=\"registerLink\"", $"id=\"registerLink\" onclick=\"{RegisterUrlForClickEvent(navigationManager, portalSettings, helper)}\""); + result = result.Replace("id=\"loginLink\"", $"id=\"loginLink\" onclick=\"{LoginUrlForClickEvent(portalSettings, helper)}\""); + } + + return new MvcHtmlString(result); + } + + private static bool CanRegister(PortalSettings portalSettings) + { + return (portalSettings.UserRegistration != (int)Globals.PortalRegistrationType.NoRegistration) + && (portalSettings.Users < portalSettings.UserQuota || portalSettings.UserQuota == 0); + } + + private static string RegisterUrl(INavigationManager navigationManager) + { + return Globals.RegisterURL(HttpUtility.UrlEncode(navigationManager.NavigateURL()), Null.NullString); + } + + private static string LoginUrl() + { + string returnUrl = HttpContext.Current.Request.RawUrl; + if (returnUrl.IndexOf("?returnurl=", StringComparison.OrdinalIgnoreCase) != -1) + { + returnUrl = returnUrl.Substring(0, returnUrl.IndexOf("?returnurl=", StringComparison.OrdinalIgnoreCase)); + } + + returnUrl = HttpUtility.UrlEncode(returnUrl); + + return Globals.LoginURL(returnUrl, HttpContext.Current.Request.QueryString["override"] != null); + } + + private static bool UsePopUp(PortalSettings portalSettings) + { + return portalSettings.EnablePopUps + && portalSettings.LoginTabId == Null.NullInteger + /* && !AuthenticationController.HasSocialAuthenticationEnabled(portalSettings)*/; + } + + private static string RegisterUrlForClickEvent(INavigationManager navigationManager, PortalSettings portalSettings, HtmlHelper helper) + { + return "return " + UrlUtils.PopUpUrl(HttpUtility.UrlDecode(RegisterUrl(navigationManager)), portalSettings, true, false, 600, 950); + } + + private static string LoginUrlForClickEvent(PortalSettings portalSettings, HtmlHelper helper) + { + return "return " + UrlUtils.PopUpUrl(HttpUtility.UrlDecode(LoginUrl()), portalSettings, true, false, 300, 650); + } + + private static string LocalizeString(HtmlHelper helper, string key) + { + return Localization.GetString(key, GetSkinsResourceFile("UserAndLogin.ascx")); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.cs new file mode 100644 index 00000000000..9a952b4cac2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Collections; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Controllers; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin-related helper methods shared across MVC skin helper partials. + /// + public static partial class SkinHelpers + { + /// + /// Gets the path to a resource file located in the specified template directory. + /// + /// The template source directory. + /// The resource file name. + /// The full relative path to the resource file. + public static string GetResourceFile(string templateSourceDirectory, string fileName) + { + return templateSourceDirectory + "/" + Localization.LocalResourceDirectory + "/" + fileName; + } + + /// + /// Gets the path to a resource file located under the admin skins directory. + /// + /// The resource file name. + /// The full relative path to the admin skins resource file. + public static string GetSkinsResourceFile(string fileName) + { + return GetResourceFile("/admin/Skins", fileName); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.jQuery.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.jQuery.cs new file mode 100644 index 00000000000..95e606215a7 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.jQuery.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + using DotNetNuke.Common; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Web.MvcPipeline.Models; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Skin helper methods for requesting jQuery and related JavaScript libraries. + /// + public static partial class SkinHelpers + { + /// + /// Requests registration of jQuery and optional related libraries for the current page. + /// + /// The HTML helper for the current . + /// If set to true, requests the DNN jQuery plugins bundle. + /// If set to true, requests the HoverIntent plugin. + /// If set to true, requests jQuery UI. + /// An empty HTML string; scripts are registered via the JavaScript library helper. + public static IHtmlString JQuery(this HtmlHelper helper, bool dnnjQueryPlugins = false, bool jQueryHoverIntent = false, bool jQueryUI = false) + { + var javaScript = HtmlHelpers.GetDependencyProvider(helper).GetRequiredService(); + + javaScript.RequestRegistration(CommonJs.jQuery); + javaScript.RequestRegistration(CommonJs.jQueryMigrate); + + if (jQueryUI) + { + javaScript.RequestRegistration(CommonJs.jQueryUI); + } + + if (dnnjQueryPlugins) + { + javaScript.RequestRegistration(CommonJs.DnnPlugins); + } + + if (jQueryHoverIntent) + { + javaScript.RequestRegistration(CommonJs.HoverIntent); + } + + return new MvcHtmlString(string.Empty); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Startup.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Startup.cs new file mode 100644 index 00000000000..86a3c67d081 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Startup.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline +{ + using System.Web.Mvc; + + using DotNetNuke.Common; + using DotNetNuke.Common.Internal; + using DotNetNuke.DependencyInjection; + using DotNetNuke.Web.Mvc.Extensions; + using DotNetNuke.Web.MvcPipeline.ModelFactories; + using DotNetNuke.Web.MvcPipeline.Routing; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + + /// + /// Configures services and MVC controllers used by the DNN MVC pipeline. + /// + public class Startup : IDnnStartup + { + /// + public void ConfigureServices(IServiceCollection services) + { + services.TryAddEnumerable(new ServiceDescriptor(typeof(IRoutingManager), typeof(MvcRoutingManager), ServiceLifetime.Singleton)); + + services.AddMvcControllers(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + DependencyResolver.SetResolver(new DnnMvcPipelineDependencyResolver(Globals.DependencyProvider)); + + // TODO: CSP - enable when CSP implementation is ready + // services.AddScoped(); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/UI/Utilities/MvcClientAPI.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/UI/Utilities/MvcClientAPI.cs new file mode 100644 index 00000000000..62a875abead --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/UI/Utilities/MvcClientAPI.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.UI.Utilities +{ + using System; + using System.Collections.Generic; + using System.Web; + + /// + /// Helper methods for collecting client-side variables and startup scripts for MVC views. + /// + public class MvcClientAPI + { + /// + /// Gets the dictionary of client variables for the current HTTP request, + /// creating it when it does not yet exist. + /// + /// + /// A dictionary containing client variables for the current request. + /// + public static Dictionary GetClientVariableList() + { + var dic = HttpContext.Current.Items["CAPIVariableList"] as Dictionary; + if (dic == null) + { + dic = new Dictionary(); + HttpContext.Current.Items["CAPIVariableList"] = dic; + } + + return dic; + } + + /// + /// Gets the dictionary of client startup scripts for the current HTTP request, + /// creating it when it does not yet exist. + /// + /// + /// A dictionary containing client startup scripts for the current request. + /// + public static Dictionary GetClientStartupScriptList() + { + var dic = HttpContext.Current.Items["CAPIStartupScriptList"] as Dictionary; + if (dic == null) + { + dic = new Dictionary(); + HttpContext.Current.Items["CAPIStartupScriptList"] = dic; + } + + return dic; + } + + /// + /// Registers a client variable for the current request. + /// + /// The variable key. + /// The variable value. + /// + /// If set to true, overwrites an existing value for the same key; otherwise the existing value is preserved. + /// + public static void RegisterClientVariable(string key, string value, bool overwrite) + { + var variables = GetClientVariableList(); + if (!overwrite && variables.ContainsKey(key)) + { + return; + } + + variables[key] = value; + } + + /// + /// Registers a client variable for an embedded resource. + /// + /// The resource file name. + /// A type from the assembly containing the embedded resource. + /// Always thrown; this method is not yet implemented. + public static void RegisterEmbeddedResource(string fileName, Type assemblyType) + { + // RegisterClientVariable(FileName + ".resx", ThePage.ClientScript.GetWebResourceUrl(AssemblyType, FileName), true); + throw new NotImplementedException(); + } + + /// + /// Registers a startup script for the current request under the specified key. + /// + /// The script key. + /// The script content. + public static void RegisterStartupScript(string key, string value) + { + var scripts = GetClientStartupScriptList(); + if (!scripts.ContainsKey(key)) + { + scripts.Add(key, value); + } + } + + /// + /// Registers a script for the current request under the specified key. + /// + /// The script key. + /// The script content. + public static void RegisterScript(string key, string value) + { + var scripts = GetClientStartupScriptList(); + if (!scripts.ContainsKey(key)) + { + scripts.Add(key, value); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/EmptyController.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/EmptyController.cs new file mode 100644 index 00000000000..6ef275f8af5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/EmptyController.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Utils +{ + using System.Web.Mvc; + + /// + /// Empty MVC controller used to create a new for . + /// + public class EmptyController : Controller + { + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/FakeView.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/FakeView.cs new file mode 100644 index 00000000000..1a594d80c94 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/FakeView.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Utils +{ + using System.IO; + using System.Web.Mvc; + + /// + /// Fake view implementation for HtmlHelper contexts that don't require actual view rendering. + /// + internal class FakeView : IView + { + /// + public void Render(ViewContext viewContext, TextWriter writer) + { + // No-op implementation since we're only using this for HtmlHelper context. + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/MvcViewEngine.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/MvcViewEngine.cs new file mode 100644 index 00000000000..a68dc2c2911 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/MvcViewEngine.cs @@ -0,0 +1,362 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Utils +{ + using System; + using System.IO; + using System.Web; + using System.Web.Mvc; + using System.Web.Routing; + + /// + /// Renders MVC views and HtmlHelper output to strings or writers using the standard MVC view engine. + /// Requires an ASP.NET to be present, but can be used outside of a controller. + /// + public class MvcViewEngine + { + /// + /// Initializes a new instance of the class. + /// + /// + /// If you are running within the context of an ASP.NET MVC request, pass in + /// the controller's context. Only leave out the context if no context is otherwise available. + /// + public MvcViewEngine(ControllerContext controllerContext = null) + { + // Create a known controller from HttpContext if no context is passed. + if (controllerContext == null) + { + if (HttpContext.Current != null) + { + controllerContext = CreateController().ControllerContext; + } + else + { + throw new InvalidOperationException( + "ViewRenderer must run in the context of an ASP.NET " + + "Application and requires HttpContext.Current to be present."); + } + } + + this.Context = controllerContext; + } + + /// + /// Gets or sets the current controller context used for rendering. + /// + protected ControllerContext Context { get; set; } + + /// + /// Renders a partial MVC view to string. Use this method to render + /// a partial view that doesn't merge with _Layout and doesn't fire + /// _ViewStart. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// The model to pass to the view renderer. + /// String of the rendered view or null on error. + public string RenderPartialViewToString(string viewPath, object model = null) + { + return this.RenderViewToStringInternal(viewPath, model, true); + } + + /// + /// Renders a partial MVC view to given Writer. Use this method to render + /// a partial view that doesn't merge with _Layout and doesn't fire + /// _ViewStart. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// The model to pass to the view renderer. + /// Writer to render the view to. + public void RenderPartialView(string viewPath, object model, TextWriter writer) + { + this.RenderViewToWriterInternal(viewPath, writer, model, true); + } + + /// + /// Renders a full MVC view to a string. Will render with the full MVC + /// view engine including running _ViewStart and merging into _Layout. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// The model to render the view with. + /// String of the rendered view or null on error. + public string RenderViewToString(string viewPath, object model = null) + { + return this.RenderViewToStringInternal(viewPath, model, false); + } + + /// + /// Renders a full MVC view to a writer. Will render with the full MVC + /// view engine including running _ViewStart and merging into _Layout. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// The model to render the view with. + /// Writer to render the view to. + public void RenderView(string viewPath, object model, TextWriter writer) + { + this.RenderViewToWriterInternal(viewPath, writer, model, false); + } + + /// + /// Renders a partial MVC view to string. Use this method to render + /// a partial view that doesn't merge with _Layout and doesn't fire + /// _ViewStart. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// The model to pass to the view renderer. + /// Active controller context. + /// String of the rendered view or null on error. + public static string RenderPartialView(string viewPath, object model = null, ControllerContext controllerContext = null) + { + var renderer = new MvcViewEngine(controllerContext); + return renderer.RenderPartialViewToString(viewPath, model); + } + + /// + /// Renders a partial MVC view to string. Use this method to render + /// a partial view that doesn't merge with _Layout and doesn't fire + /// _ViewStart. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// Text writer to render view to. + /// The model to pass to the view renderer. + /// Active controller context. + public static void RenderPartialView(string viewPath, TextWriter writer, object model = null, ControllerContext controllerContext = null) + { + var renderer = new MvcViewEngine(controllerContext); + renderer.RenderPartialView(viewPath, model, writer); + } + + /// + /// Renders an HtmlHelper delegate to a string. This method creates a temporary view context + /// and captures the output of the HtmlHelper function. + /// + /// A function that takes an and returns or . + /// The model to attach to the view data. + /// Active controller context (optional). + /// String representation of the rendered HTML helper output. + public static string RenderHtmlHelperToString(Func htmlHelperFunc, object model = null, ControllerContext controllerContext = null) + { + if (htmlHelperFunc == null) + { + throw new ArgumentNullException(nameof(htmlHelperFunc)); + } + + var renderer = new MvcViewEngine(controllerContext); + return renderer.RenderHtmlHelperToStringInternal(htmlHelperFunc, model); + } + + /// + /// Renders an HtmlHelper delegate to a string with error handling. This method creates a temporary view context + /// and captures the output of the HtmlHelper function. + /// + /// A function that takes an and returns or . + /// The model to attach to the view data. + /// Active controller context (optional). + /// Output parameter that captures any error message instead of throwing. + /// String representation of the rendered HTML helper output or null on error. + public static string RenderHtmlHelperToString( + Func htmlHelperFunc, + object model, + ControllerContext controllerContext, + out string errorMessage) + { + errorMessage = null; + try + { + return RenderHtmlHelperToString(htmlHelperFunc, model, controllerContext); + } + catch (Exception ex) + { + errorMessage = ex.GetBaseException().Message; + return null; + } + } + + /// + /// Internal method that handles rendering of either partial or full views. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// Text writer to render view to. + /// Model to render the view with. + /// Determines whether to render a full or partial view. + protected void RenderViewToWriterInternal(string viewPath, TextWriter writer, object model = null, bool partial = false) + { + // First find the ViewEngine for this view. + ViewEngineResult viewEngineResult = null; + if (partial) + { + viewEngineResult = ViewEngines.Engines.FindPartialView(this.Context, viewPath); + } + else + { + viewEngineResult = ViewEngines.Engines.FindView(this.Context, viewPath, null); + } + + if (viewEngineResult == null) + { + throw new FileNotFoundException(); + } + + // Get the view and attach the model to view data. + var view = viewEngineResult.View; + this.Context.Controller.ViewData.Model = model; + + var ctx = new ViewContext( + this.Context, + view, + this.Context.Controller.ViewData, + this.Context.Controller.TempData, + writer); + view.Render(ctx, writer); + } + + /// + /// Internal method that handles rendering of either partial or full views. + /// + /// + /// The path to the view to render. Either in same controller, shared by + /// name, or as fully qualified ~/ path including extension. + /// + /// Model to render the view with. + /// Determines whether to render a full or partial view. + /// String of the rendered view. + private string RenderViewToStringInternal(string viewPath, object model, bool partial = false) + { + // First find the ViewEngine for this view. + ViewEngineResult viewEngineResult = null; + if (partial) + { + viewEngineResult = ViewEngines.Engines.FindPartialView(this.Context, viewPath); + } + else + { + viewEngineResult = ViewEngines.Engines.FindView(this.Context, viewPath, null); + } + + if (viewEngineResult == null || viewEngineResult.View == null) + { + throw new FileNotFoundException("ViewCouldNotBeFound"); + } + + // Get the view and attach the model to view data. + var view = viewEngineResult.View; + this.Context.Controller.ViewData.Model = model; + + string result; + + using (var sw = new StringWriter()) + { + var ctx = new ViewContext( + this.Context, + view, + this.Context.Controller.ViewData, + this.Context.Controller.TempData, + sw); + view.Render(ctx, sw); + result = sw.ToString(); + } + + return result; + } + + /// + /// Internal method that handles rendering of an delegate to a string. + /// + /// A function that takes an and returns or . + /// Model to attach to the view data. + /// String representation of the rendered HTML helper output. + private string RenderHtmlHelperToStringInternal(Func htmlHelperFunc, object model = null) + { + // Set the model to view data. + this.Context.Controller.ViewData.Model = model; + + string result; + + using (var sw = new StringWriter()) + { + // Create a view data dictionary for the HtmlHelper. + var viewDataContainer = new ViewDataContainer(this.Context.Controller.ViewData); + + // Create the HtmlHelper with the proper context. + var htmlHelper = new HtmlHelper( + new ViewContext(this.Context, new FakeView(), this.Context.Controller.ViewData, this.Context.Controller.TempData, sw), + viewDataContainer); + + // Execute the HtmlHelper function and capture the result. + var htmlResult = htmlHelperFunc(htmlHelper); + result = htmlResult?.ToString() ?? string.Empty; + } + + return result; + } + + /// + /// Creates an instance of an MVC controller from scratch + /// when no existing is present. + /// + /// Type of the controller to create. + /// Optional route data used to initialize the controller context. + /// Optional constructor parameters for the controller. + /// Controller for . + /// Thrown if is not available. + public static T CreateController(RouteData routeData = null, params object[] parameters) + where T : Controller, new() + { + // create a disconnected controller instance + T controller = (T)Activator.CreateInstance(typeof(T), parameters); + + // get context wrapper from HttpContext if available + HttpContextBase wrapper = null; + if (HttpContext.Current != null) + { + wrapper = new HttpContextWrapper(HttpContext.Current); + } + else + { + throw new InvalidOperationException( + "Can't create Controller Context if no active HttpContext instance is available."); + } + + if (routeData == null) + { + routeData = new RouteData(); + } + + // add the controller routing if not existing + if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller")) + { + routeData.Values.Add( + "controller", + controller.GetType().Name + .ToLower() + .Replace("controller", string.Empty)); + } + + controller.ControllerContext = new ControllerContext(wrapper, routeData, controller); + return controller; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/ViewDataContainer.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/ViewDataContainer.cs new file mode 100644 index 00000000000..92fecdac376 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/ViewDataContainer.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcPipeline.Utils +{ + using System.Web.Mvc; + + /// + /// Simple implementation used for HtmlHelper contexts in . + /// + internal class ViewDataContainer : IViewDataContainer + { + /// + public ViewDataDictionary ViewData { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The view data dictionary. + public ViewDataContainer(ViewDataDictionary viewData) + { + this.ViewData = viewData; + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/dnn.json b/DNN Platform/DotNetNuke.Web.MvcPipeline/dnn.json new file mode 100644 index 00000000000..c6e3d864a5a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/dnn.json @@ -0,0 +1,16 @@ +{ + "projectType": "library", + "name": "Dnn_DotNetNukeWebMvcPipeline", + "friendlyName": "Dnn DotNetNukeWebMvcPipeline", + "description": "Dnn DotNetNukeWebMvcPipeline Library", + "packageName": "Dnn_DotNetNukeWebMvcPipeline", + "folder": "Dnn/DotNetNukeWebMvcPipeline", + "library": {}, + "pathsAndFiles": { + "pathToAssemblies": "./bin", + "pathToScripts": "./Server/SqlScripts", + "assemblies": ["DotNetNuke.Web.MvcPipeline.dll"], + "releaseFiles": [], + "zipName": "Dnn.DotNetNukeWebMvcPipeline" + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/razor-module-development.md b/DNN Platform/DotNetNuke.Web.MvcPipeline/razor-module-development.md new file mode 100644 index 00000000000..7770be304a8 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/razor-module-development.md @@ -0,0 +1,494 @@ +# Razor Module Development Guide + +## Overview + +This guide explains how to create DNN modules using `RazorModuleControlBase`, which provides a modern MVC-based approach to module development using Razor views. This pattern follows the ViewComponent pattern from .NET Core, making it easier to transition to .NET Core in the future. + +## Table of Contents + +1. [Introduction](#introduction) +2. [Architecture Overview](#architecture-overview) +3. [Creating a Module Control](#creating-a-module-control) +4. [Implementing the Invoke Method](#implementing-the-invoke-method) +5. [Creating Models](#creating-models) +6. [Creating Razor Views](#creating-razor-views) +7. [Module Configuration](#module-configuration) +8. [Available Properties and Methods](#available-properties-and-methods) +9. [Return Types](#return-types) +10. [Using Razor Modules in WebForms with WrapperModule](#using-razor-modules-in-webforms-with-wrappermodule) +11. [Best Practices](#best-practices) + +## Introduction + +`RazorModuleControlBase` is an abstract base class that enables modules to render content using Razor views with MVC 5. It's recommended for migrating WebForms controls and follows a pattern similar to .NET Core ViewComponents. + +**Key Benefits:** + +- Modern MVC-based rendering +- Separation of concerns (Model-View-Control) +- Easy migration path to .NET Core +- Full access to DNN module context and services +- Dependency injection support + +## Architecture Overview + +The `RazorModuleControlBase` class hierarchy: + +``` +DefaultMvcModuleControlBase (abstract) + └── RazorModuleControlBase (abstract) + └── YourModuleControl (concrete) +``` + +**Key Components:** + +- **RazorModuleControlBase**: Abstract base class providing Razor view rendering +- **IRazorModuleResult**: Interface for different result types (View, Content, Error) +- **ViewRazorModuleResult**: Renders Razor views with models +- **ContentRazorModuleResult**: Renders plain HTML content +- **ErrorRazorModuleResult**: Renders error messages + +## Creating a Module Control + +### Step 1: Create the Control Class + +Create a class that inherits from `RazorModuleControlBase`: + +```csharp +using DotNetNuke.Web.MvcPipeline.ModuleControl; +using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + +namespace YourNamespace.Controls +{ + public class YourModuleControl : RazorModuleControlBase + { + // Constructor with dependency injection + public YourModuleControl() + { + // Initialize dependencies + } + + // Required: Implement the Invoke method + public override IRazorModuleResult Invoke() + { + // Your logic here + return View(); + } + } +} +``` + +### Step 2: Implement the Invoke Method + +The `Invoke()` method is where your module's logic resides. +It must return an `IRazorModuleResult`: + +```csharp +public override IRazorModuleResult Invoke() +{ + // 1. Get data from services/controllers + var data = GetYourData(); + + // 2. Create a model + var model = new YourModuleModel + { + Property = data + }; + + // 3. Return a view with the model + return View(model); +} +``` + +### Using Default View Name + +If you don't specify a view name, the system uses a default path: + +``` +~/DesktopModules/YourModule/Views/YourModuleControl.cshtml +``` + +```csharp +public override IRazorModuleResult Invoke() +{ + return View(); // Uses default view path +} +``` + +### Specifying Custom View Path + +```csharp +public override IRazorModuleResult Invoke() +{ + var model = GetModel(); + return View("~/DesktopModules/YourModule/Views/CustomView.cshtml", model); +} +``` + +### Returning Plain Content + +```csharp +public override IRazorModuleResult Invoke() +{ + return Content("
Hello World
"); +} +``` + +### Error Handling + +```csharp +public override IRazorModuleResult Invoke() +{ + try + { + var model = GetModel(); + return View(model); + } + catch (Exception ex) + { + return Error("Error Heading", ex.Message); + } +} +``` + +## Creating Models + +Create simple POCO classes to pass data to your views: + +```csharp +namespace YourNamespace.Models +{ + public class YourModuleModel + { + public string Title { get; set; } + public string Content { get; set; } + public List Items { get; set; } + } +} +``` + +## Creating Razor Views + +Create Razor view files (`.cshtml`) in your module's `Views` folder. + +### View Location Convention + +By default, views are located at: + +``` +~/DesktopModules/YourModule/Views/YourModuleControl.cshtml +``` + +### Basic View Example + +```razor +@model YourNamespace.Models.YourModuleModel + +
+

@Model.Title

+
@Html.Raw(Model.Content)
+
+``` + +### View with ViewData + +The base class automatically provides these ViewData entries: + +- `ModuleContext`: The module instance context +- `ModuleId`: The module ID +- `LocalResourceFile`: Path to localization resources + +```razor +@model YourNamespace.Models.YourModuleModel +@using DotNetNuke.Entities.Modules + +
+

Module ID: @ViewData["ModuleId"]

+

Content: @Model.Content

+
+``` + +## Module Configuration + +Configure your module in the `.dnn` manifest file to use the Razor module control. + +### Module Control Configuration + +In your `dnn_YourModule.dnn` file, add the `mvcControlClass` attribute to your module control: + +```xml + + + DesktopModules/YourModule/YourModule.ascx + YourNamespace.Controls.YourModuleControl, YourAssembly + False + + View + 0 + +``` + +### Multiple Controls + +You can define multiple controls for different actions (View, Edit, Settings): + +```xml + + + + + DesktopModules/YourModule/YourModule.ascx + YourNamespace.Controls.YourModuleControl, YourAssembly + View + + + + + Edit + DesktopModules/YourModule/Edit.ascx + YourNamespace.Controls.EditControl, YourAssembly + Edit + + +``` + +## Available Properties and Methods + +### Properties from DefaultMvcModuleControlBase + +- `ModuleConfiguration`: Module configuration information +- `TabId`: Current tab/page ID +- `ModuleId`: Current module instance ID +- `TabModuleId`: Tab module ID +- `PortalId`: Portal ID +- `PortalSettings`: Portal settings object +- `UserInfo`: Current user information +- `UserId`: Current user ID +- `Settings`: Module settings hashtable +- `ControlPath`: Path to the control directory +- `ControlName`: Name of the control +- `LocalResourceFile`: Path to localization resource file +- `DependencyProvider`: Service provider for dependency injection + +### Properties from RazorModuleControlBase + +- `ViewContext`: Razor view context +- `HttpContext`: HTTP context +- `Request`: HTTP request object +- `ViewData`: View data dictionary + +### Methods + +- `View()`: Returns a view result (multiple overloads) +- `Content(string)`: Returns plain HTML content +- `Error(string, string)`: Returns an error result +- `EditUrl()`: Generate edit URLs (from base class) + +## Return Types + +### View Result + +Renders a Razor view with an optional model: + +```csharp +// Default view +return View(); + +// View with model +return View(model); + +// Specific view with model +return View("ViewName", model); +``` + +### Content Result + +Returns plain HTML content (HTML encoded): + +```csharp +return Content("
Hello
"); +``` + +### Error Result + +Displays an error message: + +```csharp +return Error("Error Heading", "Error message details"); +``` + +## Using Razor Modules in WebForms with WrapperModule + +The `WrapperModule` class allows you to use Razor module controls within the traditional WebForms pipeline. This is useful when you need to display Razor-based modules on pages that use the WebForms pipeline (`/default.aspx`). + +### How It Works + +The `WrapperModule` acts as a bridge between WebForms and the MVC pipeline. It: +- Inherits from `PortalModuleBase` (WebForms compatible) +- Creates and renders your Razor module control +- Handles module actions and page contributors automatically +- Displays the rendered HTML within the WebForms page + +### Module Configuration + +To use a Razor module control in WebForms, configure your module control in the `.dnn` manifest file as follows: + +```xml + + + DotNetNuke.Web.MvcPipeline.ModuleControl.WebForms.WrapperModule, DotNetNuke.Web.MvcPipeline + YourNamespace.Controls.YourModuleControl, YourAssembly + False + + View + 0 + +``` + +### Important Limitations + +**⚠️ Form Tags Restriction**: Razor modules used with `WrapperModule` **cannot contain `
` tags**. This is because WebForms pages already have a form tag, and nested forms are not allowed in HTML. + +**Workarounds:** +- Use AJAX for form submissions instead of traditional form posts +- Place forms in separate pages/controls that use the MVC pipeline +- Use JavaScript to submit data without form tags + +## Best Practices + +### 1. Dependency Injection + +Use constructor injection for dependencies: + +```csharp +public class YourModuleControl : RazorModuleControlBase +{ + private readonly IYourService yourService; + + public YourModuleControl(IYourService yourService) + { + this.yourService = yourService; + } +} +``` + +Or use the `DependencyProvider`: + +```csharp +var service = this.DependencyProvider.GetRequiredService(); +``` + +### 2. Error Handling + +Always handle errors gracefully: + +```csharp +public override IRazorModuleResult Invoke() +{ + try + { + var model = GetModel(); + return View(model); + } + catch (Exception ex) + { + // Log error + Exceptions.LogException(ex); + return Error("Error", "An error occurred while loading the module."); + } +} +``` + +### 3. Localization + +Use the `LocalResourceFile` property for localized strings: + +```csharp +var localizedText = Localization.GetString("Key", this.LocalResourceFile); +``` + +### 4. Module Actions + +Implement `IActionable` interface for module actions: + +```csharp +public class YourModuleControl : RazorModuleControlBase, IActionable +{ + public ModuleActionCollection ModuleActions + { + get + { + var actions = new ModuleActionCollection(); + actions.Add( + this.GetNextActionID(), + "Edit", + ModuleActionType.EditContent, + string.Empty, + string.Empty, + this.EditUrl(), + false, + SecurityAccessLevel.Edit, + true, + false); + return actions; + } + } +} +``` + +### 5. Resource Management & Page settings + +Implement `IPageContributor` interface to register CSS and JavaScript files, request AJAX support, and configure page settings: + +```csharp +using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + +public class YourModuleControl : RazorModuleControlBase, IPageContributor +{ + public void ConfigurePage(PageConfigurationContext context) + { + // Request AJAX support (required for AJAX calls and form submissions) + context.ServicesFramework.RequestAjaxAntiForgerySupport(); + context.ServicesFramework.RequestAjaxScriptSupport(); + + // Register CSS stylesheets + context.ClientResourceController + .CreateStylesheet("~/DesktopModules/YourModule/styles.css") + .Register(); + + // Register JavaScript files + context.ClientResourceController + .CreateScript("~/DesktopModules/YourModule/js/edit.js") + .Register(); + + // Set page title + context.PageService.SetTitle("Your Module - Edit"); + } +} +``` + +**PageConfigurationContext** provides access to: +- **`ClientResourceController`**: Register CSS and JavaScript resources +- **`ServicesFramework`**: Request AJAX support (`RequestAjaxAntiForgerySupport()`, `RequestAjaxScriptSupport()`) +- **`PageService`**: Set page titles and other page-level settings +- **`JavaScriptLibraryHelper`**: Manage JavaScript libraries + +### 6. View Organization + +- Keep views simple and focused +- Use partial views for reusable components +- Follow the default naming convention when possible +- Place views in the `Views` folder under your module directory + +### 7. Model Design + +- Keep models simple (POCOs) +- Don't include business logic in models +- Use models to pass data from control to view + +## Additional Resources + +- [RazorModuleControlBase Source](DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/RazorModuleControlBase.cs) +- [HTML Module Example](DNN Platform/Modules/HTML/Controls/HtmlModuleControl.cs) +- [MVC Module Control README](DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/README.md) + diff --git a/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/DefaultController.cs b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/DefaultController.cs new file mode 100644 index 00000000000..224d74615a1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/DefaultController.cs @@ -0,0 +1,262 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcWebsite.Controllers +{ + using System; + using System.Collections.Generic; + using System.Text.RegularExpressions; + using System.Web; + using System.Web.Mvc; + + using Dnn.EditBar.UI.Mvc; + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.Application; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Pages; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Host; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Tabs; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Services.Exceptions; + using DotNetNuke.Services.Installer.Blocker; + using DotNetNuke.Services.Localization; + + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.Exceptions; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.ModelFactories; + using DotNetNuke.Web.MvcPipeline.Models; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + + /// + /// Default controller for handling page rendering in the MVC pipeline. + /// + public class DefaultController : DnnPageController + { + private readonly INavigationManager navigationManager; + private readonly IPageModelFactory pageModelFactory; + private readonly IClientResourceController clientResourceController; + private readonly IPageService pageService; + private readonly IHostSettings hostSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The navigation manager for URL generation. + /// The factory for creating page models. + /// The controller for managing client resources (scripts and stylesheets). + /// The service for page-related operations. + /// The service provider for dependency resolution. + /// The host settings configuration. + public DefaultController( + INavigationManager navigationManager, + IPageModelFactory pageModelFactory, + IClientResourceController clientResourceController, + IPageService pageService, + IServiceProvider serviceProvider, + IHostSettings hostSettings) + :base(serviceProvider) + { + this.navigationManager = navigationManager; + this.pageModelFactory = pageModelFactory; + this.clientResourceController = clientResourceController; + this.pageService = pageService; + this.hostSettings = hostSettings; + } + + /// + /// Renders a page for the specified tab and language. + /// + /// The tab (page) identifier to render. + /// The language code for localization. + /// + /// A view result containing the rendered page, or an HTTP status code result if an error occurs. + /// Returns 403 (Forbidden) for access denied, 404 (Not Found) for missing pages, or redirects as needed. + /// + public ActionResult Page(int tabid, string language) + { + // TODO: CSP - enable when CSP implementation is ready + /* + this.HttpContext.Items.Add("CSP-NONCE", this.contentSecurityPolicy.Nonce); + + this.contentSecurityPolicy.DefaultSource.AddSelf(); + this.contentSecurityPolicy.ImgSource.AddSelf(); + this.contentSecurityPolicy.FontSource.AddSelf(); + this.contentSecurityPolicy.StyleSource.AddSelf(); + this.contentSecurityPolicy.FrameSource.AddSelf(); + this.contentSecurityPolicy.FormAction.AddSelf(); + this.contentSecurityPolicy.FrameAncestors.AddSelf(); + this.contentSecurityPolicy.ObjectSource.AddNone(); + this.contentSecurityPolicy.BaseUriSource.AddNone(); + this.contentSecurityPolicy.ScriptSource.AddNonce(this.contentSecurityPolicy.Nonce); + this.contentSecurityPolicy.AddReportTo("csp-endpoint"); + this.contentSecurityPolicy.AddReportEndpoint("csp-endpoint", this.Request.Url.Scheme + "://" + this.Request.Url.Host + "/DesktopModules/Csp/Report"); + + if (this.Request.IsAuthenticated) + { + this.contentSecurityPolicy.FrameSource.AddHost("https://dnndocs.com").AddHost("https://docs.dnncommunity.org"); + } + */ + + // There could be a pending installation/upgrade process + if (InstallBlocker.Instance.IsInstallInProgress()) + { + Exceptions.ProcessHttpException(new HttpException(503, Localization.GetString("SiteAccessedWhileInstallationWasInProgress.Error", Localization.GlobalResourceFile))); + } + + var user = this.PortalSettings.UserInfo; + + if (PortalSettings.Current.UserId > 0) + { + // TODO: should we do this? It creates a dependency towards the PersonaBar which is probably not a great idea + MvcContentEditorManager.CreateManager(this); + } + + // Configure the ActiveTab with Skin/Container information + PortalSettingsController.Instance().ConfigureActiveTab(this.PortalSettings); + + try + { + PageModel model = this.pageModelFactory.CreatePageModel(this); + this.clientResourceController.RegisterPathNameAlias("SkinPath", this.PortalSettings.ActiveTab.SkinPath); + model.ClientResourceController = this.clientResourceController; + model.PageService = this.pageService; + this.InitializePage(model); + + // DotNetNuke.Framework.JavaScriptLibraries.MvcJavaScript.Register(this.ControllerContext); + model.ClientVariables = MvcClientAPI.GetClientVariableList(); + model.StartupScripts = MvcClientAPI.GetClientStartupScriptList(); + + // Register the scripts and stylesheets + this.RegisterScriptsAndStylesheets(model); + + return this.View(model.Skin.RazorFile, "Layout", model); + } + catch (AccesDeniedException) + { + return new HttpStatusCodeResult(403, "Access Denied"); + } + catch (MvcPageException ex) + { + if (string.IsNullOrEmpty(ex.RedirectUrl)) + { + return this.HttpNotFound(ex.Message); + } + else + { + return this.Redirect(ex.RedirectUrl); + } + } + } + + /// + /// Registers all scripts and stylesheets required for the page rendering. + /// + /// The page model containing skin, container, and resource information. + private void RegisterScriptsAndStylesheets(PageModel page) + { + foreach (var styleSheet in page.Skin.RegisteredStylesheets) + { + this.clientResourceController.CreateStylesheet(styleSheet.Stylesheet) + .SetPriority((int)styleSheet.FileOrder) + .Register(); + } + + foreach (var pane in page.Skin.Panes) + { + foreach (var container in pane.Value.Containers) + { + foreach (var stylesheet in container.Value.RegisteredStylesheets) + { + this.clientResourceController.CreateStylesheet(stylesheet.Stylesheet) + .SetPriority((int)stylesheet.FileOrder) + .Register(); + } + } + } + + foreach (var script in page.Skin.RegisteredScripts) + { + this.clientResourceController.CreateScript(script.Script) + .SetPriority((int)script.FileOrder) + .Register(); + } + } + + /// + /// Initializes the page by handling tab name redirects, setting cache control headers, and configuring cookie consent. + /// + /// The page model to initialize. + private void InitializePage(PageModel page) + { + // redirect to a specific tab based on name + if (!string.IsNullOrEmpty(this.Request.QueryString["tabname"])) + { + var tab = TabController.Instance.GetTabByName(this.Request.QueryString["TabName"], this.PortalSettings.PortalId); + if (tab != null) + { + var parameters = new List(); // maximum number of elements + for (var intParam = 0; intParam <= this.Request.QueryString.Count - 1; intParam++) + { + switch (this.Request.QueryString.Keys[intParam].ToLowerInvariant()) + { + case "tabid": + case "tabname": + break; + default: + parameters.Add( + this.Request.QueryString.Keys[intParam] + "=" + this.Request.QueryString[intParam]); + break; + } + } + + throw new MvcPageException("redirect to a specific tab based on name", this.navigationManager.NavigateURL(tab.TabID, Null.NullString, parameters.ToArray())); + } + else + { + // 404 Error - Redirect to ErrorPage + throw new NotFoundException("redirect to a specific tab based on name - tab not found"); + } + } + + var cacheability = this.Request.IsAuthenticated ? this.hostSettings.AuthenticatedCacheability : this.hostSettings.UnauthenticatedCacheability; + + switch (cacheability) + { + case CacheControlHeader.NoCache: + this.Response.Cache.SetCacheability(HttpCacheability.NoCache); + break; + case CacheControlHeader.Private: + this.Response.Cache.SetCacheability(HttpCacheability.Private); + break; + case CacheControlHeader.Public: + this.Response.Cache.SetCacheability(HttpCacheability.Public); + break; + case CacheControlHeader.ServerAndNoCache: + this.Response.Cache.SetCacheability(HttpCacheability.ServerAndNoCache); + break; + case CacheControlHeader.ServerAndPrivate: + this.Response.Cache.SetCacheability(HttpCacheability.ServerAndPrivate); + break; + } + + // Cookie Consent + if (this.PortalSettings.ShowCookieConsent) + { + MvcJavaScript.RegisterClientReference(DotNetNuke.UI.Utilities.ClientAPI.ClientNamespaceReferences.dnn); + MvcClientAPI.RegisterClientVariable("cc_morelink", this.PortalSettings.CookieMoreLink, true); + MvcClientAPI.RegisterClientVariable("cc_message", Localization.GetString("cc_message", Localization.GlobalResourceFile), true); + MvcClientAPI.RegisterClientVariable("cc_dismiss", Localization.GetString("cc_dismiss", Localization.GlobalResourceFile), true); + MvcClientAPI.RegisterClientVariable("cc_link", Localization.GetString("cc_link", Localization.GlobalResourceFile), true); + this.clientResourceController.RegisterScript("~/Resources/Shared/Components/CookieConsent/cookieconsent.min.js", FileOrder.Js.DnnControls); + this.clientResourceController.RegisterStylesheet("~/Resources/Shared/Components/CookieConsent/cookieconsent.min.cssdisa", FileOrder.Css.ResourceCss); + this.clientResourceController.RegisterScript("~/js/dnn.cookieconsent.js"); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/ModuleActionsController.cs b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/ModuleActionsController.cs new file mode 100644 index 00000000000..04b861b9212 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/ModuleActionsController.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcWebsite.Controllers +{ + using System.Web.Mvc; + + using DotNetNuke.Abstractions.Logging; + using DotNetNuke.Abstractions.Portals; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcWebsite.Models; + + /// + /// Handles module-related actions for the MVC website. + /// + public class ModuleActionsController : Controller + { + private readonly IPortalSettings portalSettings; + private readonly IPortalController portalController; + private readonly IModuleController moduleController; + private readonly IUserController userController; + private readonly IEventLogger eventLogger; + + /// + /// Initializes a new instance of the class. + /// + /// The portal controller. + /// The module controller. + /// The user controller. + /// The event logger. + public ModuleActionsController( + IPortalController portalController, + IModuleController moduleController, + IUserController userController, + IEventLogger eventLogger) + { + this.portalController = portalController; + this.portalSettings = this.portalController.GetCurrentSettings(); + this.moduleController = moduleController; + this.userController = userController; + this.eventLogger = eventLogger; + } + + /// + /// Deletes a module instance from the specified tab and logs the operation. + /// + /// The delete request model containing module and tab identifiers. + /// An empty result when the operation completes, or if the module does not exist. + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult Delete(ModuleActionsDeleteModel model) + { + var module = this.moduleController.GetModule(model.ModuleId, model.TabId, false); + if (module == null) + { + return this.HttpNotFound(); + } + + var user = this.userController.GetCurrentUserInfo(); + if (!module.IsShared) + { + foreach (var instance in this.moduleController.GetTabModulesByModule(module.ModuleID)) + { + if (instance.IsShared) + { + // HARD Delete Shared Instance + this.moduleController.DeleteTabModule(instance.TabID, instance.ModuleID, false); + this.eventLogger.AddLog(instance, this.portalSettings, user.UserID, string.Empty, EventLogType.MODULE_DELETED); + } + } + } + + this.moduleController.DeleteTabModule(model.TabId, model.ModuleId, true); + this.eventLogger.AddLog(module, this.portalSettings, user.UserID, string.Empty, EventLogType.MODULE_SENT_TO_RECYCLE_BIN); + return new EmptyResult(); + } + + /// + /// Retrieves a localized string from the global resource file. + /// + /// The resource key to localize. + /// The localized string for the specified key. + protected string LocalizeString(string key) + { + return Localization.GetString(key, Localization.GlobalResourceFile); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/PrivacyControl.cs b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/PrivacyControl.cs new file mode 100644 index 00000000000..81ef89b4ae5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/PrivacyControl.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcWebsite.Controls +{ + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + + /// + /// Control that displays the portal's privacy policy message. + /// + public class PrivacyControl : RazorModuleControlBase + { + /// + /// Gets the name of the control. + /// + public override string ControlName => "Privacy"; + + /// + /// Gets the path where the control is located. + /// + public override string ControlPath => "admin/Portal"; + + /// + /// Invokes the control and returns the privacy policy message view. + /// + /// A razor module result containing the localized privacy policy message. + public override IRazorModuleResult Invoke() + { + return this.View(Localization.GetSystemMessage(this.PortalSettings, "MESSAGE_PORTAL_PRIVACY")); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/TermsControl.cs b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/TermsControl.cs new file mode 100644 index 00000000000..384d33e2cf6 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/TermsControl.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcWebsite.Controls +{ + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + + /// + /// Control that displays the portal's terms and conditions message. + /// + public class TermsControl : RazorModuleControlBase + { + /// + /// Gets the name of the control. + /// + public override string ControlName => "Terms"; + + /// + /// Gets the path where the control is located. + /// + public override string ControlPath => "admin/Portal"; + + /// + /// Invokes the control and returns the terms and conditions message view. + /// + /// A razor module result containing the localized terms and conditions message. + public override IRazorModuleResult Invoke() + { + return this.View(Localization.GetSystemMessage(this.PortalSettings, "MESSAGE_PORTAL_TERMS")); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcWebsite/DotNetNuke.Web.MvcWebsite.csproj b/DNN Platform/DotNetNuke.Web.MvcWebsite/DotNetNuke.Web.MvcWebsite.csproj new file mode 100644 index 00000000000..f81ed6a6be3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/DotNetNuke.Web.MvcWebsite.csproj @@ -0,0 +1,68 @@ + + + DotNetNuke.Web.MvcWebsite + net48 + true + latest + bin + false + false + Sacha Trauwaen + Dnn + MvcWebsite + 2025 + MvcWebsite + 0.0.1.0 + 0.0.1.0 + DNN MVC-Website project + en-US + + Library + + Portable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + stylecop.json + + + + + + + + + + + + + + + diff --git a/DNN Platform/DotNetNuke.Web.MvcWebsite/Models/ModuleActionsDeleteModel.cs b/DNN Platform/DotNetNuke.Web.MvcWebsite/Models/ModuleActionsDeleteModel.cs new file mode 100644 index 00000000000..96e455c1ff1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Models/ModuleActionsDeleteModel.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Web.MvcWebsite.Models +{ + /// + /// Represents the model for module deletion requests. + /// + public class ModuleActionsDeleteModel + { + /// + /// Gets or sets the module identifier to delete. + /// + public int ModuleId { get; set; } + + /// + /// Gets or sets the tab (page) identifier where the module is located. + /// + public int TabId { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcWebsite/dnn.json b/DNN Platform/DotNetNuke.Web.MvcWebsite/dnn.json new file mode 100644 index 00000000000..19ed13eca26 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/dnn.json @@ -0,0 +1,16 @@ +{ + "projectType": "library", + "name": "Dnn_DotNetNukeWebMvcWebsite", + "friendlyName": "Dnn DotNetNukeWebMvcWebsite", + "description": "Dnn DotNetNukeWebMvcWebsite Library", + "packageName": "Dnn_DotNetNukeWebMvcWebsite", + "folder": "Dnn/DotNetNukeWebMvcWebsite", + "library": {}, + "pathsAndFiles": { + "pathToAssemblies": "./bin", + "pathToScripts": "./Server/SqlScripts", + "assemblies": ["DotNetNuke.Web.MvcWebsite.dll"], + "releaseFiles": [], + "zipName": "Dnn.DotNetNukeWebMvcWebsite" + } +} diff --git a/DNN Platform/Library/Data/DataProvider.cs b/DNN Platform/Library/Data/DataProvider.cs index caa2a54970a..8c58c8f7808 100644 --- a/DNN Platform/Library/Data/DataProvider.cs +++ b/DNN Platform/Library/Data/DataProvider.cs @@ -1308,7 +1308,13 @@ public virtual void UpdateModuleDefinition(int moduleDefId, string friendlyName, lastModifiedByUserID); } + [Obsolete("Deprecated in DotNetNuke 10.99.0. Scheduled removal in v12.0.0.")] public virtual int AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string iconFile, int controlType, int viewOrder, string helpUrl, bool supportsPartialRendering, bool supportsPopUps, int createdByUserID) + { + return this.AddModuleControl(moduleDefId, controlKey, controlTitle, controlSrc, null, iconFile, controlType, viewOrder, helpUrl, supportsPartialRendering, supportsPopUps, createdByUserID); + } + + public virtual int AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string mvcControlClass, string iconFile, int controlType, int viewOrder, string helpUrl, bool supportsPartialRendering, bool supportsPopUps, int createdByUserID) { return this.ExecuteScalar( "AddModuleControl", @@ -1316,6 +1322,7 @@ public virtual int AddModuleControl(int moduleDefId, string controlKey, string c this.GetNull(controlKey), this.GetNull(controlTitle), controlSrc, + this.GetNull(mvcControlClass), this.GetNull(iconFile), controlType, this.GetNull(viewOrder), @@ -1335,7 +1342,7 @@ public virtual IDataReader GetModuleControls() return this.ExecuteReader("GetModuleControls"); } - public virtual void UpdateModuleControl(int moduleControlId, int moduleDefId, string controlKey, string controlTitle, string controlSrc, string iconFile, int controlType, int viewOrder, string helpUrl, bool supportsPartialRendering, bool supportsPopUps, int lastModifiedByUserID) + public virtual void UpdateModuleControl(int moduleControlId, int moduleDefId, string controlKey, string controlTitle, string controlSrc, string mvcControlClass, string iconFile, int controlType, int viewOrder, string helpUrl, bool supportsPartialRendering, bool supportsPopUps, int lastModifiedByUserID) { this.ExecuteNonQuery( "UpdateModuleControl", @@ -1344,6 +1351,7 @@ public virtual void UpdateModuleControl(int moduleControlId, int moduleDefId, st this.GetNull(controlKey), this.GetNull(controlTitle), controlSrc, + this.GetNull(mvcControlClass), this.GetNull(iconFile), controlType, this.GetNull(viewOrder), diff --git a/DNN Platform/Library/DotNetNuke.Library.csproj b/DNN Platform/Library/DotNetNuke.Library.csproj index d56fede6e5f..98a55427b7d 100644 --- a/DNN Platform/Library/DotNetNuke.Library.csproj +++ b/DNN Platform/Library/DotNetNuke.Library.csproj @@ -91,7 +91,7 @@ - + diff --git a/DNN Platform/Library/Entities/Modules/ControlInfo.cs b/DNN Platform/Library/Entities/Modules/ControlInfo.cs index 6f6e3d17c25..71bbee74270 100644 --- a/DNN Platform/Library/Entities/Modules/ControlInfo.cs +++ b/DNN Platform/Library/Entities/Modules/ControlInfo.cs @@ -25,7 +25,11 @@ protected ControlInfo() /// Gets or sets the Control Source. /// A String. - public string ControlSrc { get; set; } + public string ControlSrc { get; set; } + + /// Gets or sets the Mvc Control Class. + /// A String. + public string MvcControlClass { get; set; } /// /// Gets or sets a value indicating whether the control support the AJAX @@ -41,7 +45,8 @@ protected override void FillInternal(IDataReader dr) // Call EntityBaseInfo's implementation base.FillInternal(dr); this.ControlKey = Null.SetNullString(dr["ControlKey"]); - this.ControlSrc = Null.SetNullString(dr["ControlSrc"]); + this.ControlSrc = Null.SetNullString(dr["ControlSrc"]); + this.MvcControlClass = Null.SetNullString(dr["MvcControlClass"]); this.SupportsPartialRendering = Null.SetNullBoolean(dr["SupportsPartialRendering"]); } @@ -54,6 +59,9 @@ protected void ReadXmlInternal(XmlReader reader) break; case "controlSrc": this.ControlSrc = reader.ReadElementContentAsString(); + break; + case "mvcControlClass": + this.MvcControlClass = reader.ReadElementContentAsString(); break; case "supportsPartialRendering": string elementvalue = reader.ReadElementContentAsString(); @@ -70,7 +78,8 @@ protected void WriteXmlInternal(XmlWriter writer) { // write out properties writer.WriteElementString("controlKey", this.ControlKey); - writer.WriteElementString("controlSrc", this.ControlSrc); + writer.WriteElementString("controlSrc", this.ControlSrc); + writer.WriteElementString("mvcControlClass", this.MvcControlClass); writer.WriteElementString("supportsPartialRendering", this.SupportsPartialRendering.ToString()); } } diff --git a/DNN Platform/Library/Entities/Modules/ModuleControlController.cs b/DNN Platform/Library/Entities/Modules/ModuleControlController.cs index 89c0488cda9..ece6a4181fb 100644 --- a/DNN Platform/Library/Entities/Modules/ModuleControlController.cs +++ b/DNN Platform/Library/Entities/Modules/ModuleControlController.cs @@ -80,6 +80,7 @@ public static int SaveModuleControl(ModuleControlInfo moduleControl, bool clearC moduleControl.ControlKey, moduleControl.ControlTitle, moduleControl.ControlSrc, + moduleControl.MvcControlClass, moduleControl.IconFile, Convert.ToInt32(moduleControl.ControlType), moduleControl.ViewOrder, @@ -97,6 +98,7 @@ public static int SaveModuleControl(ModuleControlInfo moduleControl, bool clearC moduleControl.ControlKey, moduleControl.ControlTitle, moduleControl.ControlSrc, + moduleControl.MvcControlClass, moduleControl.IconFile, Convert.ToInt32(moduleControl.ControlType), moduleControl.ViewOrder, diff --git a/DNN Platform/Library/Entities/Modules/ModuleInfo.cs b/DNN Platform/Library/Entities/Modules/ModuleInfo.cs index 6e3187179ad..e2d3827e49c 100644 --- a/DNN Platform/Library/Entities/Modules/ModuleInfo.cs +++ b/DNN Platform/Library/Entities/Modules/ModuleInfo.cs @@ -837,6 +837,11 @@ public string GetProperty(string propertyName, string format, CultureInfo format propertyNotFound = false; result = PropertyAccess.FormatString(this.ModuleControl.ControlSrc, format); break; + case "mvcControlClass": + isPublic = false; + propertyNotFound = false; + result = PropertyAccess.FormatString(this.ModuleControl.MvcControlClass, format); + break; case "controltitle": propertyNotFound = false; result = PropertyAccess.FormatString(this.ModuleControl.ControlTitle, format); diff --git a/DNN Platform/Library/Entities/Portals/IPortalSettingsController.cs b/DNN Platform/Library/Entities/Portals/IPortalSettingsController.cs index 177f3bda2dd..0f6245f813f 100644 --- a/DNN Platform/Library/Entities/Portals/IPortalSettingsController.cs +++ b/DNN Platform/Library/Entities/Portals/IPortalSettingsController.cs @@ -14,6 +14,8 @@ public interface IPortalSettingsController PortalSettings.PortalAliasMapping GetPortalAliasMappingMode(int portalId); + string GetPortalPagePipeline(int portalId); + /// The GetActiveTab method gets the active Tab for the current request. /// The current tab's id. /// The current PortalSettings. diff --git a/DNN Platform/Library/Entities/Portals/PortalSettings.cs b/DNN Platform/Library/Entities/Portals/PortalSettings.cs index 7c53759b28e..0032f4af045 100644 --- a/DNN Platform/Library/Entities/Portals/PortalSettings.cs +++ b/DNN Platform/Library/Entities/Portals/PortalSettings.cs @@ -580,6 +580,9 @@ public bool ShowQuickModuleAddMenu } } + /// + public string PagePipeline { get; internal set; } + /// public string GetProperty(string propertyName, string format, CultureInfo formatProvider, UserInfo accessingUser, Scope accessLevel, ref bool propertyNotFound) { diff --git a/DNN Platform/Library/Entities/Portals/PortalSettingsController.cs b/DNN Platform/Library/Entities/Portals/PortalSettingsController.cs index dd3f1a28f41..647eeaffd2b 100644 --- a/DNN Platform/Library/Entities/Portals/PortalSettingsController.cs +++ b/DNN Platform/Library/Entities/Portals/PortalSettingsController.cs @@ -11,6 +11,7 @@ namespace DotNetNuke.Entities.Portals using System.Linq; using DotNetNuke.Abstractions.Application; + using DotNetNuke.Abstractions.Portals; using DotNetNuke.Collections; using DotNetNuke.Common; using DotNetNuke.Common.Utilities; @@ -123,6 +124,17 @@ public virtual PortalSettings.PortalAliasMapping GetPortalAliasMappingMode(int p return aliasMapping; } + /// + public virtual string GetPortalPagePipeline(int portalId) + { + if (PortalController.Instance.GetPortalSettings(portalId).TryGetValue("PagePipeline", out var setting)) + { + return string.IsNullOrEmpty(setting) ? PagePipelineConstants.WebForms : setting; + } + + return PagePipelineConstants.WebForms; + } + /// public virtual IList GetTabModules(PortalSettings portalSettings) { @@ -278,6 +290,7 @@ public virtual void LoadPortalSettings(PortalSettings portalSettings) portalSettings.DataConsentDelayMeasurement = setting; setting = settings.GetValueOrDefault("AllowedExtensionsWhitelist", this.hostSettingsService.GetString("DefaultEndUserExtensionWhitelist")); portalSettings.AllowedExtensionsWhitelist = new FileExtensionWhitelist(setting); + portalSettings.PagePipeline = settings.GetValueOrDefault("PagePipeline", "webforms"); } [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Breaking change")] diff --git a/DNN Platform/Library/Entities/Tabs/TabInfo.cs b/DNN Platform/Library/Entities/Tabs/TabInfo.cs index ff5be7180cd..538959f8922 100644 --- a/DNN Platform/Library/Entities/Tabs/TabInfo.cs +++ b/DNN Platform/Library/Entities/Tabs/TabInfo.cs @@ -698,6 +698,27 @@ public string SkinDoctype [JsonIgnore] public bool UseBaseFriendlyUrls { get; set; } + /// Gets a value indicating the pipeline type. + [XmlIgnore] + [JsonIgnore] + public string PagePipeline + { + get + { + string pagePipeline; + if (this.TabSettings.ContainsKey("PagePipeline") && !string.IsNullOrEmpty(this.TabSettings["PagePipeline"].ToString())) + { + pagePipeline = this.TabSettings["PagePipeline"].ToString(); + } + else + { + pagePipeline = string.Empty; + } + + return pagePipeline; + } + } + /// public string GetProperty(string propertyName, string format, CultureInfo formatProvider, UserInfo accessingUser, Scope currentScope, ref bool propertyNotFound) { @@ -853,6 +874,10 @@ public string GetProperty(string propertyName, string format, CultureInfo format propertyNotFound = false; result = PropertyAccess.FormatString(this.SiteMapPriority.ToString(), format); break; + case "pagepipeline": + propertyNotFound = false; + result = PropertyAccess.FormatString(this.PagePipeline, format); + break; } if (!isPublic && currentScope != Scope.Debug) diff --git a/DNN Platform/Library/Entities/Urls/AdvancedUrlRewriter.cs b/DNN Platform/Library/Entities/Urls/AdvancedUrlRewriter.cs index 5936f6caa26..13ad65fa190 100644 --- a/DNN Platform/Library/Entities/Urls/AdvancedUrlRewriter.cs +++ b/DNN Platform/Library/Entities/Urls/AdvancedUrlRewriter.cs @@ -2194,6 +2194,44 @@ private static void SecurityCheck(HttpApplication app) } } + private static bool IsMvc(UrlAction result, NameValueCollection queryStringCol, HttpContext context, int tabId, int portalId) + { + var mvcCtls = new[] { "Terms", "Privacy" }; + bool mvcCtl = false; + if (result.RewritePath.Contains("&ctl=")) + { + foreach (var item in mvcCtls) + { + mvcCtl = mvcCtl || result.RewritePath.Contains("&ctl=" + item); + } + } + else + { + TabInfo tab = null; + if (tabId > 0 && portalId > -1) + { + tab = TabController.Instance.GetTab(tabId, portalId, false); + if (tab != null) + { + var tabPipeline = tab.PagePipeline; + if (!string.IsNullOrEmpty(tabPipeline)) + { + mvcCtl = tabPipeline == PagePipelineConstants.Mvc; + } + else + { + var portalPipeline = PortalSettingsController.Instance().GetPortalPagePipeline(portalId); + mvcCtl = portalPipeline == PagePipelineConstants.Mvc; + } + } + } + } + + mvcCtl = mvcCtl && !result.RewritePath.Contains("mvcpage=no") && queryStringCol["mvcpage"] != "no"; + mvcCtl = mvcCtl || result.RewritePath.Contains("mvcpage=yes") || queryStringCol["mvcpage"] == "yes"; + return mvcCtl; + } + private void ProcessRequest( HttpContext context, Uri requestUri, @@ -2445,7 +2483,14 @@ private void ProcessRequest( } else { - RewriterUtils.RewriteUrl(context, "~/" + result.RewritePath); + if (IsMvc(result, queryStringCol, context, result.TabId, result.PortalId)) + { + RewriterUtils.RewriteUrl(context, "~/" + result.RewritePath.Replace(Globals.glbDefaultPage, "DesktopModules/Default/Page/" + result.TabId + "/" + result.CultureCode)); + } + else + { + RewriterUtils.RewriteUrl(context, "~/" + result.RewritePath); + } } } diff --git a/DNN Platform/Library/Properties/AssemblyInfo.cs b/DNN Platform/Library/Properties/AssemblyInfo.cs index a92c00e65a9..2209569a9c4 100644 --- a/DNN Platform/Library/Properties/AssemblyInfo.cs +++ b/DNN Platform/Library/Properties/AssemblyInfo.cs @@ -44,6 +44,7 @@ [assembly: InternalsVisibleTo("DotNetNuke.Modules.Html")] // Once Globals is refactored to Dependency Injection we should be able to remove this [assembly: InternalsVisibleTo("DotNetNuke.Website.Deprecated")] // Once Globals is refactored to Dependency Injection we should be able to remove this [assembly: InternalsVisibleTo("Dnn.PersonaBar.UI")] // Once Globals is refactored to Dependency Injection we should be able to remove this +[assembly: InternalsVisibleTo("Dnn.EditBar.UI")] // Once Globals is refactored to Dependency Injection we should be able to remove this [assembly: InternalsVisibleTo("Dnn.PersonaBar.Library")] // Once Globals is refactored to Dependency Injection we should be able to remove this [assembly: InternalsVisibleTo("DotNetNuke.Modules.Groups")] // Once Globals is refactored to Dependency Injection we should be able to remove this [assembly: InternalsVisibleTo("DotNetNuke.Modules.Journal")] // Once Globals is refactored to Dependency Injection we should be able to remove this diff --git a/DNN Platform/Library/Properties/AssemblyInfoMvc.cs b/DNN Platform/Library/Properties/AssemblyInfoMvc.cs new file mode 100644 index 00000000000..c70ee5b202a --- /dev/null +++ b/DNN Platform/Library/Properties/AssemblyInfoMvc.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +using System.Runtime.CompilerServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: InternalsVisibleTo("DotNetNuke.Web.MvcPipeline")] +[assembly: InternalsVisibleTo("DotNetNuke.Web.MvcWebsite")] +[assembly: InternalsVisibleTo("DotNetNuke.Web.MvcUrlRewriter")] +[assembly: InternalsVisibleTo("DotNetNuke.Web.NewDDRMenu")] // Once Globals is refactored to Dependency Injection we should be able to remove this diff --git a/DNN Platform/Library/Services/Installer/Writers/ModulePackageWriter.cs b/DNN Platform/Library/Services/Installer/Writers/ModulePackageWriter.cs index 67f0ddcee53..c69c347d5bc 100644 --- a/DNN Platform/Library/Services/Installer/Writers/ModulePackageWriter.cs +++ b/DNN Platform/Library/Services/Installer/Writers/ModulePackageWriter.cs @@ -137,7 +137,7 @@ private static void ProcessControls(XPathNavigator controlNav, string moduleFold controlSrc = controlSrc.Replace('\\', '/'); moduleControl.ControlSrc = controlSrc; - + moduleControl.MvcControlClass = Util.ReadElement(controlNav, "mvcControlClass"); moduleControl.IconFile = Util.ReadElement(controlNav, "iconfile"); string controlType = Util.ReadElement(controlNav, "type"); diff --git a/DNN Platform/Library/Services/Tokens/HtmlTokenReplace.cs b/DNN Platform/Library/Services/Tokens/HtmlTokenReplace.cs index af39969e6d6..0a408246fd7 100644 --- a/DNN Platform/Library/Services/Tokens/HtmlTokenReplace.cs +++ b/DNN Platform/Library/Services/Tokens/HtmlTokenReplace.cs @@ -3,18 +3,33 @@ // See the LICENSE file in the project root for more information namespace DotNetNuke.Services.Tokens { + using System; using System.Web.UI; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using Microsoft.Extensions.DependencyInjection; + public class HtmlTokenReplace : TokenReplace { /// Initializes a new instance of the class. /// The page on which the module is rendering. + [Obsolete("Deprecated in DotNetNuke 10.0.0. Please use overload with IClientResourceController. Scheduled removal in v12.0.0.")] public HtmlTokenReplace(Page page) + : this((IClientResourceController)null) + { + } + + /// Initializes a new instance of the class. + /// The client resource controller. + public HtmlTokenReplace(IClientResourceController clientResourceController) : base(Scope.DefaultSettings) { - this.AddPropertySource("css", new CssPropertyAccess(page)); - this.AddPropertySource("js", new JavaScriptPropertyAccess(page)); - this.AddPropertySource("javascript", new JavaScriptPropertyAccess(page)); + clientResourceController ??= Globals.DependencyProvider.GetRequiredService(); + + this.AddPropertySource("css", new CssPropertyAccess(clientResourceController)); + this.AddPropertySource("js", new JavaScriptPropertyAccess(clientResourceController)); + this.AddPropertySource("javascript", new JavaScriptPropertyAccess(clientResourceController)); this.AddPropertySource("antiforgerytoken", new AntiForgeryTokenPropertyAccess()); } } diff --git a/DNN Platform/Library/Services/Tokens/PropertyAccess/CssPropertyAccess.cs b/DNN Platform/Library/Services/Tokens/PropertyAccess/CssPropertyAccess.cs index dda8d0f64f4..c88f7504687 100644 --- a/DNN Platform/Library/Services/Tokens/PropertyAccess/CssPropertyAccess.cs +++ b/DNN Platform/Library/Services/Tokens/PropertyAccess/CssPropertyAccess.cs @@ -6,21 +6,33 @@ namespace DotNetNuke.Services.Tokens { using System; + using System.Net.Configuration; using System.Web.UI; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; using DotNetNuke.Entities.Users; - using DotNetNuke.Web.Client; - using DotNetNuke.Web.Client.ClientResourceManagement; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Web.Client.ResourceManager; + using Microsoft.Extensions.DependencyInjection; public class CssPropertyAccess : JsonPropertyAccess { - private readonly Page page; + private readonly IClientResourceController clientResourceController; /// Initializes a new instance of the class. /// The page to which the CSS should be registered. + [Obsolete("Deprecated in DotNetNuke 10.0.0. Please use overload with IClientResourceController. Scheduled removal in v12.0.0.")] public CssPropertyAccess(Page page) + : this((IClientResourceController)null) { - this.page = page; + } + + /// Initializes a new instance of the class. + /// The client resource controller. + public CssPropertyAccess(IClientResourceController clientResourceController) + { + this.clientResourceController = clientResourceController ?? Globals.GetCurrentServiceProvider().GetRequiredService(); } /// @@ -38,11 +50,15 @@ protected override string ProcessToken(StylesheetDto model, UserInfo accessingUs if (string.IsNullOrEmpty(model.Provider)) { - ClientResourceManager.RegisterStyleSheet(this.page, model.Path, model.Priority); + this.clientResourceController.RegisterStylesheet(model.Path, (FileOrder.Css)model.Priority); } else { - ClientResourceManager.RegisterStyleSheet(this.page, model.Path, model.Priority, model.Provider); + this.clientResourceController + .CreateStylesheet(model.Path) + .SetPriority((FileOrder.Css)model.Priority) + .SetProvider(model.Provider) + .Register(); } return string.Empty; diff --git a/DNN Platform/Library/Services/Tokens/PropertyAccess/JavaScriptPropertyAccess.cs b/DNN Platform/Library/Services/Tokens/PropertyAccess/JavaScriptPropertyAccess.cs index 1b8496d25d8..33ba7179262 100644 --- a/DNN Platform/Library/Services/Tokens/PropertyAccess/JavaScriptPropertyAccess.cs +++ b/DNN Platform/Library/Services/Tokens/PropertyAccess/JavaScriptPropertyAccess.cs @@ -9,21 +9,32 @@ namespace DotNetNuke.Services.Tokens using System.Collections.Generic; using System.Web.UI; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Collections; + using DotNetNuke.Common; using DotNetNuke.Entities.Users; using DotNetNuke.Framework.JavaScriptLibraries; - using DotNetNuke.Web.Client; - using DotNetNuke.Web.Client.ClientResourceManagement; + using DotNetNuke.Web.Client.ResourceManager; + using Microsoft.Extensions.DependencyInjection; /// Property Access implementation for javascript registration. public class JavaScriptPropertyAccess : JsonPropertyAccess { - private readonly Page page; + private readonly IClientResourceController clientResourceController; /// Initializes a new instance of the class. /// The current page. + [Obsolete("Deprecated in DotNetNuke 10.0.0. Please use overload with IClientResourceController. Scheduled removal in v12.0.0.")] public JavaScriptPropertyAccess(Page page) + : this((IClientResourceController)null) { - this.page = page; + } + + /// Initializes a new instance of the class. + /// The client resource controller. + public JavaScriptPropertyAccess(IClientResourceController clientResourceController) + { + this.clientResourceController = clientResourceController ?? Globals.GetCurrentServiceProvider().GetRequiredService(); } /// @@ -45,14 +56,29 @@ protected override string ProcessToken(JavaScriptDto model, UserInfo accessingUs } else { - ClientResourceManager.RegisterScript( - this.page, - model.Path, - model.Priority, - model.Provider ?? string.Empty, - model.JsName ?? string.Empty, - model.Version ?? string.Empty, - model.HtmlAttributes); + var script = this.clientResourceController + .CreateScript(model.Path); + if (model.Priority > 0) + { + script.SetPriority(model.Priority); + } + + if (!string.IsNullOrEmpty(model.Provider)) + { + script.SetProvider(model.Provider); + } + + if (!string.IsNullOrEmpty(model.JsName)) + { + script.SetNameAndVersion(model.JsName, model.Version, false); + } + + if (model.HtmlAttributes != null) + { + model.HtmlAttributes.ForEach(kvp => script.AddAttribute(kvp.Key, kvp.Value)); + } + + script.Register(); } return string.Empty; diff --git a/DNN Platform/Library/Services/Upgrade/Upgrade.cs b/DNN Platform/Library/Services/Upgrade/Upgrade.cs index 22abb9eb26c..30458d006b7 100644 --- a/DNN Platform/Library/Services/Upgrade/Upgrade.cs +++ b/DNN Platform/Library/Services/Upgrade/Upgrade.cs @@ -212,13 +212,14 @@ public static TabInfo AddHostPage(string tabName, string description, string tab /// The key for this control in the Definition. /// The title of this control. /// The source of ths control. + /// The mvc control class of ths control. /// The icon file. /// The type of control. /// The vieworder for this module. - public static void AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string iconFile, SecurityAccessLevel controlType, int viewOrder) + public static void AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string mvcControlClass, string iconFile, SecurityAccessLevel controlType, int viewOrder) { // Call Overload with HelpUrl = Null.NullString - AddModuleControl(moduleDefId, controlKey, controlTitle, controlSrc, iconFile, controlType, viewOrder, Null.NullString); + AddModuleControl(moduleDefId, controlKey, controlTitle, controlSrc, mvcControlClass, iconFile, controlType, viewOrder, Null.NullString); } /// AddModuleDefinition adds a new Core Module Definition to the system. @@ -2101,37 +2102,36 @@ protected static bool IsLanguageEnabled(int portalid, string code) /// The type of control. /// The vieworder for this module. /// The Help Url. - private static void AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string iconFile, SecurityAccessLevel controlType, int viewOrder, string helpURL) + private static void AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string mvcControlClass, string iconFile, SecurityAccessLevel controlType, int viewOrder, string helpURL) { - AddModuleControl(moduleDefId, controlKey, controlTitle, controlSrc, iconFile, controlType, viewOrder, helpURL, false); + AddModuleControl(moduleDefId, controlKey, controlTitle, controlSrc, mvcControlClass, iconFile, controlType, viewOrder, helpURL, false); } - private static void AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string iconFile, SecurityAccessLevel controlType, int viewOrder, string helpURL, bool supportsPartialRendering) + private static void AddModuleControl(int moduleDefId, string controlKey, string controlTitle, string controlSrc, string mvcControlClass, string iconFile, SecurityAccessLevel controlType, int viewOrder, string helpURL, bool supportsPartialRendering) { DnnInstallLogger.InstallLogInfo(Localization.GetString("LogStart", Localization.GlobalResourceFile) + "AddModuleControl:" + moduleDefId); // check if module control exists var moduleControl = ModuleControlController.GetModuleControlByControlKey(controlKey, moduleDefId); - if (moduleControl != null) - { - return; - } - - moduleControl = new ModuleControlInfo - { - ModuleControlID = Null.NullInteger, - ModuleDefID = moduleDefId, - ControlKey = controlKey, - ControlTitle = controlTitle, - ControlSrc = controlSrc, - ControlType = controlType, - ViewOrder = viewOrder, - IconFile = iconFile, - HelpURL = helpURL, - SupportsPartialRendering = supportsPartialRendering, - }; + if (moduleControl == null) + { + moduleControl = new ModuleControlInfo + { + ModuleControlID = Null.NullInteger, + ModuleDefID = moduleDefId, + ControlKey = controlKey, + ControlTitle = controlTitle, + ControlSrc = controlSrc, + MvcControlClass = mvcControlClass, + ControlType = controlType, + ViewOrder = viewOrder, + IconFile = iconFile, + HelpURL = helpURL, + SupportsPartialRendering = supportsPartialRendering, + }; - ModuleControlController.AddModuleControl(moduleControl); + ModuleControlController.AddModuleControl(moduleControl); + } } /// AddModuleDefinition adds a new Core Module Definition to the system. diff --git a/DNN Platform/Library/UI/Modules/Html5/Html5HostControl.cs b/DNN Platform/Library/UI/Modules/Html5/Html5HostControl.cs index ec02283e343..a2ff32c29af 100644 --- a/DNN Platform/Library/UI/Modules/Html5/Html5HostControl.cs +++ b/DNN Platform/Library/UI/Modules/Html5/Html5HostControl.cs @@ -8,6 +8,7 @@ namespace DotNetNuke.UI.Modules.Html5 using System.Web; using System.Web.UI; + using DotNetNuke.Abstractions.ClientResources; using DotNetNuke.Abstractions.Modules; using DotNetNuke.Common; using DotNetNuke.Common.Utilities; @@ -15,13 +16,13 @@ namespace DotNetNuke.UI.Modules.Html5 using DotNetNuke.Entities.Modules.Actions; using DotNetNuke.Framework; using DotNetNuke.Services.Cache; - using DotNetNuke.Web.Client; + using DotNetNuke.Services.ClientDependency; using DotNetNuke.Web.Client.ClientResourceManagement; - using Microsoft.Extensions.DependencyInjection; public class Html5HostControl : ModuleControlBase, IActionable { + private readonly Lazy serviceScopeContainer = new Lazy(ServiceScopeContainer.GetRequestOrCreateScope); private readonly string html5File; private readonly IBusinessControllerProvider businessControllerProvider; private string fileContent; @@ -47,6 +48,9 @@ public Html5HostControl(string html5File, IBusinessControllerProvider businessCo /// public ModuleActionCollection ModuleActions { get; private set; } + /// Gets the dependency injection service provider. + protected IServiceProvider DependencyProvider => this.serviceScopeContainer.Value.ServiceScope.ServiceProvider; + /// protected override void OnInit(EventArgs e) { @@ -54,24 +58,26 @@ protected override void OnInit(EventArgs e) if (!string.IsNullOrEmpty(this.html5File)) { + var clientResourceController = this.DependencyProvider.GetRequiredService(); + // Check if css file exists var cssFile = Path.ChangeExtension(this.html5File, ".css"); if (this.FileExists(cssFile)) { - ClientResourceManager.RegisterStyleSheet(this.Page, cssFile, FileOrder.Css.DefaultPriority); + clientResourceController.RegisterStylesheet(cssFile, FileOrder.Css.DefaultPriority); } // Check if js file exists var jsFile = Path.ChangeExtension(this.html5File, ".js"); if (this.FileExists(jsFile)) { - ClientResourceManager.RegisterScript(this.Page, jsFile, FileOrder.Js.DefaultPriority); + clientResourceController.RegisterScript(jsFile, FileOrder.Js.DefaultPriority); } this.fileContent = this.GetFileContent(this.html5File); this.ModuleActions = new ModuleActionCollection(); - var tokenReplace = new Html5ModuleTokenReplace(this.Page, this.businessControllerProvider, this.html5File, this.ModuleContext, this.ModuleActions); + var tokenReplace = new Html5ModuleTokenReplace(this.Page, new HttpRequestWrapper(this.Page.Request), clientResourceController, this.businessControllerProvider, this.html5File, this.ModuleContext, this.ModuleActions); this.fileContent = tokenReplace.ReplaceEnvironmentTokens(this.fileContent); } diff --git a/DNN Platform/Library/UI/Modules/Html5/Html5ModuleControlFactory.cs b/DNN Platform/Library/UI/Modules/Html5/Html5ModuleControlFactory.cs index 7f5cde62c71..cf5fd4304a3 100644 --- a/DNN Platform/Library/UI/Modules/Html5/Html5ModuleControlFactory.cs +++ b/DNN Platform/Library/UI/Modules/Html5/Html5ModuleControlFactory.cs @@ -6,10 +6,10 @@ namespace DotNetNuke.UI.Modules.Html5 using System; using System.Web.UI; + using DotNetNuke.Abstractions.ClientResources; using DotNetNuke.Abstractions.Modules; using DotNetNuke.Common; using DotNetNuke.Entities.Modules; - using Microsoft.Extensions.DependencyInjection; /// Module control factory for HTML modules. diff --git a/DNN Platform/Library/UI/Modules/Html5/Html5ModuleTokenReplace.cs b/DNN Platform/Library/UI/Modules/Html5/Html5ModuleTokenReplace.cs index 53fd0a7d0d7..a48d83695b3 100644 --- a/DNN Platform/Library/UI/Modules/Html5/Html5ModuleTokenReplace.cs +++ b/DNN Platform/Library/UI/Modules/Html5/Html5ModuleTokenReplace.cs @@ -3,15 +3,17 @@ // See the LICENSE file in the project root for more information namespace DotNetNuke.UI.Modules.Html5 { + using System; + using System.Web; using System.Web.UI; + using DotNetNuke.Abstractions.ClientResources; using DotNetNuke.Abstractions.Modules; using DotNetNuke.Common; using DotNetNuke.Entities.Modules; using DotNetNuke.Entities.Modules.Actions; using DotNetNuke.Services.Personalization; using DotNetNuke.Services.Tokens; - using Microsoft.Extensions.DependencyInjection; /// A for HTML modules. @@ -22,6 +24,7 @@ public class Html5ModuleTokenReplace : HtmlTokenReplace /// The path to the module's HTML file. /// The module context. /// The module actions collection. + [Obsolete("Deprecated in DotNetNuke 10.0.0. Please use overload with IClientResourceController. Scheduled removal in v12.0.0.")] public Html5ModuleTokenReplace( Page page, string html5File, @@ -37,23 +40,46 @@ public Html5ModuleTokenReplace( /// The path to the module's HTML file. /// The module context. /// The module actions collection. + [Obsolete("Deprecated in DotNetNuke 10.0.0. Please use overload with IClientResourceController. Scheduled removal in v12.0.0.")] public Html5ModuleTokenReplace( Page page, IBusinessControllerProvider businessControllerProvider, string html5File, ModuleInstanceContext moduleContext, ModuleActionCollection moduleActions) - : base(page) + : this(page, new HttpRequestWrapper(page.Request), null, businessControllerProvider, html5File, moduleContext, moduleActions) + { + } + + /// Initializes a new instance of the class. + /// The page on which the module is rendering. + /// Http request. + /// The client resource controller. + /// The business controller provider. + /// The path to the module's HTML file. + /// The module context. + /// The module actions collection. + public Html5ModuleTokenReplace( + Page page, // null in mvc scenario + HttpRequestBase request, + IClientResourceController clientResourceController, + IBusinessControllerProvider businessControllerProvider, + string html5File, + ModuleInstanceContext moduleContext, + ModuleActionCollection moduleActions) + : base(clientResourceController) { this.AccessingUser = moduleContext.PortalSettings.UserInfo; this.DebugMessages = Personalization.GetUserMode() != Entities.Portals.PortalSettings.Mode.View; this.ModuleId = moduleContext.ModuleId; this.PortalSettings = moduleContext.PortalSettings; + clientResourceController ??= Globals.DependencyProvider.GetRequiredService(); + this.AddPropertySource("moduleaction", new ModuleActionsPropertyAccess(moduleContext, moduleActions)); this.AddPropertySource("resx", new ModuleLocalizationPropertyAccess(moduleContext, html5File)); this.AddPropertySource("modulecontext", new ModuleContextPropertyAccess(moduleContext)); - this.AddPropertySource("request", new RequestPropertyAccess(page.Request)); + this.AddPropertySource("request", new RequestPropertyAccess(request)); // DNN-7750 businessControllerProvider ??= Globals.DependencyProvider.GetRequiredService(); diff --git a/DNN Platform/Library/UI/Modules/Html5/RequestPropertyAccess.cs b/DNN Platform/Library/UI/Modules/Html5/RequestPropertyAccess.cs index bb136ac8bb5..d81c77cddb0 100644 --- a/DNN Platform/Library/UI/Modules/Html5/RequestPropertyAccess.cs +++ b/DNN Platform/Library/UI/Modules/Html5/RequestPropertyAccess.cs @@ -13,11 +13,11 @@ namespace DotNetNuke.UI.Modules.Html5 /// Replaces tokens related to the current http request. public class RequestPropertyAccess : IPropertyAccess { - private readonly HttpRequest request; + private readonly HttpRequestBase request; /// Initializes a new instance of the class. /// The current http request. - public RequestPropertyAccess(HttpRequest request) + public RequestPropertyAccess(HttpRequestBase request) { this.request = request; } diff --git a/DNN Platform/Modules/DDRMenu/Localisation/Localiser.cs b/DNN Platform/Modules/DDRMenu/Localisation/Localiser.cs index 70240a863b6..f34be3b93b2 100644 --- a/DNN Platform/Modules/DDRMenu/Localisation/Localiser.cs +++ b/DNN Platform/Modules/DDRMenu/Localisation/Localiser.cs @@ -108,7 +108,7 @@ public void LocaliseNode(MenuNode node, int portalId) node.TabId = -1; } - node.Children.ForEach(this.LocaliseNode); + node.Children.ForEach(n => this.LocaliseNode(n, portalId)); } private TabInfo LocaliseTab(TabInfo tab, int portalId) diff --git a/DNN Platform/Modules/DDRMenu/Properties/AssemblyInfo.cs b/DNN Platform/Modules/DDRMenu/Properties/AssemblyInfo.cs index 291441a4a81..df9ecaecde4 100644 --- a/DNN Platform/Modules/DDRMenu/Properties/AssemblyInfo.cs +++ b/DNN Platform/Modules/DDRMenu/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -14,3 +15,4 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("3b7331d9-4c26-4936-8198-be5410591f8c")] +[assembly: InternalsVisibleTo("DotNetNuke.Web.NewDDRMenu")] diff --git a/DNN Platform/Modules/HTML/CkEditorContents.css b/DNN Platform/Modules/HTML/CkEditorContents.css new file mode 100644 index 00000000000..1796d7d2b6f --- /dev/null +++ b/DNN Platform/Modules/HTML/CkEditorContents.css @@ -0,0 +1,44 @@ +/* +DNN Custom Override Styles to fix issues with other modules, this is loaded inside the iframe for the editor. +*/ + +body { + /* Remove the background color to make it transparent */ + background-image: none; + background: none; + background-color: #fff; + margin: 20px; +} + +.cke_editable { + font-size: 13px; + line-height: 1.6; +} + +.cke_contents_ltr blockquote { + padding-left: 20px; + padding-right: 8px; + border-left-width: 5px; +} + +.cke_contents_rtl blockquote { + padding-left: 8px; + padding-right: 20px; + border-right-width: 5px; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +.text-justify { + text-align: justify; +} diff --git a/DNN Platform/Modules/HTML/Components/HtmlTextController.cs b/DNN Platform/Modules/HTML/Components/HtmlTextController.cs index 9d5cda53129..23e6eb8bff9 100644 --- a/DNN Platform/Modules/HTML/Components/HtmlTextController.cs +++ b/DNN Platform/Modules/HTML/Components/HtmlTextController.cs @@ -15,6 +15,7 @@ namespace DotNetNuke.Modules.Html using System.Xml; using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; using DotNetNuke.Common; using DotNetNuke.Common.Utilities; using DotNetNuke.Entities.Content.Taxonomy; @@ -65,9 +66,23 @@ public HtmlTextController(INavigationManager navigationManager) /// The HtmlText Content. /// Module Settings. /// The Portal Settings. - /// The Page Instance. + /// The page. /// The formatted HTML content. + [Obsolete("Use overload without IClientResourceController")] public static string FormatHtmlText(int moduleId, string content, HtmlModuleSettings settings, PortalSettings portalSettings, Page page) + { + var clientResourceController = Globals.GetCurrentServiceProvider().GetRequiredService(); + return FormatHtmlText(moduleId, content, settings, portalSettings, clientResourceController); + } + + /// FormatHtmlText formats HtmlText content for display in the browser. + /// The ModuleID. + /// The HtmlText Content. + /// Module Settings. + /// The Portal Settings. + /// ClientResourceController. + /// The formatted HTML content. + public static string FormatHtmlText(int moduleId, string content, HtmlModuleSettings settings, PortalSettings portalSettings, IClientResourceController clientResourceController) { // Html decode content content = HttpUtility.HtmlDecode(content); @@ -75,7 +90,7 @@ public static string FormatHtmlText(int moduleId, string content, HtmlModuleSett // token replace if (settings.ReplaceTokens) { - var tr = new HtmlTokenReplace(page) + var tr = new HtmlTokenReplace(clientResourceController) { AccessingUser = UserController.Instance.GetCurrentUserInfo(), DebugMessages = Personalization.GetUserMode() != PortalSettings.Mode.View, diff --git a/DNN Platform/Modules/HTML/Controllers/HTMLController.cs b/DNN Platform/Modules/HTML/Controllers/HTMLController.cs new file mode 100644 index 00000000000..32e12c2628c --- /dev/null +++ b/DNN Platform/Modules/HTML/Controllers/HTMLController.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Controllers +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Web.Mvc; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Content.Workflow; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Modules.Html; + using DotNetNuke.Modules.Html.Components; + using DotNetNuke.Modules.Html.Models; + using DotNetNuke.Web.MvcPipeline.Controllers; + + public class HTMLController : ModuleControllerBase + { + private readonly INavigationManager navigationManager; + private readonly HtmlTextController htmlTextController; + private readonly HtmlTextLogController htmlTextLogController = new HtmlTextLogController(); + private readonly IWorkflowManager workflowManager = WorkflowManager.Instance; + private readonly HtmlModuleSettingsRepository settingsRepository; + private readonly IClientResourceController clientResourceController; + + public HTMLController(IServiceProvider dependencyProvider, INavigationManager navigationManager, IClientResourceController clientResourceController) + : base(dependencyProvider) + { + this.navigationManager = navigationManager; + this.htmlTextController = new HtmlTextController(this.navigationManager); + this.settingsRepository = new HtmlModuleSettingsRepository(); + this.clientResourceController = clientResourceController; + } + + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult Save(EditHtmlViewModel model) + { + try + { + // get content + int workflowID = this.htmlTextController.GetWorkflow(model.ModuleId, model.TabId, this.PortalSettings.PortalId).Value; + var htmlContent = this.GetLatestHTMLContent(workflowID, model.ModuleId); + + var aliases = from PortalAliasInfo pa in PortalAliasController.Instance.GetPortalAliasesByPortalId(this.PortalSettings.PortalId) + select pa.HTTPAlias; + + var content = model.EditorContent; + + if (this.Request.QueryString["nuru"] == null) + { + content = HtmlUtils.AbsoluteToRelativeUrls(content, aliases); + } + + htmlContent.Content = content; + var workflow = this.workflowManager.GetWorkflow(workflowID); + var draftStateID = workflow.FirstState.StateID; + var publishedStateID = workflow.LastState.StateID; + + switch (model.CurrentWorkflowType) + { + case WorkflowType.DirectPublish: + this.htmlTextController.UpdateHtmlText(htmlContent, this.htmlTextController.GetMaximumVersionHistory(model.PortalId)); + + break; + case WorkflowType.SaveDraft: + case WorkflowType.ContentApproval: + // if it's already published set it back to draft + if (htmlContent.StateID == publishedStateID) + { + htmlContent.StateID = draftStateID; + } + + this.htmlTextController.UpdateHtmlText(htmlContent, this.htmlTextController.GetMaximumVersionHistory(model.PortalId)); + break; + } + + return new EmptyResult(); + } + catch (Exception exc) + { + // Gérer l'exception + throw new Exception(exc.Message, exc); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult ShowHistory(EditHtmlViewModel model) + { + model.ShowHistoryView = true; + model.LocalResourceFile = "DesktopModules\\HTML\\App_LocalResources/EditHTML"; + model.RedirectUrl = this.navigationManager.NavigateURL(model.TabId); + + try + { + int workflowID = this.htmlTextController.GetWorkflow(model.ModuleId, model.TabId, this.PortalSettings.PortalId).Value; + var htmlContent = this.GetLatestHTMLContent(workflowID, model.ModuleId); + + var maxVersions = this.htmlTextController.GetMaximumVersionHistory(this.PortalSettings.PortalId); + model.MaxVersions = maxVersions; + + var versions = this.htmlTextController.GetAllHtmlText(model.ModuleId); + model.VersionItems = versions.Cast().ToList(); + + return this.PartialView(this.ActiveModule, "EditHtml", model); + } + catch (Exception exc) + { + // Gérer l'exception + throw new Exception(exc.Message, exc); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult ShowPreview(EditHtmlViewModel model) + { + model.ShowPreviewView = true; + model.LocalResourceFile = "DesktopModules\\HTML\\App_LocalResources/EditHTML"; + model.RedirectUrl = this.navigationManager.NavigateURL(model.TabId); + + try + { + model.PreviewContent = model.EditorContent; // HttpUtility.HtmlDecode(model.HiddenEditorContent); + return this.PartialView(this.ActiveModule, "EditHtml", model); + } + catch (Exception exc) + { + // Gérer l'exception + throw new Exception(exc.Message, exc); + } + } + + public ActionResult ShowEdit(EditHtmlViewModel model) + { + model.ShowEditView = true; + model.LocalResourceFile = "DesktopModules\\HTML\\App_LocalResources/EditHTML"; + model.RedirectUrl = this.navigationManager.NavigateURL(model.TabId); + try + { + return this.PartialView(this.ActiveModule, "EditHtml", model); + } + catch (Exception exc) + { + // Gérer l'exception + throw new Exception(exc.Message, exc); + } + } + + public ActionResult HistoryRemove(EditHtmlViewModel model) + { + this.htmlTextController.DeleteHtmlText(model.ModuleId, model.ItemID); + return this.ShowEdit(model); + } + + public ActionResult HistoryRollback(EditHtmlViewModel model) + { + int workflowID = this.htmlTextController.GetWorkflow(model.ModuleId, model.TabId, this.PortalSettings.PortalId).Value; + var htmlContent = this.htmlTextController.GetHtmlText(model.ModuleId, model.ItemID); + htmlContent.ItemID = -1; + htmlContent.ModuleID = model.ModuleId; + htmlContent.WorkflowID = workflowID; + htmlContent.StateID = this.workflowManager.GetWorkflow(workflowID).FirstState.StateID; + this.htmlTextController.UpdateHtmlText(htmlContent, this.htmlTextController.GetMaximumVersionHistory(this.PortalSettings.PortalId)); + return this.ShowEdit(model); + } + + public ActionResult HistoryPreview(EditHtmlViewModel model) + { + model.ShowPreviewView = true; + model.LocalResourceFile = "DesktopModules\\HTML\\App_LocalResources/EditHTML"; + model.RedirectUrl = this.navigationManager.NavigateURL(model.TabId); + + try + { + int workflowID = this.htmlTextController.GetWorkflow(model.ModuleId, model.TabId, this.PortalSettings.PortalId).Value; + var htmlContent = this.htmlTextController.GetHtmlText(model.ModuleId, model.ItemID); + + var moduleSettings = this.settingsRepository.GetSettings(this.ActiveModule); + model.PreviewContent = HtmlTextController.FormatHtmlText(model.ModuleId, htmlContent.Content, moduleSettings, this.PortalSettings, this.clientResourceController); + + return this.PartialView(this.ActiveModule, "EditHtml", model); + } + catch (Exception exc) + { + // Gérer l'exception + throw new Exception(exc.Message, exc); + } + } + + private HtmlTextInfo GetLatestHTMLContent(int workflowID, int moduleId) + { + var htmlContent = this.htmlTextController.GetTopHtmlText(moduleId, false, workflowID); + if (htmlContent == null) + { + htmlContent = new HtmlTextInfo(); + htmlContent.ItemID = -1; + htmlContent.StateID = this.workflowManager.GetWorkflow(workflowID).FirstState.StateID; + htmlContent.WorkflowID = workflowID; + htmlContent.ModuleID = moduleId; + } + + return htmlContent; + } + + private ActionResult PartialView(ModuleInfo module, string viewName, EditHtmlViewModel model) + { + return this.PartialView("~/" + Path.GetDirectoryName(module.ModuleControl.ControlSrc) + "/Views/" + viewName + ".cshtml", model); + } + } +} diff --git a/DNN Platform/Modules/HTML/Controls/EditHTMLControl.cs b/DNN Platform/Modules/HTML/Controls/EditHTMLControl.cs new file mode 100644 index 00000000000..979c4d5f01d --- /dev/null +++ b/DNN Platform/Modules/HTML/Controls/EditHTMLControl.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Controls +{ + using System; + using System.Collections.Specialized; + using System.Linq; + using System.Web.UI.WebControls; + + using DNNConnect.CKEditorProvider.Utilities; + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Content.Workflow; + using DotNetNuke.Entities.Content.Workflow.Entities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Modules.Html; + using DotNetNuke.Modules.Html.Models; + using DotNetNuke.Security; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + + using Microsoft.Extensions.DependencyInjection; + + public class EditHTMLControl : RazorModuleControlBase, IPageContributor + { + private readonly INavigationManager navigationManager; + private readonly HtmlTextController htmlTextController; + private readonly IWorkflowManager workflowManager = WorkflowManager.Instance; + + public EditHTMLControl() + { + this.navigationManager = Globals.DependencyProvider.GetRequiredService(); + this.htmlTextController = new HtmlTextController(this.navigationManager); + } + + public override string ControlName => "HtmlEdit"; + + public override string ResourceName => this.ControlName + ".ascx.resx"; + + public void ConfigurePage(PageConfigurationContext context) + { + context.ClientResourceController.CreateStylesheet("~/DesktopModules/HTML/edit.css").Register(); + context.ClientResourceController.CreateStylesheet("~/Portals/_default/Skins/_default/WebControlSkin/Default/GridView.default.css").Register(); + context.ClientResourceController.CreateScript("~/Resources/Shared/scripts/jquery/jquery.form.min.js").Register(); + context.ClientResourceController.CreateScript("~/DesktopModules/HTML/js/edit.js").Register(); + } + + public override IRazorModuleResult Invoke() + { + var model = new EditHtmlViewModel(); + try + { + model.LocalResourceFile = this.LocalResourceFile; + model.ShowEditView = true; + model.ModuleId = this.ModuleId; + model.TabId = this.TabId; + model.PortalId = this.PortalId; + model.RedirectUrl = this.navigationManager.NavigateURL(); + int workflowID = this.htmlTextController.GetWorkflow(this.ModuleId, this.TabId, this.PortalId).Value; + + var htmlContentItemID = Null.NullInteger; + var htmlContent = this.htmlTextController.GetTopHtmlText(this.ModuleId, false, workflowID); + + if (htmlContent != null) + { + htmlContentItemID = htmlContent.ItemID; + var html = System.Web.HttpUtility.HtmlDecode(htmlContent.Content); + model.EditorContent = html; + } + + var workflow = this.workflowManager.GetWorkflow(workflowID); + var workflowStates = workflow.States.ToList(); + model.MaxVersions = this.htmlTextController.GetMaximumVersionHistory(this.PortalId); + var userCanEdit = this.UserInfo.IsSuperUser || PortalSecurity.IsInRole(this.PortalSettings.AdministratorRoleName); + + model.PageSize = Math.Min(Math.Max(model.MaxVersions, 5), 10); // min 5, max 10 + + switch (workflow.WorkflowKey) + { + case SystemWorkflowManager.DirectPublishWorkflowKey: + model.CurrentWorkflowType = WorkflowType.DirectPublish; + break; + case SystemWorkflowManager.SaveDraftWorkflowKey: + model.CurrentWorkflowType = WorkflowType.SaveDraft; + break; + case SystemWorkflowManager.ContentAprovalWorkflowKey: + model.CurrentWorkflowType = WorkflowType.ContentApproval; + break; + } + + if (htmlContentItemID != -1) + { + this.PopulateModelWithContent(model, htmlContent); + } + else + { + this.PopulateModelWithInitialContent(model, workflowStates[0]); + } + + model.ShowPublishOption = model.CurrentWorkflowType != WorkflowType.DirectPublish; + model.ShowCurrentVersion = model.CurrentWorkflowType != WorkflowType.DirectPublish; + model.ShowPreviewVersion = model.CurrentWorkflowType != WorkflowType.DirectPublish; + model.ShowHistoryView = false; + + // Master Content Button + var objModule = ModuleController.Instance.GetModule(this.ModuleId, this.TabId, false); + if (objModule.DefaultLanguageModule != null) + { + model.ShowMasterContentButton = true; + } + else + { + model.ShowMasterContentButton = false; + } + + // Render Options + model.RenderOptions = new System.Collections.Generic.List(); + model.RenderOptions.Add(new System.Web.Mvc.SelectListItem { Text = Localization.GetString("liRichText", this.LocalResourceFile), Value = "RICH" }); + model.RenderOptions.Add(new System.Web.Mvc.SelectListItem { Text = Localization.GetString("liBasicText", this.LocalResourceFile), Value = "BASIC" }); + + // Load CKEditor Settings + // this.LoadEditorSettings(model); + } + catch (Exception exc) + { + // Exceptions.ProcessModuleLoadException(this, exc); + throw new Exception("EditHTML", exc); + } + + return this.View(model); + + // TODO: CSP - enable when CSP implementation is ready + // this.contentSecurityPolicy.StyleSource.AddInline(); + // this.contentSecurityPolicy.ScriptSource.AddSelf().AddInline(); + // this.contentSecurityPolicy.ImgSource.AddScheme("data:"); + } + + private void PopulateModelWithContent(EditHtmlViewModel model, HtmlTextInfo htmlContent) + { + model.CurrentWorkflowInUse = htmlContent.WorkflowName; + model.CurrentWorkflowState = htmlContent.StateName; + model.CurrentVersion = htmlContent.Version.ToString(); + + // model.Content = this.FormatContent(htmlContent.Content); + } + + private void PopulateModelWithInitialContent(EditHtmlViewModel model, WorkflowState firstState) + { + // model.EditorContent = this.LocalizeString("AddContent"); + model.CurrentWorkflowInUse = firstState.StateName; + model.ShowCurrentWorkflowState = false; + model.ShowCurrentVersion = false; + } + + /* + private void LoadEditorSettings(EditHtmlViewModel model) + { + const string ProviderType = "htmlEditor"; + + // Load config settings + var settings = SettingsLoader.LoadConfigSettings(ProviderType); + var configFolder = settings["configFolder"]; + + // Load editor settings + var currentEditorSettings = SettingsLoader.LoadSettings( + this.PortalSettings, + this.ModuleId, + this.ModuleId.ToString(), + configFolder); + + // Get module configuration + var moduleConfiguration = ModuleController.Instance.GetModule(this.ModuleId, this.TabId, false); + + // Populate settings + var emptyAttributes = new NameValueCollection(); + SettingsLoader.PopulateSettings( + settings, + currentEditorSettings, + this.PortalSettings, + moduleConfiguration, + emptyAttributes, + Unit.Empty, + Unit.Empty, + this.ModuleId.ToString(), + this.ModuleId, + null); + + // Generate config script + var editorVar = $"editor{this.ModuleId}"; + model.ConfigScript = SettingsLoader.GetEditorConfigScript(settings, editorVar); + } + */ + } +} diff --git a/DNN Platform/Modules/HTML/Controls/HtmlModuleControl.cs b/DNN Platform/Modules/HTML/Controls/HtmlModuleControl.cs new file mode 100644 index 00000000000..96d8c86ccc4 --- /dev/null +++ b/DNN Platform/Modules/HTML/Controls/HtmlModuleControl.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Modules.Html.Controls +{ + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Entities.Content.Workflow; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Modules.Html.Components; + using DotNetNuke.Modules.Html.Models; + using DotNetNuke.Security; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Personalization; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + using Microsoft.Extensions.DependencyInjection; + + public class HtmlModuleControl : RazorModuleControlBase, IActionable + { + private readonly INavigationManager navigationManager; + private readonly HtmlTextController htmlTextController; + private readonly IWorkflowManager workflowManager; + private readonly IClientResourceController clientResourceController; + + private HtmlModuleSettings settings; + + public HtmlModuleControl(INavigationManager navigationManager, IClientResourceController clientResourceController) + { + this.navigationManager = navigationManager; + this.htmlTextController = new HtmlTextController(this.navigationManager); + this.workflowManager = WorkflowManager.Instance; + this.clientResourceController = clientResourceController; + } + + public override string ControlName => "HtmlModule"; + + public override string ResourceName => this.ControlName + ".ascx.resx"; + + public HtmlModuleSettings HtmlSettings + { + get + { + if (this.settings == null) + { + var repo = new HtmlModuleSettingsRepository(); + this.settings = repo.GetSettings(this.ModuleConfiguration); + } + + return this.settings; + } + } + + /// Gets moduleActions is an interface property that returns the module actions collection for the module. + public ModuleActionCollection ModuleActions + { + get + { + // add the Edit Text action + var actions = new ModuleActionCollection(); + actions.Add( + this.GetNextActionID(), + Localization.GetString(ModuleActionType.AddContent, this.LocalResourceFile), + ModuleActionType.AddContent, + string.Empty, + string.Empty, + this.EditUrl(), + false, + SecurityAccessLevel.Edit, + true, + false); + + // add mywork to action menu + actions.Add( + this.GetNextActionID(), + Localization.GetString("MyWork.Action", this.LocalResourceFile), + "MyWork.Action", + string.Empty, + "view.gif", + this.EditUrl("MyWork"), + false, + SecurityAccessLevel.Edit, + true, + false); + + return actions; + } + } + + public override IRazorModuleResult Invoke() + { + bool editorEnabled = this.PortalSettings.InlineEditorEnabled; + if (editorEnabled && this.ModuleContext.IsEditable && Personalization.GetUserMode() == DotNetNuke.Entities.Portals.PortalSettings.Mode.Edit) + { + editorEnabled = true; + } + else + { + editorEnabled = false; + } + + int workflowID = this.htmlTextController.GetWorkflow(this.ModuleId, this.TabId, this.PortalId).Value; + HtmlTextInfo content = this.htmlTextController.GetTopHtmlText(this.ModuleId, !this.ModuleContext.IsEditable, workflowID); + + var contentString = string.Empty; + if (content != null) + { + contentString = content.Content; + } + else + { + if (Personalization.GetUserMode() == DotNetNuke.Entities.Portals.PortalSettings.Mode.Edit) + { + contentString = editorEnabled + ? Localization.GetString("AddContentFromToolBar.Text", this.LocalResourceFile) + : Localization.GetString("AddContentFromActionMenu.Text", this.LocalResourceFile); + } + else + { + return this.Content(string.Empty); + } + } + + var html = HtmlTextController.FormatHtmlText(this.ModuleId, contentString, this.HtmlSettings, this.PortalSettings, this.clientResourceController); + + return this.View(new HtmlModuleModel() + { + Html = html, + }); + } + } +} diff --git a/DNN Platform/Modules/HTML/Controls/MyWorkControl.cs b/DNN Platform/Modules/HTML/Controls/MyWorkControl.cs new file mode 100644 index 00000000000..283c9444391 --- /dev/null +++ b/DNN Platform/Modules/HTML/Controls/MyWorkControl.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Controls +{ + using System.Linq; + + using DotNetNuke.Abstractions; + using DotNetNuke.Common; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Modules.Actions; + using DotNetNuke.Modules.Html; + using DotNetNuke.Modules.Html.Models; + using DotNetNuke.Security; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + using Microsoft.Extensions.DependencyInjection; + + public class MyWorkControl : RazorModuleControlBase, IActionable, IPageContributor + { + private readonly INavigationManager navigationManager; + + public MyWorkControl() + { + this.navigationManager = Globals.DependencyProvider.GetRequiredService(); + } + + public override string ControlName => "MyWork"; + + public override string ResourceName => this.ControlName + ".ascx.resx"; + + public ModuleActionCollection ModuleActions + { + get + { + var actions = new ModuleActionCollection(); + actions.Add( + this.GetNextActionID(), + Localization.GetString(ModuleActionType.AddContent, this.LocalResourceFile), + ModuleActionType.AddContent, + string.Empty, + string.Empty, + this.EditUrl(), + false, + SecurityAccessLevel.Edit, + true, + false); + + return actions; + } + } + + public void ConfigurePage(PageConfigurationContext context) + { + context.ClientResourceController + .CreateStylesheet("~/DesktopModules/HTML/edit.css") + .Register(); + context.ClientResourceController + .CreateStylesheet("~/Portals/_default/Skins/_default/WebControlSkin/Default/GridView.default.css") + .Register(); + } + + public override IRazorModuleResult Invoke() + { + var objHtmlTextUsers = new HtmlTextUserController(); + var lst = objHtmlTextUsers.GetHtmlTextUser(this.UserInfo.UserID).Cast(); + + return this.View(new MyWorkModel() + { + LocalResourceFile = this.LocalResourceFile, + ModuleId = this.ModuleId, + TabId = this.TabId, + RedirectUrl = this.navigationManager.NavigateURL(), + HtmlTextUsers = lst.Select(u => new HtmlTextUserModel() + { + Url = this.navigationManager.NavigateURL(u.TabID), + ModuleID = u.ModuleID, + ModuleTitle = u.ModuleTitle, + StateName = u.StateName, + }).ToList(), + }); + } + } +} diff --git a/DNN Platform/Modules/HTML/DotNetNuke.Modules.Html.csproj b/DNN Platform/Modules/HTML/DotNetNuke.Modules.Html.csproj index b65d25ed348..af2423f10a3 100644 --- a/DNN Platform/Modules/HTML/DotNetNuke.Modules.Html.csproj +++ b/DNN Platform/Modules/HTML/DotNetNuke.Modules.Html.csproj @@ -95,6 +95,21 @@ + + ..\..\packages\Microsoft.AspNet.Mvc.5.3.0\lib\net45\System.Web.Mvc.dll + + + ..\..\packages\Microsoft.AspNet.WebPages.3.3.0\lib\net45\System.Web.Helpers.dll + + + ..\..\packages\Microsoft.AspNet.WebPages.3.3.0\lib\net45\System.Web.WebPages.dll + + + ..\..\packages\Microsoft.AspNet.WebPages.3.3.0\lib\net45\System.Web.WebPages.Deployment.dll + + + ..\..\packages\Microsoft.AspNet.WebPages.3.3.0\lib\net45\System.Web.WebPages.Razor.dll + @@ -115,6 +130,10 @@ + + + + @@ -131,6 +150,12 @@ HtmlModule.ascx + + + + + + MyWork.ascx ASPXCodeBehind @@ -166,6 +191,7 @@ + @@ -196,6 +222,12 @@ + + + + + + @@ -206,6 +238,10 @@ {6928A9B1-F88A-4581-A132-D3EB38669BB0} DotNetNuke.Abstractions + + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3} + DotNetNuke.Web.MvcPipeline + {03e3afa5-ddc9-48fb-a839-ad4282ce237e} DotNetNuke.Web.Client @@ -220,6 +256,10 @@ {6b29aded-7b56-4484-bea5-c0e09079535b} False + + {ae7e021e-7c7b-4003-9bd6-5a04c781c277} + DNNConnect.CKEditorProvider + diff --git a/DNN Platform/Modules/HTML/HtmlModule.ascx.cs b/DNN Platform/Modules/HTML/HtmlModule.ascx.cs index 3e87d745e81..b1e1275d647 100644 --- a/DNN Platform/Modules/HTML/HtmlModule.ascx.cs +++ b/DNN Platform/Modules/HTML/HtmlModule.ascx.cs @@ -22,6 +22,8 @@ namespace DotNetNuke.Modules.Html using DotNetNuke.Services.Localization; using DotNetNuke.Services.Personalization; using DotNetNuke.UI.WebControls; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.Utils; using Microsoft.Extensions.DependencyInjection; /// The HtmlModule Class provides the UI for displaying the Html. diff --git a/DNN Platform/Modules/HTML/Models/EditHtmlViewModel.cs b/DNN Platform/Modules/HTML/Models/EditHtmlViewModel.cs new file mode 100644 index 00000000000..cfa30f97094 --- /dev/null +++ b/DNN Platform/Modules/HTML/Models/EditHtmlViewModel.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Models +{ + using System.Collections.Generic; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + public class EditHtmlViewModel : ModuleModelBase + { + [AllowHtml] + public string EditorContent { get; set; } + + public bool EditorEnabled { get; set; } + + public bool UseDecorate { get; set; } + + // public List ToolbarButtons { get; set; } + public string CurrentView { get; set; } + + public bool ShowEditView { get; set; } + + public bool ShowMasterContent { get; set; } + + public string MasterContent { get; set; } + + public bool ShowCurrentVersion { get; set; } + + public string CurrentWorkflowState { get; set; } + + public string CurrentVersion { get; set; } + + public string CurrentWorkflowInUse { get; set; } + + public bool ShowPublishOption { get; set; } + + public bool Publish { get; set; } + + public WorkflowType CurrentWorkflowType { get; set; } + + public int MaxVersions { get; set; } + + public bool ShowCurrentWorkflowState { get; set; } + + public List RenderOptions { get; set; } + + public string SelectedRenderOption { get; set; } + + public bool ShowPreviewVersion { get; set; } + + public bool ShowPreviewView { get; set; } + + public string RedirectUrl { get; set; } + + public bool ShowHistoryView { get; set; } + + public bool ShowMasterContentButton { get; set; } + + public List VersionItems { get; set; } + + public int ItemID { get; set; } + + public int PortalId { get; set; } + + public string PreviewContent { get; set; } + + public int PageSize { get; set; } + + public bool ShowHistoryButton { get; set; } + + public string ConfigScript { get; set; } + } +} diff --git a/DNN Platform/Modules/HTML/Models/HtmlModuleModel.cs b/DNN Platform/Modules/HTML/Models/HtmlModuleModel.cs new file mode 100644 index 00000000000..ec09b08ce67 --- /dev/null +++ b/DNN Platform/Modules/HTML/Models/HtmlModuleModel.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Models +{ + public class HtmlModuleModel + { + public string Html { get; set; } + } +} diff --git a/DNN Platform/Modules/HTML/Models/HtmlTextUserModel.cs b/DNN Platform/Modules/HTML/Models/HtmlTextUserModel.cs new file mode 100644 index 00000000000..747b4491718 --- /dev/null +++ b/DNN Platform/Modules/HTML/Models/HtmlTextUserModel.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Models +{ + public class HtmlTextUserModel + { + public string Url { get; set; } + + public int ModuleID { get; set; } + + public string ModuleTitle { get; set; } + + public string StateName { get; set; } + } +} diff --git a/DNN Platform/Modules/HTML/Models/MyWorkModel.cs b/DNN Platform/Modules/HTML/Models/MyWorkModel.cs new file mode 100644 index 00000000000..1fc2cdc1ca6 --- /dev/null +++ b/DNN Platform/Modules/HTML/Models/MyWorkModel.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information +namespace DotNetNuke.Modules.Html.Models +{ + using System.Collections.Generic; + + using DotNetNuke.Web.MvcPipeline.Models; + + public class MyWorkModel : ModuleModelBase + { + public IEnumerable HtmlTextUsers { get; set; } + + public string RedirectUrl { get; internal set; } + } +} diff --git a/DNN Platform/Modules/HTML/Models/WorkflowType.cs b/DNN Platform/Modules/HTML/Models/WorkflowType.cs new file mode 100644 index 00000000000..2742fabc6c6 --- /dev/null +++ b/DNN Platform/Modules/HTML/Models/WorkflowType.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Models +{ + /// + /// Définit les différents types de workflows disponibles pour la gestion du contenu HTML. + /// + public enum WorkflowType + { + /// + /// Le contenu est publié directement sans validation intermédiaire. + /// + DirectPublish = 1, + + /// + /// Le contenu est sauvegardé comme brouillon avant publication. + /// + SaveDraft = 2, + + /// + /// Le contenu doit passer par un processus d'approbation avant publication. + /// + ContentApproval = 3, + } +} diff --git a/DNN Platform/Modules/HTML/Mvc/RouteConfig.cs b/DNN Platform/Modules/HTML/Mvc/RouteConfig.cs new file mode 100644 index 00000000000..648c5e77ab9 --- /dev/null +++ b/DNN Platform/Modules/HTML/Mvc/RouteConfig.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +namespace DotNetNuke.Modules.Html.Mvc +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Web; + + using DotNetNuke.Web.MvcPipeline.Routing; + + public class RouteConfig : IMvcRouteMapper + { + public void RegisterRoutes(IMapRoute mapRouteManager) + { + mapRouteManager.MapRoute( + "HTML", + "Html", + "{controller}/{action}", + new[] { "DotNetNuke.Modules.Html.Controllers" }); + } + } +} diff --git a/DNN Platform/Modules/HTML/MyWork.ascx.designer.cs b/DNN Platform/Modules/HTML/MyWork.ascx.designer.cs index d1c20a4491d..0310486ee53 100644 --- a/DNN Platform/Modules/HTML/MyWork.ascx.designer.cs +++ b/DNN Platform/Modules/HTML/MyWork.ascx.designer.cs @@ -1,36 +1,43 @@ -// -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See LICENSE file in the project root for full license information. -// -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. +//------------------------------------------------------------------------------ +// +// Ce code a été généré par un outil. // -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// +// Les changements apportés à ce fichier peuvent provoquer un comportement incorrect et seront perdues si +// le code est régénéré. +// //------------------------------------------------------------------------------ -namespace DotNetNuke.Modules.Html { - - - public partial class MyWork { - /// customJS control. +namespace DotNetNuke.Modules.Html +{ + + + public partial class MyWork + { + + /// + /// Contrôle customJS. + /// /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. + /// Champ généré automatiquement. + /// Pour modifier, déplacez la déclaration de champ du fichier de concepteur dans le fichier code-behind. /// protected global::DotNetNuke.Web.Client.ClientResourceManagement.DnnCssInclude customJS; - /// dgTabs control. + + /// + /// Contrôle dgTabs. + /// /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. + /// Champ généré automatiquement. + /// Pour modifier, déplacez la déclaration de champ du fichier de concepteur dans le fichier code-behind. /// protected global::DotNetNuke.Web.UI.WebControls.Internal.DnnGrid dgTabs; - /// hlCancel control. + + /// + /// Contrôle hlCancel. + /// /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. + /// Champ généré automatiquement. + /// Pour modifier, déplacez la déclaration de champ du fichier de concepteur dans le fichier code-behind. /// protected global::System.Web.UI.WebControls.HyperLink hlCancel; } diff --git a/DNN Platform/Modules/HTML/Views/EditHTML.cshtml b/DNN Platform/Modules/HTML/Views/EditHTML.cshtml new file mode 100644 index 00000000000..0dc151c0803 --- /dev/null +++ b/DNN Platform/Modules/HTML/Views/EditHTML.cshtml @@ -0,0 +1,92 @@ +@using DotNetNuke.Modules.Html.Models +@using DotNetNuke.Web.MvcPipeline +@using DotNetNuke.Services.Localization +@model EditHtmlViewModel + +
+ @using (Html.BeginForm("Save", "HTML", new { area = "HTML" })) + { + @Html.AntiForgeryToken() + @Html.HiddenFor(x => x.TabId) + @Html.HiddenFor(x => x.PortalId) + @Html.HiddenFor(x => x.ModuleId) + @Html.HiddenFor(x => x.CurrentWorkflowType) + +
+
+ @if (Model.ShowEditView) + { + @Html.Partial("~/DeskTopModules/Html/Views/_Edit.cshtml", Model) + } + else + { + @Html.HiddenFor(x => x.EditorContent) + } + @if (Model.ShowPreviewView) + { + +
+ @if (Model.ShowPreviewVersion) + { +
+ + Model.PreviewVersion +
+
+ + Model.PreviewWorkflowInUse +
+
+ + Model.PreviewWorkflowState +
+ } +
+ @Html.Raw(Model.PreviewContent) +
+
+ } + @if (Model.ShowHistoryView) + { + @Html.Partial("~/DesktopModules/HTML/Views/_History.cshtml", Model) + } +
+
+
+
    +
  • + +
  • +
  • + @Localization.GetString("cmdCancel", Model.LocalResourceFile) +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • + @*
  • +
    + @Html.DropDownListFor(m => m.SelectedRenderOption, Model.RenderOptions, new { @class = "dnnSecondaryAction", id = "ddlRender" }) +
    +
  • *@ + @if (Model.ShowMasterContentButton) + { +
  • + +
  • +
  • + +
  • + } +
+
+ } +
diff --git a/DNN Platform/Modules/HTML/Views/HtmlModule.cshtml b/DNN Platform/Modules/HTML/Views/HtmlModule.cshtml new file mode 100644 index 00000000000..f3bf8285f16 --- /dev/null +++ b/DNN Platform/Modules/HTML/Views/HtmlModule.cshtml @@ -0,0 +1,4 @@ +@using DotNetNuke.Modules.Html +@using DotNetNuke.Entities.Modules +@model DotNetNuke.Modules.Html.Models.HtmlModuleModel +
@Html.Raw(Model.Html)
\ No newline at end of file diff --git a/DNN Platform/Modules/HTML/Views/LoadSettings.cshtml b/DNN Platform/Modules/HTML/Views/LoadSettings.cshtml new file mode 100644 index 00000000000..cfa1125880c --- /dev/null +++ b/DNN Platform/Modules/HTML/Views/LoadSettings.cshtml @@ -0,0 +1,40 @@ +@using DotNetNuke.Modules.Html.Models +@using DotNetNuke.Services.Localization +@using DotNetNuke.Entities.Modules +@using DotNetNuke.Web.MvcPipeline.Modules +@model HtmlModuleSettingsModel +@{ + var LocalResourceFile = "~/DesktopModules/HTML/" + Localization.LocalResourceDirectory + "/Settings.ascx"; + +} + +
+ +
+ @Html.DnnLabelFor(m => m.ReplaceTokens, LocalResourceFile) + @Html.CheckBoxFor(m => m.ReplaceTokens) +
+
+ @Html.DnnLabelFor(m => m.UseDecorate, LocalResourceFile) + @Html.CheckBoxFor(m => m.UseDecorate) +
+
+ @Html.DnnLabelFor(m => m.SearchDescLength, LocalResourceFile) + @Html.TextBoxFor(m => m.SearchDescLength) + @Html.ValidationMessageFor(m => m.SearchDescLength) +
+
+ @Html.DnnLabelFor(m => m.SelectedWorkflow, LocalResourceFile) + @Html.DropDownListFor(m => m.SelectedWorkflow, new SelectList(Model.Workflows, "WorkflowID", "WorkflowName")) +
+
+ @Html.DnnLabelFor(m => m.ApplyTo, LocalResourceFile) + @Html.RadioButtonFor(m => m.ApplyTo, "Module") Module + @Html.RadioButtonFor(m => m.ApplyTo, "Page") Page + @Html.RadioButtonFor(m => m.ApplyTo, "Site") Site +
+ @Html.DnnLabelFor(m => m.Replace, LocalResourceFile) + @Html.CheckBoxFor(m => m.Replace, new { @class = "inline" }) +
+
+
\ No newline at end of file diff --git a/DNN Platform/Modules/HTML/Views/MyWork.cshtml b/DNN Platform/Modules/HTML/Views/MyWork.cshtml new file mode 100644 index 00000000000..926409178a0 --- /dev/null +++ b/DNN Platform/Modules/HTML/Views/MyWork.cshtml @@ -0,0 +1,38 @@ +@using DotNetNuke.Modules.Html.Models +@using DotNetNuke.Entities.Modules +@using DotNetNuke.Services.Localization +@model MyWorkModel +@{} + + +
+ @if (Model.HtmlTextUsers.Any()) + { + + + + + + + + @foreach (var item in Model.HtmlTextUsers) + { + + + + } + +
@Localization.GetString("Page", Model.LocalResourceFile)
+ @item.ModuleTitle ( @item.StateName ) +
+ } + else + { +
+ @Localization.GetString("lblNoRecords", Model.LocalResourceFile) +
+ } + +
\ No newline at end of file diff --git a/DNN Platform/Modules/HTML/Views/_Edit.cshtml b/DNN Platform/Modules/HTML/Views/_Edit.cshtml new file mode 100644 index 00000000000..d8a16325492 --- /dev/null +++ b/DNN Platform/Modules/HTML/Views/_Edit.cshtml @@ -0,0 +1,44 @@ +@using DotNetNuke.Modules.Html.Models +@using DotNetNuke.Web.MvcPipeline.Modules +@using DotNetNuke.Services.Localization +@using DotNetNuke.Web.MvcPipeline.Containers +@using DNNConnect.CKEditorProvider.Web +@model EditHtmlViewModel + +
+
+ @if (Model.ShowMasterContent) + { +
+
+ @Html.Raw(Model.MasterContent) +
+
+ } + @* Html.TextAreaFor(x => x.EditorContent, new { @class = "dnnTextEditor", rows = "10", cols = "100" })*@ + @*@Html.TextEditorFor(x => x.EditorContent)*@ + @Html.CKEditorEditorFor(x => x.EditorContent, Model.ModuleId) +
+ + @if (Model.ShowCurrentVersion) + { +
+
+
+ +
+ @Localization.GetString(Model.CurrentWorkflowState, Model.LocalResourceFile) + (@Localization.GetString("plCurrentWorkVersion", Model.LocalResourceFile) @Model.CurrentVersion) (@Localization.GetString(Model.CurrentWorkflowInUse, Model.LocalResourceFile)) +
+ @if (Model.ShowPublishOption) + { +
+
+ +
+ @Html.CheckBoxFor(m => m.Publish) @Localization.GetString("chkPublish", Model.LocalResourceFile) +
+ } +
+ } +
diff --git a/DNN Platform/Modules/HTML/Views/_History.cshtml b/DNN Platform/Modules/HTML/Views/_History.cshtml new file mode 100644 index 00000000000..6fcf8fac84a --- /dev/null +++ b/DNN Platform/Modules/HTML/Views/_History.cshtml @@ -0,0 +1,72 @@ +@using DotNetNuke.Modules.Html.Models +@using DotNetNuke.Web.MvcPipeline +@using DotNetNuke.Services.Localization +@model EditHtmlViewModel + + +@if (Model.ShowHistoryView) +{ +
+
+
+
+ +
+ @Model.MaxVersions +
+ + + + + + + + + + + @foreach (var item in Model.VersionItems) + { + + + + + + + + } +
+ Version @Localization.GetString("Version", Model.LocalResourceFile) + + Date + + User + + State + + Actions +
+ @item.Version + + @item.LastModifiedOnDate + + @item.DisplayName + + @item.StateName + + + + + + + +
+ + + + + +
+
+
+
+} diff --git a/DNN Platform/Modules/HTML/Views/web.config b/DNN Platform/Modules/HTML/Views/web.config new file mode 100644 index 00000000000..a7751b2f5f9 --- /dev/null +++ b/DNN Platform/Modules/HTML/Views/web.config @@ -0,0 +1,33 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Modules/HTML/dnn_HTML.dnn b/DNN Platform/Modules/HTML/dnn_HTML.dnn index 9c505e1be5a..6c4ae00ed47 100644 --- a/DNN Platform/Modules/HTML/dnn_HTML.dnn +++ b/DNN Platform/Modules/HTML/dnn_HTML.dnn @@ -104,7 +104,7 @@ Providers\DataProviders\SqlDataProvider 10.00.02.SqlDataProvider 10.00.02 - + + + + + + diff --git a/DNN Platform/Modules/NewDDRMenu/DNNMenu/DNNMenu.js b/DNN Platform/Modules/NewDDRMenu/DNNMenu/DNNMenu.js new file mode 100644 index 00000000000..cc74a7232ff --- /dev/null +++ b/DNN Platform/Modules/NewDDRMenu/DNNMenu/DNNMenu.js @@ -0,0 +1,239 @@ +if (!DDR.Menu.Providers.DNNMenu) { + DDRjQuery(function ($) { + DDR.Menu.Providers.DNNMenu = function (jqContainer, dnnNavParams) { + var me = this; + + me.baseConstructor(jqContainer, dnnNavParams); + } + DDR.Menu.Providers.DNNMenu.prototype = new DDR.Menu.Providers.BaseRenderer(); + + DDR.Menu.Providers.DNNMenu.prototype.createRootMenu = function () { + var me = this; + + var outerContainer = $(""); + var dnnNavContainer = me.createRenderedMenu(me.rootMenu); + dnnNavContainer.addClass(me.dnnNavParams.CSSControl); + outerContainer.append(dnnNavContainer); + + me.subMenus.each(function (m) { + dnnNavContainer.append(me.createRenderedMenu(m)); + }); + + me.jqContainer.replaceWith(outerContainer); + + me.jqContainer.show(1); + me.jqContainer.queue(function () { + me.addCovering(); + me.prepareHideAndShow(); + + $(this).dequeue(); + }); + } + + DDR.Menu.Providers.DNNMenu.prototype.createRenderedMenu = function (menu) { + var me = this; + + var level = menu.level; + var childItems = menu.childItems; + + if (level == 0) { + menu.flyout = false; + menu.layout = me.orientHorizontal ? "horizontal" : "vertical"; + var result = $(""); + childItems.each(function (i) { + result.append(me.createRenderedItem(i)); + }); + } + else { + menu.flyout = true; + menu.layout = "vertical"; + var parentItem = menu.parentItem; + var parentMenu = parentItem.parentMenu; + +// var result = $("").css({ "position": "absolute", "left": "-1000px" }); + var result = $("
").css({ "position": "absolute", "left": "-1000px" }); + var table = result.children("table"); + table.addClass(this.dnnNavParams.CSSContainerSub); + table.addClass("m"); + table.addClass("m" + (menu.level - 1)); + table.addClass("mid" + menu.id); + childItems.each(function (i) { + table.append(me.createRenderedItem(i)); + }); + } + + menu.rendered = result; + + return result; + }; + + DDR.Menu.Providers.DNNMenu.prototype.createRenderedItem = function (item) { + var me = this; + + var level = item.level; + var title = item.title; + var image = item.image; + var href = item.href; + var separator = item.separator; + + var result; + + if (level == 0) { + result = me.orientHorizontal ? $("") : $("
"); + var spanImg = result.children("span:eq(0)"); + var spanText = result.children("span:eq(1)"); + + result.addClass("root"); + + if (href && !item.isSeparator) { + item.coveringHere = function () { return item.rendered; }; + } + + spanImg.addClass("icn"); + if (image) { + spanImg.append($("").attr("src", image)); + } + + spanText.addClass("txt"); + spanText.css("cursor", "pointer").text(title); + + var nodeLeftHTML = me.dnnNavParams.NodeLeftHTMLRoot || ""; + var nodeRightHTML = me.dnnNavParams.NodeRightHTMLRoot || ""; + var separatorLeftHTML = me.dnnNavParams.SeparatorLeftHTML || ""; + var separatorRightHTML = me.dnnNavParams.SeparatorRightHTML || ""; + var cssClass = this.dnnNavParams.CSSNodeRoot; + + if (item.isBreadcrumb) { + if ((me.dnnNavParams.CSSBreadCrumbRoot || "") != "") + cssClass = me.dnnNavParams.CSSBreadCrumbRoot; + nodeLeftHTML = me.dnnNavParams.NodeLeftHTMLBreadCrumbRoot || nodeLeftHTML; + nodeRightHTML = me.dnnNavParams.NodeRightHTMLBreadCrumbRoot || nodeRightHTML; + separatorLeftHTML = me.dnnNavParams.SeparatorLeftHTMLBreadCrumb || separatorLeftHTML; + separatorRightHTML = me.dnnNavParams.SeparatorRightHTMLBreadCrumb || separatorRightHTML; + } + + if (item.isSelected) { + if ((me.dnnNavParams.CSSNodeSelectedRoot || "") != "") + cssClass = me.dnnNavParams.CSSNodeSelectedRoot; + separatorLeftHTML = me.dnnNavParams.SeparatorLeftHTMLActive || separatorLeftHTML; + separatorRightHTML = me.dnnNavParams.SeparatorRightHTMLActive || separatorRightHTML; + } + + result.addClass(cssClass); + + if (!item.first) { + separatorLeftHTML = (me.dnnNavParams.SeparatorHTML || "") + separatorLeftHTML; + } + separatorLeftHTML = separatorLeftHTML + nodeLeftHTML; + separatorRightHTML = nodeRightHTML + separatorRightHTML; + + if (separatorLeftHTML != "") { + result.prepend($("").append(separatorLeftHTML)); + } + if (separatorRightHTML != "") { + result.append($("").append(separatorRightHTML)); + } + + if (item.childMenu && me.dnnNavParams.IndicateChildren) + result.css({ + "background-image": "url(" + me.dnnNavParams.PathSystemImage + me.dnnNavParams.IndicateChildImageRoot + ")", + "background-repeat": "no-repeat", + "background-position": "right" + }); + } + else { + result = $("
"); + var tdImg = result.find("td:eq(0)"); + var spanImg = result.find("span:eq(0)"); + var spanText = result.find("span:eq(1)"); + var tdArrow = result.find("td:eq(2)"); + + if (href) { + item.coveringHere = function () { return item.rendered.find("td"); }; + } + + tdImg.addClass("icn"); + if (image) { + spanImg.append($("").attr("src", image)); + } + + spanText.addClass("txt"); + if (!item.isSeparator) { + spanText.text(title); + } + spanText.css("white-space", "nowrap"); + + if (item.childMenu && me.dnnNavParams.IndicateChildren) + tdArrow.append($("").attr("src", me.dnnNavParams.PathSystemImage + me.dnnNavParams.IndicateChildImageSub)); + + tdImg.addClass(me.dnnNavParams.CSSIcon); + tdArrow.addClass(me.dnnNavParams.CSSIndicateChildSub); + result.css("cursor", "pointer"); + + var nodeLeftHTML = me.dnnNavParams.NodeLeftHTMLSub || ""; + var nodeRightHTML = me.dnnNavParams.NodeRightHTMLSub || ""; + var cssClass = me.dnnNavParams.CSSNode; + + if (item.isBreadcrumb) { + if ((me.dnnNavParams.CSSBreadCrumbSub || "") != "") + cssClass = me.dnnNavParams.CSSBreadCrumbSub; + nodeLeftHTML = me.dnnNavParams.NodeLeftHTMLBreadCrumbSub || nodeLeftHTML; + nodeRightHTML = me.dnnNavParams.NodeRightHTMLBreadCrumbSub || nodeRightHTML; + } + + if (item.isSelected) { + if ((me.dnnNavParams.CSSNodeSelectedSub || "") != "") + cssClass = me.dnnNavParams.CSSNodeSelectedSub; + } + + if (item.isSeparator) { + cssClass = (me.dnnNavParams.CSSBreak || ""); + } + + result.addClass(cssClass); + + if (nodeLeftHTML) + tdImg.prepend($("").append(nodeLeftHTML)); + if (nodeRightHTML) + tdArrow.append($("").append(nodeRightHTML)); + } + + if (item.isSelected) { + result.addClass("sel"); + } + if (item.isBreadcrumb) { + result.addClass("bc"); + } + if (item.isSeparator) { + result.addClass("break"); + } + result.addClass("mi"); + result.addClass("mi" + item.path); + result.addClass("id" + item.id); + if (item.first) { + result.addClass("first"); + } + if (item.last) { + result.addClass("last"); + } + if (item.first && item.last) { + result.addClass("firstlast"); + } + + item.rendered = result; + + return result; + }; + + DDR.Menu.Providers.DNNMenu.prototype.menuItemHover = function (item) { + var me = this; + + if (item.level == 0) { + item.rendered.setHoverClass("hov " + (me.dnnNavParams.CSSNodeHoverRoot || (me.dnnNavParams.CSSNodeHover || ""))); + } + else { + item.rendered.setHoverClass("hov " + (me.dnnNavParams.CSSNodeHoverSub || (me.dnnNavParams.CSSNodeHover || ""))); + } + }; + }); +} diff --git a/DNN Platform/Modules/NewDDRMenu/DNNMenu/DNNMenu.min.js b/DNN Platform/Modules/NewDDRMenu/DNNMenu/DNNMenu.min.js new file mode 100644 index 00000000000..5c0f51fb1cd --- /dev/null +++ b/DNN Platform/Modules/NewDDRMenu/DNNMenu/DNNMenu.min.js @@ -0,0 +1 @@ +(function(n){n.effects||function(n){function t(t,i){var u=t[1]&&t[1].constructor==Object?t[1]:{},r,f;return i&&(u.mode=i),r=t[1]&&t[1].constructor!=Object?t[1]:u.duration?u.duration:t[2],r=n.fx.off?0:typeof r=="number"?r:n.fx.speeds[r]||n.fx.speeds._default,f=u.callback||n.isFunction(t[1])&&t[1]||n.isFunction(t[2])&&t[2]||n.isFunction(t[3])&&t[3],[t[0],u,r,f]}function i(t){var i;return t&&t.constructor==Array&&t.length==3?t:(i=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(t))?[parseInt(i[1],10),parseInt(i[2],10),parseInt(i[3],10)]:(i=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(t))?[parseFloat(i[1])*2.55,parseFloat(i[2])*2.55,parseFloat(i[3])*2.55]:(i=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(t))?[parseInt(i[1],16),parseInt(i[2],16),parseInt(i[3],16)]:(i=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(t))?[parseInt(i[1]+i[1],16),parseInt(i[2]+i[2],16),parseInt(i[3]+i[3],16)]:(i=/rgba\(0, 0, 0, 0\)/.exec(t))?r.transparent:r[n.trim(t).toLowerCase()]}function u(t,r){var u;do{if(u=n.css(t,r),u!=""&&u!="transparent"||n.nodeName(t,"body"))break;r="backgroundColor"}while(t=t.parentNode);return i(u)}n.effects={version:"1.7.2",save:function(n,t){for(var i=0;i<\/div>'),t=n.parent(),n.css("position")=="static"?(t.css({position:"relative"}),n.css({position:"relative"})):(i=n.css("top"),isNaN(parseInt(i,10))&&(i="auto"),r=n.css("left"),isNaN(parseInt(r,10))&&(r="auto"),t.css({position:n.css("position"),top:i,left:r,zIndex:n.css("z-index")}).show(),n.css({position:"relative",top:0,left:0})),t.css(u),t)},removeWrapper:function(n){return n.parent().is(".ui-effects-wrapper")?n.parent().replaceWith(n):n},setTransition:function(t,i,r,u){return u=u||{},n.each(i,function(n,i){unit=t.cssUnit(i),unit[0]>0&&(u[i]=unit[0]*r+unit[1])}),u},animateClass:function(t,i,r,u){var f=typeof r=="function"?r:u?u:null,e=typeof r=="string"?r:null;return this.each(function(){var c={},u=n(this),s=u.attr("style")||"",h,o,r;typeof s=="object"&&(s=s.cssText),t.toggle&&(u.hasClass(t.toggle)?t.remove=t.toggle:t.add=t.toggle),h=n.extend({},document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle),t.add&&u.addClass(t.add),t.remove&&u.removeClass(t.remove),o=n.extend({},document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle),t.add&&u.removeClass(t.add),t.remove&&u.addClass(t.remove);for(r in o)typeof o[r]=="function"||!o[r]||r.indexOf("Moz")!=-1||r.indexOf("length")!=-1||o[r]==h[r]||!r.match(/color/i)&&(r.match(/color/i)||isNaN(parseInt(o[r],10)))||h.position=="static"&&(h.position!="static"||r.match(/left|top|bottom|right/))||(c[r]=o[r]);u.animate(c,i,e,function(){typeof n(this).attr("style")=="object"?(n(this).attr("style").cssText="",n(this).attr("style").cssText=s):n(this).attr("style",s),t.add&&n(this).addClass(t.add),t.remove&&n(this).removeClass(t.remove),f&&f.apply(this,arguments)})})}},n.fn.extend({_show:n.fn.show,_hide:n.fn.hide,__toggle:n.fn.toggle,_addClass:n.fn.addClass,_removeClass:n.fn.removeClass,_toggleClass:n.fn.toggleClass,effect:function(t,i,r,u){return n.effects[t]?n.effects[t].call(this,{method:t,options:i||{},duration:r,callback:u}):null},show:function(){return!arguments[0]||arguments[0].constructor==Number||/(slow|normal|fast)/.test(arguments[0])?this._show.apply(this,arguments):this.effect.apply(this,t(arguments,"show"))},hide:function(){return!arguments[0]||arguments[0].constructor==Number||/(slow|normal|fast)/.test(arguments[0])?this._hide.apply(this,arguments):this.effect.apply(this,t(arguments,"hide"))},toggle:function(){return!arguments[0]||arguments[0].constructor==Number||/(slow|normal|fast)/.test(arguments[0])||n.isFunction(arguments[0])||typeof arguments[0]=="boolean"?this.__toggle.apply(this,arguments):this.effect.apply(this,t(arguments,"toggle"))},addClass:function(t,i,r,u){return i?n.effects.animateClass.apply(this,[{add:t},i,r,u]):this._addClass(t)},removeClass:function(t,i,r,u){return i?n.effects.animateClass.apply(this,[{remove:t},i,r,u]):this._removeClass(t)},toggleClass:function(t,i,r,u){return typeof i!="boolean"&&i?n.effects.animateClass.apply(this,[{toggle:t},i,r,u]):this._toggleClass(t,i)},morph:function(t,i,r,u,f){return n.effects.animateClass.apply(this,[{add:i,remove:t},r,u,f])},switchClass:function(){return this.morph.apply(this,arguments)},cssUnit:function(t){var i=this.css(t),r=[];return n.each(["em","px","%","pt"],function(n,t){i.indexOf(t)>0&&(r=[parseFloat(i),t])}),r}}),n.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","color","outlineColor"],function(t,r){n.fx.step[r]=function(n){n.state==0&&(n.start=u(n.elem,r),n.end=i(n.end)),n.elem.style[r]="rgb("+[Math.max(Math.min(parseInt(n.pos*(n.end[0]-n.start[0])+n.start[0],10),255),0),Math.max(Math.min(parseInt(n.pos*(n.end[1]-n.start[1])+n.start[1],10),255),0),Math.max(Math.min(parseInt(n.pos*(n.end[2]-n.start[2])+n.start[2],10),255),0)].join(",")+")"}});var r={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]};n.easing.jswing=n.easing.swing,n.extend(n.easing,{def:"easeOutQuad",swing:function(t,i,r,u,f){return n.easing[n.easing.def](t,i,r,u,f)},easeInQuad:function(n,t,i,r,u){return r*(t/=u)*t+i},easeOutQuad:function(n,t,i,r,u){return-r*(t/=u)*(t-2)+i},easeInOutQuad:function(n,t,i,r,u){return(t/=u/2)<1?r/2*t*t+i:-r/2*(--t*(t-2)-1)+i},easeInCubic:function(n,t,i,r,u){return r*(t/=u)*t*t+i},easeOutCubic:function(n,t,i,r,u){return r*((t=t/u-1)*t*t+1)+i},easeInOutCubic:function(n,t,i,r,u){return(t/=u/2)<1?r/2*t*t*t+i:r/2*((t-=2)*t*t+2)+i},easeInQuart:function(n,t,i,r,u){return r*(t/=u)*t*t*t+i},easeOutQuart:function(n,t,i,r,u){return-r*((t=t/u-1)*t*t*t-1)+i},easeInOutQuart:function(n,t,i,r,u){return(t/=u/2)<1?r/2*t*t*t*t+i:-r/2*((t-=2)*t*t*t-2)+i},easeInQuint:function(n,t,i,r,u){return r*(t/=u)*t*t*t*t+i},easeOutQuint:function(n,t,i,r,u){return r*((t=t/u-1)*t*t*t*t+1)+i},easeInOutQuint:function(n,t,i,r,u){return(t/=u/2)<1?r/2*t*t*t*t*t+i:r/2*((t-=2)*t*t*t*t+2)+i},easeInSine:function(n,t,i,r,u){return-r*Math.cos(t/u*(Math.PI/2))+r+i},easeOutSine:function(n,t,i,r,u){return r*Math.sin(t/u*(Math.PI/2))+i},easeInOutSine:function(n,t,i,r,u){return-r/2*(Math.cos(Math.PI*t/u)-1)+i},easeInExpo:function(n,t,i,r,u){return t==0?i:r*Math.pow(2,10*(t/u-1))+i},easeOutExpo:function(n,t,i,r,u){return t==u?i+r:r*(-Math.pow(2,-10*t/u)+1)+i},easeInOutExpo:function(n,t,i,r,u){return t==0?i:t==u?i+r:(t/=u/2)<1?r/2*Math.pow(2,10*(t-1))+i:r/2*(-Math.pow(2,-10*--t)+2)+i},easeInCirc:function(n,t,i,r,u){return-r*(Math.sqrt(1-(t/=u)*t)-1)+i},easeOutCirc:function(n,t,i,r,u){return r*Math.sqrt(1-(t=t/u-1)*t)+i},easeInOutCirc:function(n,t,i,r,u){return(t/=u/2)<1?-r/2*(Math.sqrt(1-t*t)-1)+i:r/2*(Math.sqrt(1-(t-=2)*t)+1)+i},easeInElastic:function(n,t,i,r,u){var f=1.70158,e=0,o=r;return t==0?i:(t/=u)==1?i+r:(e||(e=u*.3),o<\/div>").css({position:"absolute",visibility:"visible",left:-e*(s/u),top:-f*(h/r)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:s/u,height:h/r,left:o.left+e*(s/u)+(t.options.mode=="show"?(e-Math.floor(u/2))*(s/u):0),top:o.top+f*(h/r)+(t.options.mode=="show"?(f-Math.floor(r/2))*(h/r):0),opacity:t.options.mode=="show"?0:1}).animate({left:o.left+e*(s/u)+(t.options.mode=="show"?0:(e-Math.floor(u/2))*(s/u)),top:o.top+f*(h/r)+(t.options.mode=="show"?0:(f-Math.floor(r/2))*(h/r)),opacity:t.options.mode=="show"?1:0},t.duration||500);setTimeout(function(){t.options.mode=="show"?i.css({visibility:"visible"}):i.css({visibility:"visible"}).hide(),t.callback&&t.callback.apply(i[0]),i.dequeue(),n("div.ui-effects-explode").remove()},t.duration||500)})}}(n),function(n){n.effects.fold=function(t){return this.queue(function(){var i=n(this),h=["position","top","left"],r=n.effects.setMode(i,t.options.mode||"hide"),f=t.options.size||15,c=!!t.options.horizFirst,l=t.duration?t.duration/2:n.fx.speeds._default/2,o,s;n.effects.save(i,h),i.show();var u=n.effects.createWrapper(i).css({overflow:"hidden"}),a=r=="show"!=c,v=a?["width","height"]:["height","width"],e=a?[u.width(),u.height()]:[u.height(),u.width()],y=/([0-9]+)%/.exec(f);y&&(f=parseInt(y[1],10)/100*e[r=="hide"?0:1]),r=="show"&&u.css(c?{height:0,width:f}:{height:f,width:0}),o={},s={},o[v[0]]=r=="show"?e[0]:f,s[v[1]]=r=="show"?e[1]:0,u.animate(o,l,t.options.easing).animate(s,l,t.options.easing,function(){r=="hide"&&i.hide(),n.effects.restore(i,h),n.effects.removeWrapper(i),t.callback&&t.callback.apply(i[0],arguments),i.dequeue()})})}}(n),function(n){n.effects.puff=function(t){return this.queue(function(){var i=n(this),r=n.extend(!0,{},t.options),f=n.effects.setMode(i,t.options.mode||"hide"),o=parseInt(t.options.percent,10)||150,u,e;r.fade=!0,u={height:i.height(),width:i.width()},e=o/100,i.from=f=="hide"?u:{height:u.height*e,width:u.width*e},r.from=i.from,r.percent=f=="hide"?o:100,r.mode=f,i.effect("scale",r,t.duration,t.callback),i.dequeue()})},n.effects.scale=function(t){return this.queue(function(){var i=n(this),r=n.extend(!0,{},t.options),u=n.effects.setMode(i,t.options.mode||"effect"),o=parseInt(t.options.percent,10)||(parseInt(t.options.percent,10)==0?0:u=="hide"?0:100),s=t.options.direction||"both",h=t.options.origin,f,e;u!="effect"&&(r.origin=h||["middle","center"],r.restore=!0),f={height:i.height(),width:i.width()},i.from=t.options.from||(u=="show"?{height:0,width:0}:f),e={y:s!="horizontal"?o/100:1,x:s!="vertical"?o/100:1},i.to={height:f.height*e.y,width:f.width*e.x},t.options.fade&&(u=="show"&&(i.from.opacity=0,i.to.opacity=1),u=="hide"&&(i.from.opacity=1,i.to.opacity=0)),r.from=i.from,r.to=i.to,r.mode=u,i.effect("size",r,t.duration,t.callback),i.dequeue()})},n.effects.size=function(t){return this.queue(function(){var i=n(this),f=["position","top","left","width","height","overflow","opacity"],v=["position","top","left","overflow","opacity"],a=["width","height","overflow"],c=["fontSize"],e=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],o=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=n.effects.setMode(i,t.options.mode||"effect"),l=t.options.restore||!1,s=t.options.scale||"both",y=t.options.origin,u={height:i.height(),width:i.width()},h,r;i.from=t.options.from||u,i.to=t.options.to||u,y&&(h=n.effects.getBaseline(y,u),i.from.top=(u.height-i.from.height)*h.y,i.from.left=(u.width-i.from.width)*h.x,i.to.top=(u.height-i.to.height)*h.y,i.to.left=(u.width-i.to.width)*h.x),r={from:{y:i.from.height/u.height,x:i.from.width/u.width},to:{y:i.to.height/u.height,x:i.to.width/u.width}},(s=="box"||s=="both")&&(r.from.y!=r.to.y&&(f=f.concat(e),i.from=n.effects.setTransition(i,e,r.from.y,i.from),i.to=n.effects.setTransition(i,e,r.to.y,i.to)),r.from.x!=r.to.x&&(f=f.concat(o),i.from=n.effects.setTransition(i,o,r.from.x,i.from),i.to=n.effects.setTransition(i,o,r.to.x,i.to))),(s=="content"||s=="both")&&r.from.y!=r.to.y&&(f=f.concat(c),i.from=n.effects.setTransition(i,c,r.from.y,i.from),i.to=n.effects.setTransition(i,c,r.to.y,i.to)),n.effects.save(i,l?f:v),i.show(),n.effects.createWrapper(i),i.css("overflow","hidden").css(i.from),(s=="content"||s=="both")&&(e=e.concat(["marginTop","marginBottom"]).concat(c),o=o.concat(["marginLeft","marginRight"]),a=f.concat(e).concat(o),i.find("*[width]").each(function(){child=n(this),l&&n.effects.save(child,a);var i={height:child.height(),width:child.width()};child.from={height:i.height*r.from.y,width:i.width*r.from.x},child.to={height:i.height*r.to.y,width:i.width*r.to.x},r.from.y!=r.to.y&&(child.from=n.effects.setTransition(child,e,r.from.y,child.from),child.to=n.effects.setTransition(child,e,r.to.y,child.to)),r.from.x!=r.to.x&&(child.from=n.effects.setTransition(child,o,r.from.x,child.from),child.to=n.effects.setTransition(child,o,r.to.x,child.to)),child.css(child.from),child.animate(child.to,t.duration,t.options.easing,function(){l&&n.effects.restore(child,a)})})),i.animate(i.to,{queue:!1,duration:t.duration,easing:t.options.easing,complete:function(){p=="hide"&&i.hide(),n.effects.restore(i,l?f:v),n.effects.removeWrapper(i),t.callback&&t.callback.apply(this,arguments),i.dequeue()}})})}}(n),function(n){n.effects.slide=function(t){return this.queue(function(){var i=n(this),h=["position","top","left"],u=n.effects.setMode(i,t.options.mode||"show"),r=t.options.direction||"left",s;n.effects.save(i,h),i.show(),n.effects.createWrapper(i).css({overflow:"hidden"});var f=r=="up"||r=="down"?"top":"left",e=r=="up"||r=="left"?"pos":"neg",o=t.options.distance||(f=="top"?i.outerHeight({margin:!0}):i.outerWidth({margin:!0}));u=="show"&&i.css(f,e=="pos"?-o:o),s={},s[f]=(u=="show"?e=="pos"?"+=":"-=":e=="pos"?"-=":"+=")+o,i.animate(s,{queue:!1,duration:t.duration,easing:t.options.easing,complete:function(){u=="hide"&&i.hide(),n.effects.restore(i,h),n.effects.removeWrapper(i),t.callback&&t.callback.apply(this,arguments),i.dequeue()}})})}}(n)})(DDRjQuery),window.DDR||(window.DDR={}),DDR.Menu||(DDR.Menu={}),DDR.Menu.Providers||(DDR.Menu.Providers={}),DDRjQuery(function(n){DDR.Menu.Providers.BaseRenderer=function(){},DDR.Menu.Providers.BaseRenderer.prototype.baseConstructor=function(t,i){var r=this;r.jqContainer=t,r.dnnNavParams=i,r.menus=t.find("ul").toDDRObjectArray(),r.items=t.find("li").toDDRObjectArray(),r.subMenus=r.menus.filter(function(n){return n.level>0}),r.rootItems=r.items.filter(function(n){return n.level==0}),r.jqRootMenu=t.children("ul"),r.rootMenu=r.jqRootMenu.toDDRObject(),r.clientID=t[0].id,r.showEffect=i.effect||"slide",r.showEffectOptions=JSON.parse(i.effectOptions)||{},r.showEffectSpeed=i.effectSpeed||200,r.orientHorizontal=i.ControlOrientation!="Vertical",r.useShim=n.browser.msie&&i.shim},DDR.Menu.Providers.BaseRenderer.prototype.mapToRendered=function(t){return n(n.map(t.get(),function(t){return n(t).ddrData().rendered.get(0)}))},DDR.Menu.Providers.BaseRenderer.prototype.addCovering=function(){var i=this,t=[],r=n("<\/a>").css("background",n.browser.msie?"url("+i.dnnNavParams.PathSystemImage+"spacer.gif)":"transparent"),u=n.browser.msie&&n.browser.version.startsWith("6.")||!n.support.boxModel;i.menus.each(function(i){i.coverings=[],i.childItems.each(function(f){if(f.coveringHere){var e=f.rendered.text();f.coveringHere().each(function(){var o=n(this),s,c,h;o.css("position","relative"),s=r.clone(),s.attr("href",f.href||"javascript:void(0)").children("span").text(e),o.prepend(s),s=o.children("a:first"),c=s.offsetParent(),h=o[0],h===c[0]||h.offsetParent===null?u&&s.css({left:"",right:"",width:o.outerWidth(!1)+"px",height:o.outerHeight(!1)+"px"}):(h.moveCovering=!0,h.covering=s,h.coveringHere=o,s.css({width:"0",height:"0"}),o.bind("mouseenter",function(){i.coverings.each(function(n){var t,i;n.moveCovering&&(t=n.coveringHere.offset(),n.covering.css({top:"0",left:"0"}),i=n.covering.offset(),n.covering.css({top:t.top-i.top+"px",left:t.left-i.left+"px",width:n.coveringHere.outerWidth(!1)+"px",height:n.coveringHere.outerHeight(!1)+"px"}),n.moveCovering=!1)})}),i.coverings.push(h),t.push(h))})}})}),t.length>0&&n(window).resize(function(){t.each(function(n){n.covering.css({width:"0",height:"0"}),n.moveCovering=!0})})},DDR.Menu.Providers.BaseRenderer.prototype.prepareHideAndShow=function(){var n=this;n.hideAllMenus=n.menus.filter(function(n){return n.flyout}),n.setItemsHideAndShow(),n.attachEvents(),n.closeUp()},DDR.Menu.Providers.BaseRenderer.prototype.setItemsHideAndShow=function(){var n=this;n.items.each(function(t){var r=t.allParentMenus,u=t.allChildMenus,i=t.childMenu,f=n.subMenus.filter(function(n){return n.flyout&&!(r.contains(n)||u.contains(n))});t.hideThese=f,t.showThese=[],i&&i.flyout&&(t.showThese[0]=i)})},DDR.Menu.Providers.BaseRenderer.prototype.attachEvents=function(){var t=this;t.menus.each(function(i){if(i.flyout){var r=i.rendered,u=r[0];u.hideMenu=function(){r.stop(!0,!0),this.style.display!="none"&&(r.hide(),i.shim&&i.shim.hide())},u.showMenu=function(u){this.style.display=="none"&&(r.stop(!0,!0),i.childItems.allRendered().stop(!0,!0).unbind("mouseenter mouseleave"),t.positionMenu(i),u||t.showEffectSpeed==0?r.show():t.showEffect=="none"?(r.queue(function(){setTimeout(function(){r.dequeue()},t.showEffectSpeed)}),r.show(1)):t.showEffect=="fade"?r.fadeIn(t.showEffectSpeed):(t.showEffectOptions.direction=t.showEffect=="slide"||t.showEffect=="drop"?i.slideDirection:i.blindDirection,r.show(t.showEffect,t.showEffectOptions,t.showEffectSpeed)),r.queue(function(){r.css("display","none").css("display","block"),i.childItems.each(function(n){t.menuItemHover&&t.menuItemHover(n),n.rendered.hover(function(){n.hideThese.allRendered().each(function(){this.hideMenu()}),n.showThese.allRendered().each(function(){this.showMenu()})},function(){}),n.rendered.focus(function(){n.hideThese.allRendered().each(function(){this.hideMenu()}),n.showThese.allRendered().each(function(){this.showMenu(!0)})})}),n(this).dequeue()}))}}else i.childItems.each(function(n){t.menuItemHover&&t.menuItemHover(n),n.rendered.hover(function(){n.hideThese.allRendered().each(function(){this.hideMenu()}),n.showThese.allRendered().each(function(){this.showMenu()})},function(){}),n.rendered.focus(function(){n.hideThese.allRendered().each(function(){this.hideMenu()}),n.showThese.allRendered().each(function(){this.showMenu(!0)})})});i.rendered.mouseover(function(){t.timeoutID&&(window.clearTimeout(t.timeoutID),t.timeoutID=null)}),i.rendered.mouseout(function(){t.timeoutID||(t.timeoutID=window.setTimeout(function(){t.closeUp()},400))}),i.rendered.mouseover(function(){t.timeoutID&&(window.clearTimeout(t.timeoutID),t.timeoutID=null)}),i.rendered.mouseout(function(){t.timeoutID||(t.timeoutID=window.setTimeout(function(){t.closeUp()},400))})})},DDR.Menu.Providers.BaseRenderer.prototype.positionMenu=function(t){if(t.childItems&&t.childItems.length>0){var l=this,s=t.level,h=t.parentItem.parentMenu,a=t.parentItem,r=t.layout.match(/,menu$/)?h.rendered:a.rendered,i=t.rendered,e=n(window),c=e.scrollLeft(),u=c+e.width(),o=e.scrollTop(),f=o+e.height(),v=i.css("display");i.css({display:"block",width:"auto",height:"auto","overflow-x":"visible","overflow-y":"visible","z-index":1e4+s*3}),i.width(i.width()),i.height(i.height()),h.layout.match(/^horizontal/)?(t.slideDirection="up",t.blindDirection="vertical",i.alignElement(function(){return t.childItems[0].rendered.getLeft(2)},r.getLeft(2),function(){return i.getTop(1)},r.getBottom(1)),i.getRight(3)>u&&i.alignHorizontal(function(){return i.getRight(3)},u),i.getBottom(3)>f&&(i.alignVertical(function(){return i.getBottom(1)},r.getTop(1)),t.slideDirection="down"),i.getTop(3)u&&i.alignHorizontal(function(){return i.getRight(3)},u),t.slideDirection="up"):(i.alignElement(function(){return t.childItems[0].rendered.getLeft(2)},r.getLeft(2),function(){return i.getTop(1)},r.getBottom(1)),i.getRight(3)>u&&i.alignHorizontal(function(){return i.getRight(3)},u),i.getBottom(3)>f&&(i.alignVertical(function(){return i.getBottom(1)},r.getTop(1)),t.slideDirection="down"),i.getTop(3)u&&i.alignHorizontal(function(){return i.getRight(3)},u),t.slideDirection="up"))):(t.slideDirection="left",t.blindDirection="horizontal",i.alignElement(function(){return i.getLeft(1)},r.getRight(1),function(){return t.childItems[0].rendered.getTop(2)},r.getTop(2)),i.getBottom(3)>f&&i.alignVertical(function(){return i.getBottom(3)},f),i.getRight(3)>u&&(i.alignHorizontal(function(){return i.getRight(1)},r.getLeft(1)),t.slideDirection="right"),i.getLeft(3)").css({position:"absolute","z-index":9999+s*3,"background-color":"transparent",filter:"progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)"}).appendTo(n(document.body))),t.shim.css({top:i.css("top"),left:i.css("left"),width:i.outerWidth(!0)+"px",height:i.outerHeight(!0)+"px",display:"block"})),i.css("display",v)}},DDR.Menu.Providers.BaseRenderer.prototype.closeUp=function(){var n=this;n.hideAllMenus.allRendered().each(function(){this.hideMenu()})},DDR.Menu.getCSS=function(t,i){return parseFloat("0"+n.css(t,i,!0))},Array.prototype.each||(Array.prototype.each=function(n){for(var t=0;t=3&&(t+=DDR.Menu.getCSS(i,"borderLeftWidth")),n>=4&&(t+=DDR.Menu.getCSS(i,"paddingLeft")),t},getTop:function(n){var r=this,t=r.offset().top,i=r[0];return n==1&&(t-=DDR.Menu.getCSS(i,"marginTop")),n>=3&&(t+=DDR.Menu.getCSS(i,"borderTopWidth")),n>=4&&(t+=DDR.Menu.getCSS(i,"paddingTop")),t},getRight:function(n){var t=this.getLeft(4)+this.width(),i=this[0];return n<4&&(t+=DDR.Menu.getCSS(i,"paddingRight")),n<3&&(t+=DDR.Menu.getCSS(i,"borderRightWidth")),n<2&&(t+=DDR.Menu.getCSS(i,"marginRight")),t},getBottom:function(n){var t=this.getTop(4)+this.height(),i=this[0];return n<4&&(t+=DDR.Menu.getCSS(i,"paddingBottom")),n<3&&(t+=DDR.Menu.getCSS(i,"borderBottomWidth")),n<2&&(t+=DDR.Menu.getCSS(i,"marginBottom")),t},alignElement:function(t,i,r,u){return this.each(function(){var f=n(this),e=f.css("display");f.css({left:"-999px",top:"-999px",position:"absolute",display:"block"}),i-=999+t(),u-=999+r(),e!="block"&&f.css("display",e),f.css({left:i+"px",top:u+"px"})})},alignHorizontal:function(t,i){return this.each(function(){var r=n(this),u=r.css("display");r.css({left:"-999px",position:"absolute",display:"block"}),i-=999+t(),u!="block"&&r.css("display",u),r.css({left:i+"px"})})},alignVertical:function(t,i){return this.each(function(){var r=n(this),u=r.css("display");r.css({top:"-999px",position:"absolute",display:"block"}),i-=999+t(),u!="block"&&r.css("display",u),r.css({top:i+"px"})})},sizeElement:function(t,i,r,u){return this.each(function(){var f=n(this),e=f.css("display");f.css({width:"9999px",height:"9999px",display:"block"}),i+=9999-t(),u+=9999-r(),e!="block"&&f.css("display",e),f.css({width:i+"px",height:u+"px"})})},setContentWidth:function(t){return this.each(function(){var i=n(this);i.width(n.support.boxModel?t:t+i.fullWidth()-i.width())})},setContentHeight:function(t){return this.each(function(){var i=n(this);i.height(n.support.boxModel?t:t+i.fullHeight()-i.height())})},setFullWidth:function(t){return this.each(function(){var i=n(this),r=i.width()-i.fullWidth(),u,f;for(i.width(t+r),u=0;u<100;u++){if(f=i.fullWidth(),f==t)return;r+=t-f,i.width(t+r)}})},setFullHeight:function(t){return this.each(function(){var i=n(this),r=i.height()-i.fullHeight(),u,f;for(i.height(t+r),u=0;u<100;u++){if(f=i.fullHeight(),f==t)return;r+=t-f,i.height(t+r)}})},fullWidth:function(){var t=0;return this.each(function(){var i=n(this).outerWidth(!0);i>t&&(t=i)}),t},fullHeight:function(){var t=0;return this.each(function(){var i=n(this).outerHeight(!0);i>t&&(t=i)}),t},totalWidth:function(){var t=0;return this.each(function(){t+=n(this).outerWidth(!0)}),t},totalHeight:function(){var t=0;return this.each(function(){t+=n(this).outerHeight(!0)}),t},matchWidths:function(){return this.setFullWidth(this.fullWidth())},matchHeights:function(){return this.setFullHeight(this.fullHeight())},lineUpHorizontal:function(t){var r=n(this[0]),f=left=r.getLeft(1),u=r.getTop(2),i=0;return this.each(function(){var r=n(this);r.alignElement(function(){return r.getLeft(1)},left,function(){return r.getTop(2)},u),left+=r.fullWidth(),i++,i==t&&(i=0,left=f,u+=r.fullHeight())})},lineUpVertical:function(){var t=n(this[0]),r=t.getLeft(2),i=t.getTop(1);return this.each(function(){var t=n(this);t.alignElement(function(){return t.getLeft(2)},r,function(){return t.getTop(1)},i),i+=t.fullHeight()})},fitToContent:function(){return this.each(function(){var i=-999999,r=-999999,t=n(this),u=t.css("display");t.css({display:"block"}),t.children().each(function(){var t=n(this),u=t.getRight(1),f=t.getBottom(1);u>i&&(i=u),f>r&&(r=f)}),t.sizeElement(function(){return t.getRight(4)},i,function(){return t.getBottom(4)},r),u!="block"&&t.css("display",u)})},stretchBlockHyperlinks:function(){this.find("a").each(function(){var t=n(this);t.css("display")=="block"&&(t.setFullWidth(t.parent().width()),t.setFullHeight(t.parent().height()))})},setHoverClass:function(t,i,r){return this.each(function(){if(t){var f=n(this),u=i||f;f.hover(function(){u.each(function(){this.hoverClass||(this.hoverClass=this.className)}),u.addClass(t),r&&u.removeClass(r)},function(){u.each(function(){this.hoverClass&&(this.className=this.hoverClass)})})}})},ddrData:function(){return this.data("ddrData")||this.data("ddrData",{}),this.data("ddrData")},setMenuData:function(t,i){return this.each(function(){var r=n(this),u=r.children("li"),e=r.attr("nid"),f="";i!=null&&(f=i.data("ddrData").path+"-"),r.data("ddrData",{isMenu:!0,id:e,level:t,path:f,childItems:u,parentItem:i,rendered:r,itemIndex:0}),u.setItemData(t,r),u.length>0&&(r.children("li:first").ddrData().first=!0,r.children("li:last").ddrData().last=!0)})},setItemData:function(t,i){this.each(function(){var r=n(this),f=r.children("ul"),o=r.find("ul"),s=r.parents("ul"),u=r.children("a"),e=u.length?u:r,h=e.children("img"),c=e.children("span"),l=r.attr("nid"),a=i.data("ddrData").path+i.data("ddrData").itemIndex++;r.data("ddrData",{isItem:!0,id:l,level:t,path:a,first:!1,last:!1,href:u.attr("href"),image:h.attr("src"),title:c.text(),isBreadcrumb:r.hasClass("breadcrumb"),isSelected:r.hasClass("selected"),isSeparator:r.hasClass("separator"),childMenu:f,parentMenu:i,allChildMenus:o,allParentMenus:s,rendered:r}),f.setMenuData(t+1,r)})},toDDRObject:function(){var t,i,r,n;if(this.data("ddrObject"))return this.data("ddrObject");t=this.ddrData(),i={},this.data("ddrObject",i),r=0;for(n in t)i[n]=n!="rendered"&&t[n]&&t[n].jquery?n.substr(n.length-1)=="s"?t[n].toDDRObjectArray():t[n].length==0?null:t[n].toDDRObject():t[n],r++;return i},toDDRObjectArray:function(){return n.map(this,function(t){return n(t).toDDRObject()})}}),DDR.Menu.createTable=function(){return n("
").attr("cellpadding",0).attr("cellspacing",0).attr("border",0)},DDR.Menu.addTableCell=function(t,i){var r=t.find("tr:first"),u;return r.length==0&&t.append(r=n("")),u=n("");u.append(r),e.each(function(){var t=n(this);r.append(t),r.children("td").length==i&&(r=n(""),u.append(r))}),r.children("td").length==0&&r.remove(),f.remove()}}),DDR.Menu.registerMenu=function(n,t){document.write("" + ); + + DDRjQuery(function ($) { + var jqContainer = $("#" + clientID); + var rootMenu = jqContainer.children("ul"); + + jqContainer.hide(); + while ((rootMenu.children("li").length == 0) && (rootMenu.children().length > 0)) { + rootMenu.html(rootMenu.children().html()); + } + if (rootMenu.children().length > 0) { + rootMenu.setMenuData(0, null); + + new DDR.Menu.Providers[dnnNavParams.MenuStyle](jqContainer, dnnNavParams).createRootMenu(); + + jqContainer.css({ "display": "block" }); + } + }); +} diff --git a/DNN Platform/Modules/NewDDRMenu/js/jquery.effects.js b/DNN Platform/Modules/NewDDRMenu/js/jquery.effects.js new file mode 100644 index 00000000000..b0d13187e87 --- /dev/null +++ b/DNN Platform/Modules/NewDDRMenu/js/jquery.effects.js @@ -0,0 +1,1069 @@ +(function(jQuery) { + /* + * jQuery UI Effects 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/ + */ + ; jQuery.effects || (function($) { + + $.effects = { + version: "1.7.2", + + // Saves a set of properties in a data storage + save: function(element, set) { + for (var i = 0; i < set.length; i++) { + if (set[i] !== null) element.data("ec.storage." + set[i], element[0].style[set[i]]); + } + }, + + // Restores a set of previously saved properties from a data storage + restore: function(element, set) { + for (var i = 0; i < set.length; i++) { + if (set[i] !== null) element.css(set[i], element.data("ec.storage." + set[i])); + } + }, + + setMode: function(el, mode) { + if (mode == 'toggle') mode = el.is(':hidden') ? 'show' : 'hide'; // Set for toggle + return mode; + }, + + getBaseline: function(origin, original) { // Translates a [top,left] array into a baseline value + // this should be a little more flexible in the future to handle a string & hash + var y, x; + switch (origin[0]) { + case 'top': y = 0; break; + case 'middle': y = 0.5; break; + case 'bottom': y = 1; break; + default: y = origin[0] / original.height; + }; + switch (origin[1]) { + case 'left': x = 0; break; + case 'center': x = 0.5; break; + case 'right': x = 1; break; + default: x = origin[1] / original.width; + }; + return { x: x, y: y }; + }, + + // Wraps the element around a wrapper that copies position properties + createWrapper: function(element) { + + //if the element is already wrapped, return it + if (element.parent().is('.ui-effects-wrapper')) + return element.parent(); + + //Cache width,height and float properties of the element, and create a wrapper around it + var props = { width: element.outerWidth(true), height: element.outerHeight(true), 'float': element.css('float') }; + element.wrap('
'); + var wrapper = element.parent(); + + //Transfer the positioning of the element to the wrapper + if (element.css('position') == 'static') { + wrapper.css({ position: 'relative' }); + element.css({ position: 'relative' }); + } else { + var top = element.css('top'); if (isNaN(parseInt(top, 10))) top = 'auto'; + var left = element.css('left'); if (isNaN(parseInt(left, 10))) left = 'auto'; + wrapper.css({ position: element.css('position'), top: top, left: left, zIndex: element.css('z-index') }).show(); + element.css({ position: 'relative', top: 0, left: 0 }); + } + + wrapper.css(props); + return wrapper; + }, + + removeWrapper: function(element) { + if (element.parent().is('.ui-effects-wrapper')) + return element.parent().replaceWith(element); + return element; + }, + + setTransition: function(element, list, factor, value) { + value = value || {}; + $.each(list, function(i, x) { + unit = element.cssUnit(x); + if (unit[0] > 0) value[x] = unit[0] * factor + unit[1]; + }); + return value; + }, + + //Base function to animate from one class to another in a seamless transition + animateClass: function(value, duration, easing, callback) { + + var cb = (typeof easing == "function" ? easing : (callback ? callback : null)); + var ea = (typeof easing == "string" ? easing : null); + + return this.each(function() { + + var offset = {}; var that = $(this); var oldStyleAttr = that.attr("style") || ''; + if (typeof oldStyleAttr == 'object') oldStyleAttr = oldStyleAttr["cssText"]; /* Stupidly in IE, style is a object.. */ + if (value.toggle) { that.hasClass(value.toggle) ? value.remove = value.toggle : value.add = value.toggle; } + + //Let's get a style offset + var oldStyle = $.extend({}, (document.defaultView ? document.defaultView.getComputedStyle(this, null) : this.currentStyle)); + if (value.add) that.addClass(value.add); if (value.remove) that.removeClass(value.remove); + var newStyle = $.extend({}, (document.defaultView ? document.defaultView.getComputedStyle(this, null) : this.currentStyle)); + if (value.add) that.removeClass(value.add); if (value.remove) that.addClass(value.remove); + + // The main function to form the object for animation + for (var n in newStyle) { + if (typeof newStyle[n] != "function" && newStyle[n] /* No functions and null properties */ + && n.indexOf("Moz") == -1 && n.indexOf("length") == -1 /* No mozilla spezific render properties. */ + && newStyle[n] != oldStyle[n] /* Only values that have changed are used for the animation */ + && (n.match(/color/i) || (!n.match(/color/i) && !isNaN(parseInt(newStyle[n], 10)))) /* Only things that can be parsed to integers or colors */ + && (oldStyle.position != "static" || (oldStyle.position == "static" && !n.match(/left|top|bottom|right/))) /* No need for positions when dealing with static positions */ + ) offset[n] = newStyle[n]; + } + + that.animate(offset, duration, ea, function() { // Animate the newly constructed offset object + // Change style attribute back to original. For stupid IE, we need to clear the damn object. + if (typeof $(this).attr("style") == 'object') { $(this).attr("style")["cssText"] = ""; $(this).attr("style")["cssText"] = oldStyleAttr; } else $(this).attr("style", oldStyleAttr); + if (value.add) $(this).addClass(value.add); if (value.remove) $(this).removeClass(value.remove); + if (cb) cb.apply(this, arguments); + }); + + }); + } + }; + + + function _normalizeArguments(a, m) { + + var o = a[1] && a[1].constructor == Object ? a[1] : {}; if (m) o.mode = m; + var speed = a[1] && a[1].constructor != Object ? a[1] : (o.duration ? o.duration : a[2]); //either comes from options.duration or the secon/third argument + speed = $.fx.off ? 0 : typeof speed === "number" ? speed : $.fx.speeds[speed] || $.fx.speeds._default; + var callback = o.callback || ($.isFunction(a[1]) && a[1]) || ($.isFunction(a[2]) && a[2]) || ($.isFunction(a[3]) && a[3]); + + return [a[0], o, speed, callback]; + + } + + //Extend the methods of jQuery + $.fn.extend({ + + //Save old methods + _show: $.fn.show, + _hide: $.fn.hide, + __toggle: $.fn.toggle, + _addClass: $.fn.addClass, + _removeClass: $.fn.removeClass, + _toggleClass: $.fn.toggleClass, + + // New effect methods + effect: function(fx, options, speed, callback) { + return $.effects[fx] ? $.effects[fx].call(this, { method: fx, options: options || {}, duration: speed, callback: callback }) : null; + }, + + show: function() { + if (!arguments[0] || (arguments[0].constructor == Number || (/(slow|normal|fast)/).test(arguments[0]))) + return this._show.apply(this, arguments); + else { + return this.effect.apply(this, _normalizeArguments(arguments, 'show')); + } + }, + + hide: function() { + if (!arguments[0] || (arguments[0].constructor == Number || (/(slow|normal|fast)/).test(arguments[0]))) + return this._hide.apply(this, arguments); + else { + return this.effect.apply(this, _normalizeArguments(arguments, 'hide')); + } + }, + + toggle: function() { + if (!arguments[0] || + (arguments[0].constructor == Number || (/(slow|normal|fast)/).test(arguments[0])) || + ($.isFunction(arguments[0]) || typeof arguments[0] == 'boolean')) { + return this.__toggle.apply(this, arguments); + } else { + return this.effect.apply(this, _normalizeArguments(arguments, 'toggle')); + } + }, + + addClass: function(classNames, speed, easing, callback) { + return speed ? $.effects.animateClass.apply(this, [{ add: classNames }, speed, easing, callback]) : this._addClass(classNames); + }, + removeClass: function(classNames, speed, easing, callback) { + return speed ? $.effects.animateClass.apply(this, [{ remove: classNames }, speed, easing, callback]) : this._removeClass(classNames); + }, + toggleClass: function(classNames, speed, easing, callback) { + return ((typeof speed !== "boolean") && speed) ? $.effects.animateClass.apply(this, [{ toggle: classNames }, speed, easing, callback]) : this._toggleClass(classNames, speed); + }, + morph: function(remove, add, speed, easing, callback) { + return $.effects.animateClass.apply(this, [{ add: add, remove: remove }, speed, easing, callback]); + }, + switchClass: function() { + return this.morph.apply(this, arguments); + }, + + // helper functions + cssUnit: function(key) { + var style = this.css(key), val = []; + $.each(['em', 'px', '%', 'pt'], function(i, unit) { + if (style.indexOf(unit) > 0) + val = [parseFloat(style), unit]; + }); + return val; + } + }); + + /* + * jQuery Color Animations + * Copyright 2007 John Resig + * Released under the MIT and GPL licenses. + */ + + // We override the animation for all of these color styles + $.each(['backgroundColor', 'borderBottomColor', 'borderLeftColor', 'borderRightColor', 'borderTopColor', 'color', 'outlineColor'], function(i, attr) { + $.fx.step[attr] = function(fx) { + if (fx.state == 0) { + fx.start = getColor(fx.elem, attr); + fx.end = getRGB(fx.end); + } + + fx.elem.style[attr] = "rgb(" + [ + Math.max(Math.min(parseInt((fx.pos * (fx.end[0] - fx.start[0])) + fx.start[0], 10), 255), 0), + Math.max(Math.min(parseInt((fx.pos * (fx.end[1] - fx.start[1])) + fx.start[1], 10), 255), 0), + Math.max(Math.min(parseInt((fx.pos * (fx.end[2] - fx.start[2])) + fx.start[2], 10), 255), 0) + ].join(",") + ")"; + }; + }); + + // Color Conversion functions from highlightFade + // By Blair Mitchelmore + // http://jquery.offput.ca/highlightFade/ + + // Parse strings looking for color tuples [255,255,255] + function getRGB(color) { + var result; + + // Check if we're already dealing with an array of colors + if (color && color.constructor == Array && color.length == 3) + return color; + + // Look for rgb(num,num,num) + if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(color)) + return [parseInt(result[1], 10), parseInt(result[2], 10), parseInt(result[3], 10)]; + + // Look for rgb(num%,num%,num%) + if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(color)) + return [parseFloat(result[1]) * 2.55, parseFloat(result[2]) * 2.55, parseFloat(result[3]) * 2.55]; + + // Look for #a0b1c2 + if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)) + return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]; + + // Look for #fff + if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)) + return [parseInt(result[1] + result[1], 16), parseInt(result[2] + result[2], 16), parseInt(result[3] + result[3], 16)]; + + // Look for rgba(0, 0, 0, 0) == transparent in Safari 3 + if (result = /rgba\(0, 0, 0, 0\)/.exec(color)) + return colors['transparent']; + + // Otherwise, we're most likely dealing with a named color + return colors[$.trim(color).toLowerCase()]; + } + + function getColor(elem, attr) { + var color; + + do { + color = $(elem).css(attr); + + // Keep going until we find an element that has color, or we hit the body + if (color != '' && color != 'transparent' || $.nodeName(elem, "body")) + break; + + attr = "backgroundColor"; + } while (elem = elem.parentNode); + + return getRGB(color); + }; + + // Some named colors to work with + // From Interface by Stefan Petre + // http://interface.eyecon.ro/ + + var colors = { + aqua: [0, 255, 255], + azure: [240, 255, 255], + beige: [245, 245, 220], + black: [0, 0, 0], + blue: [0, 0, 255], + brown: [165, 42, 42], + cyan: [0, 255, 255], + darkblue: [0, 0, 139], + darkcyan: [0, 139, 139], + darkgrey: [169, 169, 169], + darkgreen: [0, 100, 0], + darkkhaki: [189, 183, 107], + darkmagenta: [139, 0, 139], + darkolivegreen: [85, 107, 47], + darkorange: [255, 140, 0], + darkorchid: [153, 50, 204], + darkred: [139, 0, 0], + darksalmon: [233, 150, 122], + darkviolet: [148, 0, 211], + fuchsia: [255, 0, 255], + gold: [255, 215, 0], + green: [0, 128, 0], + indigo: [75, 0, 130], + khaki: [240, 230, 140], + lightblue: [173, 216, 230], + lightcyan: [224, 255, 255], + lightgreen: [144, 238, 144], + lightgrey: [211, 211, 211], + lightpink: [255, 182, 193], + lightyellow: [255, 255, 224], + lime: [0, 255, 0], + magenta: [255, 0, 255], + maroon: [128, 0, 0], + navy: [0, 0, 128], + olive: [128, 128, 0], + orange: [255, 165, 0], + pink: [255, 192, 203], + purple: [128, 0, 128], + violet: [128, 0, 128], + red: [255, 0, 0], + silver: [192, 192, 192], + white: [255, 255, 255], + yellow: [255, 255, 0], + transparent: [255, 255, 255] + }; + + /* + * jQuery Easing v1.3 - http://gsgd.co.uk/sandbox/jquery/easing/ + * + * Uses the built in easing capabilities added In jQuery 1.1 + * to offer multiple easing options + * + * TERMS OF USE - jQuery Easing + * + * Open source under the BSD License. + * + * Copyright 2008 George McGinley Smith + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * Neither the name of the author nor the names of contributors may be used to endorse + * or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + + // t: current time, b: begInnIng value, c: change In value, d: duration + $.easing.jswing = $.easing.swing; + + $.extend($.easing, +{ + def: 'easeOutQuad', + swing: function(x, t, b, c, d) { + //alert($.easing.default); + return $.easing[$.easing.def](x, t, b, c, d); + }, + easeInQuad: function(x, t, b, c, d) { + return c * (t /= d) * t + b; + }, + easeOutQuad: function(x, t, b, c, d) { + return -c * (t /= d) * (t - 2) + b; + }, + easeInOutQuad: function(x, t, b, c, d) { + if ((t /= d / 2) < 1) return c / 2 * t * t + b; + return -c / 2 * ((--t) * (t - 2) - 1) + b; + }, + easeInCubic: function(x, t, b, c, d) { + return c * (t /= d) * t * t + b; + }, + easeOutCubic: function(x, t, b, c, d) { + return c * ((t = t / d - 1) * t * t + 1) + b; + }, + easeInOutCubic: function(x, t, b, c, d) { + if ((t /= d / 2) < 1) return c / 2 * t * t * t + b; + return c / 2 * ((t -= 2) * t * t + 2) + b; + }, + easeInQuart: function(x, t, b, c, d) { + return c * (t /= d) * t * t * t + b; + }, + easeOutQuart: function(x, t, b, c, d) { + return -c * ((t = t / d - 1) * t * t * t - 1) + b; + }, + easeInOutQuart: function(x, t, b, c, d) { + if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b; + return -c / 2 * ((t -= 2) * t * t * t - 2) + b; + }, + easeInQuint: function(x, t, b, c, d) { + return c * (t /= d) * t * t * t * t + b; + }, + easeOutQuint: function(x, t, b, c, d) { + return c * ((t = t / d - 1) * t * t * t * t + 1) + b; + }, + easeInOutQuint: function(x, t, b, c, d) { + if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b; + return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; + }, + easeInSine: function(x, t, b, c, d) { + return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; + }, + easeOutSine: function(x, t, b, c, d) { + return c * Math.sin(t / d * (Math.PI / 2)) + b; + }, + easeInOutSine: function(x, t, b, c, d) { + return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; + }, + easeInExpo: function(x, t, b, c, d) { + return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; + }, + easeOutExpo: function(x, t, b, c, d) { + return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; + }, + easeInOutExpo: function(x, t, b, c, d) { + if (t == 0) return b; + if (t == d) return b + c; + if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b; + return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; + }, + easeInCirc: function(x, t, b, c, d) { + return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; + }, + easeOutCirc: function(x, t, b, c, d) { + return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; + }, + easeInOutCirc: function(x, t, b, c, d) { + if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; + return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; + }, + easeInElastic: function(x, t, b, c, d) { + var s = 1.70158; var p = 0; var a = c; + if (t == 0) return b; if ((t /= d) == 1) return b + c; if (!p) p = d * .3; + if (a < Math.abs(c)) { a = c; var s = p / 4; } + else var s = p / (2 * Math.PI) * Math.asin(c / a); + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; + }, + easeOutElastic: function(x, t, b, c, d) { + var s = 1.70158; var p = 0; var a = c; + if (t == 0) return b; if ((t /= d) == 1) return b + c; if (!p) p = d * .3; + if (a < Math.abs(c)) { a = c; var s = p / 4; } + else var s = p / (2 * Math.PI) * Math.asin(c / a); + return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b; + }, + easeInOutElastic: function(x, t, b, c, d) { + var s = 1.70158; var p = 0; var a = c; + if (t == 0) return b; if ((t /= d / 2) == 2) return b + c; if (!p) p = d * (.3 * 1.5); + if (a < Math.abs(c)) { a = c; var s = p / 4; } + else var s = p / (2 * Math.PI) * Math.asin(c / a); + if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b; + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b; + }, + easeInBack: function(x, t, b, c, d, s) { + if (s == undefined) s = 1.70158; + return c * (t /= d) * t * ((s + 1) * t - s) + b; + }, + easeOutBack: function(x, t, b, c, d, s) { + if (s == undefined) s = 1.70158; + return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; + }, + easeInOutBack: function(x, t, b, c, d, s) { + if (s == undefined) s = 1.70158; + if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; + return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; + }, + easeInBounce: function(x, t, b, c, d) { + return c - $.easing.easeOutBounce(x, d - t, 0, c, d) + b; + }, + easeOutBounce: function(x, t, b, c, d) { + if ((t /= d) < (1 / 2.75)) { + return c * (7.5625 * t * t) + b; + } else if (t < (2 / 2.75)) { + return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b; + } else if (t < (2.5 / 2.75)) { + return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b; + } else { + return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b; + } + }, + easeInOutBounce: function(x, t, b, c, d) { + if (t < d / 2) return $.easing.easeInBounce(x, t * 2, 0, c, d) * .5 + b; + return $.easing.easeOutBounce(x, t * 2 - d, 0, c, d) * .5 + c * .5 + b; + } +}); + + /* + * + * TERMS OF USE - EASING EQUATIONS + * + * Open source under the BSD License. + * + * Copyright 2001 Robert Penner + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * Neither the name of the author nor the names of contributors may be used to endorse + * or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + + })(jQuery); + /* + * jQuery UI Effects Blind 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/Blind + * + * Depends: + * effects.core.js + */ + (function($) { + + $.effects.blind = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this), props = ['position', 'top', 'left']; + + // Set options + var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode + var direction = o.options.direction || 'vertical'; // Default direction + + // Adjust + $.effects.save(el, props); el.show(); // Save & Show + var wrapper = $.effects.createWrapper(el).css({ overflow: 'hidden' }); // Create Wrapper + var ref = (direction == 'vertical') ? 'height' : 'width'; + var distance = (direction == 'vertical') ? wrapper.height() : wrapper.width(); + if (mode == 'show') wrapper.css(ref, 0); // Shift + + // Animation + var animation = {}; + animation[ref] = mode == 'show' ? distance : 0; + + // Animate + wrapper.animate(animation, o.duration, o.options.easing, function() { + if (mode == 'hide') el.hide(); // Hide + $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore + if (o.callback) o.callback.apply(el[0], arguments); // Callback + el.dequeue(); + }); + + }); + + }; + + })(jQuery); + /* + * jQuery UI Effects Clip 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/Clip + * + * Depends: + * effects.core.js + */ + (function($) { + + $.effects.clip = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this), props = ['position', 'top', 'left', 'height', 'width']; + + // Set options + var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode + var direction = o.options.direction || 'vertical'; // Default direction + + // Adjust + $.effects.save(el, props); el.show(); // Save & Show + var wrapper = $.effects.createWrapper(el).css({ overflow: 'hidden' }); // Create Wrapper + var animate = el[0].tagName == 'IMG' ? wrapper : el; + var ref = { + size: (direction == 'vertical') ? 'height' : 'width', + position: (direction == 'vertical') ? 'top' : 'left' + }; + var distance = (direction == 'vertical') ? animate.height() : animate.width(); + if (mode == 'show') { animate.css(ref.size, 0); animate.css(ref.position, distance / 2); } // Shift + + // Animation + var animation = {}; + animation[ref.size] = mode == 'show' ? distance : 0; + animation[ref.position] = mode == 'show' ? 0 : distance / 2; + + // Animate + animate.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { + if (mode == 'hide') el.hide(); // Hide + $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore + if (o.callback) o.callback.apply(el[0], arguments); // Callback + el.dequeue(); + } + }); + + }); + + }; + + })(jQuery); + /* + * jQuery UI Effects Drop 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/Drop + * + * Depends: + * effects.core.js + */ + (function($) { + + $.effects.drop = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this), props = ['position', 'top', 'left', 'opacity']; + + // Set options + var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode + var direction = o.options.direction || 'left'; // Default Direction + + // Adjust + $.effects.save(el, props); el.show(); // Save & Show + $.effects.createWrapper(el); // Create Wrapper + var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left'; + var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg'; + var distance = o.options.distance || (ref == 'top' ? el.outerHeight({ margin: true }) / 2 : el.outerWidth({ margin: true }) / 2); + if (mode == 'show') el.css('opacity', 0).css(ref, motion == 'pos' ? -distance : distance); // Shift + + // Animation + var animation = { opacity: mode == 'show' ? 1 : 0 }; + animation[ref] = (mode == 'show' ? (motion == 'pos' ? '+=' : '-=') : (motion == 'pos' ? '-=' : '+=')) + distance; + + // Animate + el.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { + if (mode == 'hide') el.hide(); // Hide + $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore + if (o.callback) o.callback.apply(this, arguments); // Callback + el.dequeue(); + } + }); + + }); + + }; + + })(jQuery); + /* + * jQuery UI Effects Explode 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/Explode + * + * Depends: + * effects.core.js + */ + (function($) { + + $.effects.explode = function(o) { + + return this.queue(function() { + + var rows = o.options.pieces ? Math.round(Math.sqrt(o.options.pieces)) : 3; + var cells = o.options.pieces ? Math.round(Math.sqrt(o.options.pieces)) : 3; + + o.options.mode = o.options.mode == 'toggle' ? ($(this).is(':visible') ? 'hide' : 'show') : o.options.mode; + var el = $(this).show().css('visibility', 'hidden'); + var offset = el.offset(); + + //Substract the margins - not fixing the problem yet. + offset.top -= parseInt(el.css("marginTop"), 10) || 0; + offset.left -= parseInt(el.css("marginLeft"), 10) || 0; + + var width = el.outerWidth(true); + var height = el.outerHeight(true); + + for (var i = 0; i < rows; i++) { // = + for (var j = 0; j < cells; j++) { // || + el + .clone() + .appendTo('body') + .wrap('
') + .css({ + position: 'absolute', + visibility: 'visible', + left: -j * (width / cells), + top: -i * (height / rows) + }) + .parent() + .addClass('ui-effects-explode') + .css({ + position: 'absolute', + overflow: 'hidden', + width: width / cells, + height: height / rows, + left: offset.left + j * (width / cells) + (o.options.mode == 'show' ? (j - Math.floor(cells / 2)) * (width / cells) : 0), + top: offset.top + i * (height / rows) + (o.options.mode == 'show' ? (i - Math.floor(rows / 2)) * (height / rows) : 0), + opacity: o.options.mode == 'show' ? 0 : 1 + }).animate({ + left: offset.left + j * (width / cells) + (o.options.mode == 'show' ? 0 : (j - Math.floor(cells / 2)) * (width / cells)), + top: offset.top + i * (height / rows) + (o.options.mode == 'show' ? 0 : (i - Math.floor(rows / 2)) * (height / rows)), + opacity: o.options.mode == 'show' ? 1 : 0 + }, o.duration || 500); + } + } + + // Set a timeout, to call the callback approx. when the other animations have finished + setTimeout(function() { + + o.options.mode == 'show' ? el.css({ visibility: 'visible' }) : el.css({ visibility: 'visible' }).hide(); + if (o.callback) o.callback.apply(el[0]); // Callback + el.dequeue(); + + $('div.ui-effects-explode').remove(); + + }, o.duration || 500); + + + }); + + }; + + })(jQuery); + /* + * jQuery UI Effects Fold 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/Fold + * + * Depends: + * effects.core.js + */ + (function($) { + + $.effects.fold = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this), props = ['position', 'top', 'left']; + + // Set options + var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode + var size = o.options.size || 15; // Default fold size + var horizFirst = !(!o.options.horizFirst); // Ensure a boolean value + var duration = o.duration ? o.duration / 2 : $.fx.speeds._default / 2; + + // Adjust + $.effects.save(el, props); el.show(); // Save & Show + var wrapper = $.effects.createWrapper(el).css({ overflow: 'hidden' }); // Create Wrapper + var widthFirst = ((mode == 'show') != horizFirst); + var ref = widthFirst ? ['width', 'height'] : ['height', 'width']; + var distance = widthFirst ? [wrapper.width(), wrapper.height()] : [wrapper.height(), wrapper.width()]; + var percent = /([0-9]+)%/.exec(size); + if (percent) size = parseInt(percent[1], 10) / 100 * distance[mode == 'hide' ? 0 : 1]; + if (mode == 'show') wrapper.css(horizFirst ? { height: 0, width: size} : { height: size, width: 0 }); // Shift + + // Animation + var animation1 = {}, animation2 = {}; + animation1[ref[0]] = mode == 'show' ? distance[0] : size; + animation2[ref[1]] = mode == 'show' ? distance[1] : 0; + + // Animate + wrapper.animate(animation1, duration, o.options.easing) + .animate(animation2, duration, o.options.easing, function() { + if (mode == 'hide') el.hide(); // Hide + $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore + if (o.callback) o.callback.apply(el[0], arguments); // Callback + el.dequeue(); + }); + + }); + + }; + + })(jQuery); + /* + * jQuery UI Effects Scale 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/Scale + * + * Depends: + * effects.core.js + */ + (function($) { + + $.effects.puff = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this); + + // Set options + var options = $.extend(true, {}, o.options); + var mode = $.effects.setMode(el, o.options.mode || 'hide'); // Set Mode + var percent = parseInt(o.options.percent, 10) || 150; // Set default puff percent + options.fade = true; // It's not a puff if it doesn't fade! :) + var original = { height: el.height(), width: el.width() }; // Save original + + // Adjust + var factor = percent / 100; + el.from = (mode == 'hide') ? original : { height: original.height * factor, width: original.width * factor }; + + // Animation + options.from = el.from; + options.percent = (mode == 'hide') ? percent : 100; + options.mode = mode; + + // Animate + el.effect('scale', options, o.duration, o.callback); + el.dequeue(); + }); + + }; + + $.effects.scale = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this); + + // Set options + var options = $.extend(true, {}, o.options); + var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode + var percent = parseInt(o.options.percent, 10) || (parseInt(o.options.percent, 10) == 0 ? 0 : (mode == 'hide' ? 0 : 100)); // Set default scaling percent + var direction = o.options.direction || 'both'; // Set default axis + var origin = o.options.origin; // The origin of the scaling + if (mode != 'effect') { // Set default origin and restore for show/hide + options.origin = origin || ['middle', 'center']; + options.restore = true; + } + var original = { height: el.height(), width: el.width() }; // Save original + el.from = o.options.from || (mode == 'show' ? { height: 0, width: 0} : original); // Default from state + + // Adjust + var factor = { // Set scaling factor + y: direction != 'horizontal' ? (percent / 100) : 1, + x: direction != 'vertical' ? (percent / 100) : 1 + }; + el.to = { height: original.height * factor.y, width: original.width * factor.x }; // Set to state + + if (o.options.fade) { // Fade option to support puff + if (mode == 'show') { el.from.opacity = 0; el.to.opacity = 1; }; + if (mode == 'hide') { el.from.opacity = 1; el.to.opacity = 0; }; + }; + + // Animation + options.from = el.from; options.to = el.to; options.mode = mode; + + // Animate + el.effect('size', options, o.duration, o.callback); + el.dequeue(); + }); + + }; + + $.effects.size = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this), props = ['position', 'top', 'left', 'width', 'height', 'overflow', 'opacity']; + var props1 = ['position', 'top', 'left', 'overflow', 'opacity']; // Always restore + var props2 = ['width', 'height', 'overflow']; // Copy for children + var cProps = ['fontSize']; + var vProps = ['borderTopWidth', 'borderBottomWidth', 'paddingTop', 'paddingBottom']; + var hProps = ['borderLeftWidth', 'borderRightWidth', 'paddingLeft', 'paddingRight']; + + // Set options + var mode = $.effects.setMode(el, o.options.mode || 'effect'); // Set Mode + var restore = o.options.restore || false; // Default restore + var scale = o.options.scale || 'both'; // Default scale mode + var origin = o.options.origin; // The origin of the sizing + var original = { height: el.height(), width: el.width() }; // Save original + el.from = o.options.from || original; // Default from state + el.to = o.options.to || original; // Default to state + // Adjust + if (origin) { // Calculate baseline shifts + var baseline = $.effects.getBaseline(origin, original); + el.from.top = (original.height - el.from.height) * baseline.y; + el.from.left = (original.width - el.from.width) * baseline.x; + el.to.top = (original.height - el.to.height) * baseline.y; + el.to.left = (original.width - el.to.width) * baseline.x; + }; + var factor = { // Set scaling factor + from: { y: el.from.height / original.height, x: el.from.width / original.width }, + to: { y: el.to.height / original.height, x: el.to.width / original.width } + }; + if (scale == 'box' || scale == 'both') { // Scale the css box + if (factor.from.y != factor.to.y) { // Vertical props scaling + props = props.concat(vProps); + el.from = $.effects.setTransition(el, vProps, factor.from.y, el.from); + el.to = $.effects.setTransition(el, vProps, factor.to.y, el.to); + }; + if (factor.from.x != factor.to.x) { // Horizontal props scaling + props = props.concat(hProps); + el.from = $.effects.setTransition(el, hProps, factor.from.x, el.from); + el.to = $.effects.setTransition(el, hProps, factor.to.x, el.to); + }; + }; + if (scale == 'content' || scale == 'both') { // Scale the content + if (factor.from.y != factor.to.y) { // Vertical props scaling + props = props.concat(cProps); + el.from = $.effects.setTransition(el, cProps, factor.from.y, el.from); + el.to = $.effects.setTransition(el, cProps, factor.to.y, el.to); + }; + }; + $.effects.save(el, restore ? props : props1); el.show(); // Save & Show + $.effects.createWrapper(el); // Create Wrapper + el.css('overflow', 'hidden').css(el.from); // Shift + + // Animate + if (scale == 'content' || scale == 'both') { // Scale the children + vProps = vProps.concat(['marginTop', 'marginBottom']).concat(cProps); // Add margins/font-size + hProps = hProps.concat(['marginLeft', 'marginRight']); // Add margins + props2 = props.concat(vProps).concat(hProps); // Concat + el.find("*[width]").each(function() { + child = $(this); + if (restore) $.effects.save(child, props2); + var c_original = { height: child.height(), width: child.width() }; // Save original + child.from = { height: c_original.height * factor.from.y, width: c_original.width * factor.from.x }; + child.to = { height: c_original.height * factor.to.y, width: c_original.width * factor.to.x }; + if (factor.from.y != factor.to.y) { // Vertical props scaling + child.from = $.effects.setTransition(child, vProps, factor.from.y, child.from); + child.to = $.effects.setTransition(child, vProps, factor.to.y, child.to); + }; + if (factor.from.x != factor.to.x) { // Horizontal props scaling + child.from = $.effects.setTransition(child, hProps, factor.from.x, child.from); + child.to = $.effects.setTransition(child, hProps, factor.to.x, child.to); + }; + child.css(child.from); // Shift children + child.animate(child.to, o.duration, o.options.easing, function() { + if (restore) $.effects.restore(child, props2); // Restore children + }); // Animate children + }); + }; + + // Animate + el.animate(el.to, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { + if (mode == 'hide') el.hide(); // Hide + $.effects.restore(el, restore ? props : props1); $.effects.removeWrapper(el); // Restore + if (o.callback) o.callback.apply(this, arguments); // Callback + el.dequeue(); + } + }); + + }); + + }; + + })(jQuery); + /* + * jQuery UI Effects Slide 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Effects/Slide + * + * Depends: + * effects.core.js + */ + (function($) { + + $.effects.slide = function(o) { + + return this.queue(function() { + + // Create element + var el = $(this), props = ['position', 'top', 'left']; + + // Set options + var mode = $.effects.setMode(el, o.options.mode || 'show'); // Set Mode + var direction = o.options.direction || 'left'; // Default Direction + + // Adjust + $.effects.save(el, props); el.show(); // Save & Show + $.effects.createWrapper(el).css({ overflow: 'hidden' }); // Create Wrapper + var ref = (direction == 'up' || direction == 'down') ? 'top' : 'left'; + var motion = (direction == 'up' || direction == 'left') ? 'pos' : 'neg'; + var distance = o.options.distance || (ref == 'top' ? el.outerHeight({ margin: true }) : el.outerWidth({ margin: true })); + if (mode == 'show') el.css(ref, motion == 'pos' ? -distance : distance); // Shift + + // Animation + var animation = {}; + animation[ref] = (mode == 'show' ? (motion == 'pos' ? '+=' : '-=') : (motion == 'pos' ? '-=' : '+=')) + distance; + + // Animate + el.animate(animation, { queue: false, duration: o.duration, easing: o.options.easing, complete: function() { + if (mode == 'hide') el.hide(); // Hide + $.effects.restore(el, props); $.effects.removeWrapper(el); // Restore + if (o.callback) o.callback.apply(this, arguments); // Callback + el.dequeue(); + } + }); + + }); + + }; + + })(jQuery); +})(DDRjQuery); diff --git a/DNN Platform/Modules/NewDDRMenu/packages.config b/DNN Platform/Modules/NewDDRMenu/packages.config new file mode 100644 index 00000000000..ea1e2740db7 --- /dev/null +++ b/DNN Platform/Modules/NewDDRMenu/packages.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/NewDDRMenu/web.Debug.config b/DNN Platform/Modules/NewDDRMenu/web.Debug.config new file mode 100644 index 00000000000..ccf59884f90 --- /dev/null +++ b/DNN Platform/Modules/NewDDRMenu/web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/DNN Platform/Modules/NewDDRMenu/web.Release.config b/DNN Platform/Modules/NewDDRMenu/web.Release.config new file mode 100644 index 00000000000..9b90a9c21c4 --- /dev/null +++ b/DNN Platform/Modules/NewDDRMenu/web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/DNN Platform/Modules/NewDDRMenu/web.config b/DNN Platform/Modules/NewDDRMenu/web.config new file mode 100644 index 00000000000..9a2e6488394 --- /dev/null +++ b/DNN Platform/Modules/NewDDRMenu/web.config @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ContactList_Mvc.dnn b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ContactList_Mvc.dnn index 18356bed9ec..26eb645776d 100644 --- a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ContactList_Mvc.dnn +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ContactList_Mvc.dnn @@ -45,6 +45,7 @@ Dnn.ContactList.Mvc.Controllers/Contact/Index.mvc + DotNetNuke.Web.MvcPipeline.ModuleControl.MvcModuleControl, DotNetNuke.Web.MvcPipeline False View diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/ContactController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/ContactController.cs index f7c95b76d30..8211e6d7cd2 100644 --- a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/ContactController.cs +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/ContactController.cs @@ -5,6 +5,8 @@ using System; using System.Web.Mvc; using Dnn.ContactList.Api; +using DotNetNuke.Abstractions.ClientResources; +using DotNetNuke.Abstractions.Pages; using DotNetNuke.Collections; using DotNetNuke.Common; using DotNetNuke.Entities.Modules.Actions; @@ -20,20 +22,23 @@ namespace Dnn.ContactList.Mvc.Controllers public class ContactController : DnnController { private readonly IContactRepository _repository; - - /// - /// Default Constructor constructs a new ContactController - /// - public ContactController() : this(ContactRepository.Instance) { } + private readonly IClientResourceController clientResourceController; + private readonly IPageService pageService; /// /// Constructor constructs a new ContactController with a passed in repository /// - public ContactController(IContactRepository repository) + public ContactController(IClientResourceController clientResourceController, + IPageService pageService) { - Requires.NotNull(repository); + //Requires.NotNull(repository); + Requires.NotNull(clientResourceController); + Requires.NotNull(pageService); + + this.clientResourceController = clientResourceController; + this.pageService = pageService; + _repository = ContactRepository.Instance; - _repository = repository; } ///
@@ -111,6 +116,15 @@ public ActionResult Edit(Contact contact) [ModuleAction(ControlKey = "Edit", TitleKey = "AddContact")] public ActionResult Index(string searchTerm = "", int pageIndex = 0) { + pageService.SetTitle("my page title"); + clientResourceController + .CreateScript("~/DesktopModules/MVC/Dnn/ContactList/script.js") + .Register(); + clientResourceController + .CreateStylesheet("~/DesktopModules/MVC/Dnn/ContactList/stylesheet.css") + .Register(); + + var contacts = _repository.GetContacts(searchTerm, PortalSettings.PortalId, pageIndex, ModuleContext.Configuration.ModuleSettings.GetValueOrDefault("PageSize", 6)); return View(contacts); diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/RouteConfig.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/RouteConfig.cs index 839376e2dfa..548f856afd3 100644 --- a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/RouteConfig.cs +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/RouteConfig.cs @@ -10,7 +10,7 @@ public class RouteConfig : IMvcRouteMapper { public void RegisterRoutes(IMapRoute mapRouteManager) { - mapRouteManager.MapRoute("ContactList", "ContactList", "{controller}/{action}", new[] + mapRouteManager.MapRoute("Dnn/ContactList", "ContactList", "{controller}/{action}", new[] {"Dnn.ContactList.Mvc.Controllers"}); } } diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/script.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/script.js new file mode 100644 index 00000000000..44d1d2774a2 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/script.js @@ -0,0 +1 @@ +// empty for now diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/stylesheet.css b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/stylesheet.css new file mode 100644 index 00000000000..336309e5ae3 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/stylesheet.css @@ -0,0 +1 @@ +.mystyle{} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/.gitignore b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/.gitignore new file mode 100644 index 00000000000..e645f442256 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +*.csproj.user diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/App_LocalResources/Edit.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/App_LocalResources/Edit.resx new file mode 100644 index 00000000000..3d46e889aac --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/App_LocalResources/Edit.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Email Address + + + Valid email is required + + + First Name + + + First name is required + + + Last Name + + + Last name is required + + + Page [PageIndex] of [PageCount] + + + Phone + + + Valid phone is required + + + Save + + + Social + + + Social handle is required + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/App_LocalResources/View.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/App_LocalResources/View.resx new file mode 100644 index 00000000000..e2ba21d4d6a --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/App_LocalResources/View.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Add New Contact + + + Contact person + + + Edit Contact + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/ContactList_Razor.dnn b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/ContactList_Razor.dnn new file mode 100644 index 00000000000..6765707710c --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/ContactList_Razor.dnn @@ -0,0 +1,97 @@ + + + + Contact List Razor + DNN Contact List using Razor + + DNN + DNN Corp. + http://www.dnnsoftware.com + support@dnnsoftware.com + + + + true + + 10.00.00 + + + + + DesktopModules\Dnn\RazorContactList\ + + + + + + + Dnn.ContactList.Razor + Dnn/RazorContactList + + + + + Contact List Razor + 0 + + + + DotNetNuke.Web.MvcPipeline.ModuleControl.WebForms.WrapperModule, DotNetNuke.Web.MvcPipeline + Dnn.ContactList.Razor.ViewControl, Dnn.ContactList.Razor + True + + View + + + False + 0 + + + Edit + DotNetNuke.Web.MvcPipeline.ModuleControl.WebForms.WrapperModule, DotNetNuke.Web.MvcPipeline + Dnn.ContactList.Razor.EditControl, Dnn.ContactList.Razor + True + Add/Update Contact + Edit + + + True + 0 + + + + + + + + + + bin + Dnn.ContactList.Razor.dll + + + bin + Dnn.ContactList.Api.dll + + + + + + DesktopModules/Dnn/RazorContactList + + Resources.zip + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Controls/EditControl.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Controls/EditControl.cs new file mode 100644 index 00000000000..c9c617c59dd --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Controls/EditControl.cs @@ -0,0 +1,108 @@ +using Dnn.ContactList.Api; +using Dnn.ContactList.Razor.Models; +using DotNetNuke.Collections; +using DotNetNuke.Common; +using DotNetNuke.Web.MvcPipeline.ModuleControl; +using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; +using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Dnn.ContactList.Razor +{ + + public class EditControl : RazorModuleControlBase, IPageContributor + { + private readonly IContactRepository _repository; + private Contact _contact = null; + + // Constructor with dependency injection + public EditControl(IContactRepository repository) + { + Requires.NotNull(repository); + _repository = repository; + } + + // IPageContributor implementation (separate configuration of the page from control rendering) + public void ConfigurePage(PageConfigurationContext context) + { + // Enable Anti-Forgery support for AJAX calls + context.ServicesFramework.RequestAjaxAntiForgerySupport(); + // Enable Service Framework support for AJAX calls + context.ServicesFramework.RequestAjaxScriptSupport(); + // Register JavaScript file + context.ClientResourceController.CreateScript("~/DesktopModules/Dnn/RazorContactList/js/edit.js").Register(); + // Set the page title + if (IsEditing()) + { + var contact = GetContact(); + if (contact != null) + { + context.PageService.SetTitle("Razor - Edit Contact: " + contact.FirstName + " " + contact.LastName); + } + } + else + { + context.PageService.SetTitle("Razor - Add Contact"); + } + } + + // Main method to render the control + public override IRazorModuleResult Invoke() + { + var returnUrl = ModuleContext.NavigateUrl(this.TabId, string.Empty, false); + // Check if we are editing an existing contact + if (IsEditing()) + { + var c = GetContact(); + if (c == null) + { + // Contact does not exist, show error message + return Error("ContactList error", "contact dous not exist"); + } + + return View(new ContactModel() + { + ContactId = c.ContactId, + Email = c.Email, + FirstName = c.FirstName, + LastName = c.LastName, + Phone = c.Phone, + Social = c.Social, + ReturnUrl = returnUrl + }); + } + else + { + return View(new ContactModel() + { + ReturnUrl = returnUrl + }); + + } + } + + private Contact GetContact() + { + if (int.TryParse(Request.QueryString["ContactId"], out int contactId)) + { + if (_contact == null) + { + // avoid multiple retrievals + _contact = _repository.GetContact(contactId, PortalSettings.PortalId); + } + return _contact; + } + else + { + return null; + } + } + private bool IsEditing() + { + return Request.QueryString["ContactId"] != null; + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Controls/ViewControl.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Controls/ViewControl.cs new file mode 100644 index 00000000000..e1434a9c6d5 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Controls/ViewControl.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; +using System.Web; +using Dnn.ContactList.Api; +using Dnn.ContactList.Razor.Models; +using DotNetNuke.Abstractions.Pages; +using DotNetNuke.Collections; +using DotNetNuke.Common; +using DotNetNuke.Entities.Modules; +using DotNetNuke.Entities.Modules.Actions; +using DotNetNuke.Security; +using DotNetNuke.Services.Localization; +using DotNetNuke.Services.Pages; +using DotNetNuke.Web.MvcPipeline.ModuleControl; +using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; +using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + +namespace Dnn.ContactList.Razor +{ + + public class ViewControl : RazorModuleControlBase, IPageContributor, IActionable + { + private readonly IContactRepository _repository; + + // Constructor with dependency injection + public ViewControl(IContactRepository repository) + { + Requires.NotNull(repository); + _repository = repository; + } + + // IActionable implementation to add module actions + public ModuleActionCollection ModuleActions + { + get + { + var actions = new ModuleActionCollection(); + + actions.Add( + this.GetNextActionID(), + Localization.GetString(ModuleActionType.AddContent, this.LocalResourceFile), + ModuleActionType.AddContent, + string.Empty, + string.Empty, + this.EditUrl(), + false, + SecurityAccessLevel.Edit, + true, + false); + + return actions; + } + } + + // Configure the page settings (separate from rendering) + public void ConfigurePage(PageConfigurationContext context) + { + var contacts = _repository.GetContacts(PortalSettings.PortalId); + + context.PageService.SetTitle("Contact List - " + contacts.Count()); + context.PageService.SetDescription("Contact List description - " + contacts.Count()); + context.PageService.SetKeyWords("keywords1"); + + context.PageService.AddInfoMessage("", "This is a simple contact list module built using Razor and DNN's MVC Pipeline"); + } + + // Render the html for module control + public override IRazorModuleResult Invoke() + { + var contacts = _repository.GetContacts(PortalSettings.PortalId); + + return View(new ContactsModel() + { + IsEditable = ModuleContext.IsEditable, + EditUrl = ModuleContext.EditUrl(), + Contacts = contacts.Select(c => new ContactModel() + { + ContactId = c.ContactId, + Email = c.Email, + FirstName = c.FirstName, + LastName = c.LastName, + Phone = c.Phone, + Social = c.Social, + IsEditable = ModuleContext.IsEditable, + EditUrl = ModuleContext.IsEditable ? this.EditUrl("ContactId", c.ContactId.ToString(), "Edit") : string.Empty + }).ToList() + }); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Dnn.ContactList.Razor.csproj b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Dnn.ContactList.Razor.csproj new file mode 100644 index 00000000000..0c49187c679 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Dnn.ContactList.Razor.csproj @@ -0,0 +1,63 @@ + + + Dnn.ContactList.Razor + net48 + bin\ + false + false + Dnn.ContactList.Razor + Dnn ContactList Razor Module + en-US + + Library + + Portable + False + false + True + .\bin + netstandard2.0 + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + SolutionInfo.cs + + + + + + + + Edit.Designer.cs + PublicResXFileCodeGenerator + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Models/ContactModel.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Models/ContactModel.cs new file mode 100644 index 00000000000..82af7ace521 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Models/ContactModel.cs @@ -0,0 +1,19 @@ +namespace Dnn.ContactList.Razor.Models +{ + public class ContactModel + { + public ContactModel() + { + } + + public int ContactId { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Phone { get; set; } + public string Social { get; set; } + public bool IsEditable { get; set; } + public string EditUrl { get; internal set; } + public object ReturnUrl { get; internal set; } + } +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Models/ContactsModel.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Models/ContactsModel.cs new file mode 100644 index 00000000000..5e007123e03 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Models/ContactsModel.cs @@ -0,0 +1,16 @@ +using Dnn.ContactList.Razor.Models; +using System.Collections.Generic; + +namespace Dnn.ContactList.Razor.Models +{ + public class ContactsModel + { + public ContactsModel() + { + } + + public bool IsEditable { get; set; } + public string EditUrl { get; set; } + public List Contacts { get; set; } + } +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Module.build b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Module.build new file mode 100644 index 00000000000..5a315e5246a --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Module.build @@ -0,0 +1,40 @@ + + + + $(MSBuildProjectDirectory)\..\..\..\.. + + + + zip + ContactList_Razor + Dnn.ContactList.Razor + $(WebsitePath)\DesktopModules\Dnn\RazorContactList + $(PathToArtifacts)\SampleModules + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Properties/AssemblyInfo.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..9aec54bfbca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information + +using System; + +[assembly: CLSCompliant(true)] diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Properties/launchSettings.json b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Properties/launchSettings.json new file mode 100644 index 00000000000..102538f7218 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Dnn.ContactList.Razor": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:64192" + } + } +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/ReleaseNotes.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/ReleaseNotes.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/ReleaseNotes.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Services/ContactController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Services/ContactController.cs new file mode 100644 index 00000000000..f59703257df --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Services/ContactController.cs @@ -0,0 +1,168 @@ +// Copyright (c) DNN Software. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Linq; +using System.Net.Http; +using System.Web.Http; +using System.Web.UI.WebControls; +using Dnn.ContactList.Api; +using Dnn.ContactList.Razor.Models; +using DotNetNuke.Common; +using DotNetNuke.Security; +using DotNetNuke.Web.Api; + +namespace Dnn.ContactList.Razor.Services +{ + /// + /// ContentTypeController provides the Web Services to manage Data Types + /// + [SupportedModules("Dnn.ContactList.Razor")] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)] + public class ContactController : DnnApiController + { + private readonly IContactRepository _repository; + + /// + /// Default Constructor constructs a new ContactController + /// + public ContactController() : this(ContactRepository.Instance) + { + + } + + /// + /// Constructor constructs a new ContactController with a passed in repository + /// + public ContactController(IContactRepository service) + { + Requires.NotNull(service); + + _repository = service; + } + + /// + /// The DeleteContact method deletes a single contact + /// + /// + [HttpPost] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)] + public HttpResponseMessage DeleteContact(ContactModel viewModel) + { + var contact = _repository.GetContact(viewModel.ContactId, PortalSettings.PortalId); + + _repository.DeleteContact(contact); + + var response = new + { + success = true + }; + + return Request.CreateResponse(response); + } + + /// + /// The GetContact method gets a single contact + /// + /// + [HttpGet] + public HttpResponseMessage GetContact(int contactId) + { + var contact = _repository.GetContact(contactId, PortalSettings.PortalId); + + return Request.CreateResponse(new ContactModel() + { + ContactId = contact.ContactId, + FirstName = contact.FirstName, + LastName = contact.LastName, + Email = contact.Email, + Phone = contact.Phone, + Social = contact.Social + }); + } + + /// + /// The GetContacts method gets all the contacts + /// + /// + [HttpGet] + public HttpResponseMessage GetContacts(string searchTerm, int pageSize, int pageIndex) + { + var contactList = _repository.GetContacts(searchTerm, PortalSettings.PortalId, pageIndex, pageSize); + var contacts = contactList + .Select(contact => new ContactModel() + { + ContactId = contact.ContactId, + FirstName = contact.FirstName, + LastName = contact.LastName, + Email = contact.Email, + Phone = contact.Phone, + Social = contact.Social + }) + .ToList(); + + var response = new + { + success = true, + data = new + { + results = contacts, + totalCount = contactList.TotalCount + } + }; + + return Request.CreateResponse(response); + } + + /// + /// The SaveContact method persists the Contact to the repository + /// + /// + /// + [HttpPost] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)] + public HttpResponseMessage SaveContact(ContactModel viewModel) + { + Contact contact; + + if (viewModel.ContactId <= 0) + { + contact = new Contact + { + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Email = viewModel.Email, + Phone = viewModel.Phone, + Social = viewModel.Social, + PortalId = PortalSettings.PortalId + }; + _repository.AddContact(contact, UserInfo.UserID); + } + else + { + //Update + contact = _repository.GetContact(viewModel.ContactId, PortalSettings.PortalId); + + if (contact != null) + { + contact.FirstName = viewModel.FirstName; + contact.LastName = viewModel.LastName; + contact.Email = viewModel.Email; + contact.Phone = viewModel.Phone; + contact.Social = viewModel.Social; + } + _repository.UpdateContact(contact, UserInfo.UserID); + } + var response = new + { + success = true, + data = new + { + contactId = contact.ContactId, + } + }; + + return Request.CreateResponse(response); + + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Services/ServiceRouteMapper.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Services/ServiceRouteMapper.cs new file mode 100644 index 00000000000..bd76571001a --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Services/ServiceRouteMapper.cs @@ -0,0 +1,22 @@ +// Copyright (c) DNN Software. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using DotNetNuke.Web.Api; + +namespace Dnn.ContactList.Razor.Services +{ + /// + /// The ServiceRouteMapper tells the DNN Web API Framework what routes this module uses + /// + public class ServiceRouteMapper : IServiceRouteMapper + { + /// + /// RegisterRoutes is used to register the module's routes + /// + /// + public void RegisterRoutes(IMapRoute mapRouteManager) + { + mapRouteManager.MapHttpRoute("Dnn/RazorContactList", "default", "{controller}/{action}", new[] { "Dnn.ContactList.Razor.Services" }); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/Edit.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/Edit.cshtml new file mode 100644 index 00000000000..71a19a21101 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/Edit.cshtml @@ -0,0 +1,92 @@ +@using DotNetNuke.Services.Localization +@using DotNetNuke.Web.Mvc.Helpers + +@using System.Web.Mvc +@using System.Web.Mvc.Html +@using Dnn.ContactList.Razor.Models +@using DotNetNuke.Web.MvcPipeline.Modules + +@model ContactModel +@{ + var moduleId = (int)ViewData["ModuleId"]; +} + +
+
+ +
+

@(Model.ContactId > 0 ? Html.LocalizeString("Edit") : Html.LocalizeString("AddContact"))

+

@Html.LocalizeString("EditDescription")

+
+
+ +
+
\ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/View.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/View.cshtml new file mode 100644 index 00000000000..63fbbe868a4 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/View.cshtml @@ -0,0 +1,20 @@ +@using System.Web.Mvc +@using System.Web.Mvc.Html +@using Dnn.ContactList.Razor.Models +@using DotNetNuke.Web.MvcPipeline.Modules + +@model ContactsModel +
+
+ @foreach (var contact in Model.Contacts) + { + @Html.ModulePartial("_ContactPartial", contact); + } +
+ @if (Model.IsEditable) + { + + } +
diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/_ContactPartial.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/_ContactPartial.cshtml new file mode 100644 index 00000000000..facb4c8aa70 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/_ContactPartial.cshtml @@ -0,0 +1,51 @@ +@using Dnn.ContactList.Razor.Models +@using DotNetNuke.Web.MvcPipeline.Modules + +@model ContactModel + +
+
+ +
+
ACME CORP
+
INNOVATION & SOLUTIONS
+
+ + @if (Model.IsEditable) + { +
+ + + +
+ } +
+
+
@Model.FirstName @Model.LastName
+
@Html.LocalizeString("ContactPerson")
+
+ + @Model.Email.ToLowerInvariant() +
+
+ + @Model.Phone +
+ @if (!string.IsNullOrEmpty(Model.Social)) + { + + } +
+
\ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/web.config b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/web.config new file mode 100644 index 00000000000..aa10037ab8c --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Views/web.config @@ -0,0 +1,34 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/js/edit.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/js/edit.js new file mode 100644 index 00000000000..de842f8eaaf --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/js/edit.js @@ -0,0 +1,122 @@ +jQuery(document).ready(function ($) { + var $editContact = $('.contactEdit-container'); + var moduleId = $editContact.attr('data-moduleid'); + var returnUrl = $editContact.attr('data-returnurl'); + + $('#btnSave').click(function () { + var params = getData(); + sf.post("SaveContact", params, + function (data) { + //Success + if (typeof data !== "undefined" && data != null && data.success === true) { + //Success + window.location.href = returnUrl; + } + else { + //Failure + alert(data.message); + } + }, + function (data) { + //Failure + } + ) + return false; + }); + + var getData = function () { + var data = {}; + $editContact.find("input, select, textarea").each(function () { + data[$(this).attr("name")] = $(this).val(); + }); + return data; + } + + var services = function () { + var serviceController = "Contact"; + var serviceFramework; + var baseServicepath; + + var call = function (httpMethod, url, params, success, failure) { + var options = { + url: url, + beforeSend: serviceFramework.setModuleHeaders, + type: httpMethod, + async: true, + success: function (data) { + if (typeof success === 'function') { + success(data || {}); + } + }, + error: function (xhr, status, err) { + if (typeof failure === 'function') { + if (xhr) { + failure(xhr, err); + } + else { + failure(null, 'Unknown error'); + } + } else { + if (xhr) { + console.error(xhr, err); + alert((xhr.responseJSON && xhr.responseJSON.Message) || err); + } else { + console.error('Unknown error'); + alert('Unknown error'); + } + } + } + }; + + if (httpMethod == 'GET') { + options.data = params; + } + else { + options.contentType = 'application/json; charset=UTF-8'; + options.data = JSON.stringify(params); + options.dataType = 'json'; + } + + return $.ajax(options); + }; + + var get = function (method, params, success, failure) { + var self = this; + var url = baseServicepath + self.serviceController + '/' + method; + return call('GET', url, params, success, failure); + }; + + var init = function (settings) { + serviceFramework = settings.servicesFramework; + baseServicepath = serviceFramework.getServiceRoot('Dnn/RazorContactList'); + }; + + var post = function (method, params, success, failure, loading) { + var self = this; + var url = baseServicepath + self.serviceController + '/' + method; + return call('POST', url, params, success, failure); + }; + + var setHeaders = function (xhr) { + if (tabId) { + xhr.setRequestHeader('TabId', tabId); + } + + if (antiForgeryToken) { + xhr.setRequestHeader('RequestVerificationToken', antiForgeryToken); + } + }; + + return { + get: get, + init: init, + post: post, + serviceController: serviceController + } + }; + + var sf = new services(); + sf.init({ servicesFramework: $.ServicesFramework(moduleId) }) + +}); + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/license.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/license.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/license.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/module.css b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/module.css new file mode 100644 index 00000000000..aab3d2dbea4 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/module.css @@ -0,0 +1,450 @@ +@import url("//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"); +@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap'); + +/* Container and Grid Layout */ +.contactList-container { + font-family: 'Open Sans', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + max-width: 1400px; + margin: 30px auto; +} + +.contactList-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 30px; + margin-bottom: 30px; +} + +/* Responsive Grid */ +@media (max-width: 1200px) { + .contactList-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .contactList-grid { + grid-template-columns: 1fr; + } +} + +/* Contact Card Styles */ +.contactCard { + background: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06); + overflow: hidden; + transition: transform 0.2s ease, box-shadow 0.2s ease; + display: flex; + flex-direction: column; +} + + .contactCard:hover { + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.08); + } + +/* Card Header */ +.contactCard-header { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + padding: 12px; + min-height: 80px; + display: flex; + align-items: center; + position: relative; +} + +.contactCard-logo { + width: 50px; + height: 50px; + margin-right: 15px; + flex-shrink: 0; +} + + .contactCard-logo svg { + width: 100%; + height: 100%; + } + +.contactCard-company { + color: #ffffff; + flex-grow: 1; +} + +.company-name { + font-size: 16px; + font-weight: 700; + letter-spacing: 1.5px; + margin-bottom: 4px; +} + +.company-tagline { + font-size: 10px; + font-weight: 400; + letter-spacing: 0.8px; + opacity: 0.9; +} + +.contactCard-actions { + position: absolute; + top: 12px; + right: 12px; + display: flex; + gap: 8px; +} + + .contactCard-actions a { + color: #ffffff; + background: rgba(255, 255, 255, 0.2); + width: 32px; + height: 32px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; + text-decoration: none; + } + + .contactCard-actions a:hover { + background: rgba(255, 255, 255, 0.3); + } + + .contactCard-actions .fa { + font-size: 14px; + } + +/* Card Body */ +.contactCard-body { + padding: 24px; + background: #ffffff; +} + +.contact-name { + font-size: 22px; + font-weight: 700; + color: #333333; + margin-bottom: 6px; + line-height: 1.3; +} + +.contact-title { + font-size: 16px; + font-weight: 600; + color: #666666; + margin-bottom: 16px; +} + +.contact-detail { + font-size: 14px; + color: #333333; + margin-bottom: 10px; + display: flex; + align-items: center; + line-height: 1.6; +} + + .contact-detail .fa { + font-size: 14px; + color: var(--dnn-color-tertiary-light,#3c7a9a); + margin-right: 10px; + width: 16px; + text-align: center; + } + + .contact-detail span { + word-break: break-word; + } + +.contact-social a { + color: var(--dnn-color-tertiary-light,#3c7a9a); + text-decoration: none; + transition: color 0.2s ease; +} + + .contact-social a:hover { + color: var(--dnn-color-tertiary-light,#3c7a9a); + text-decoration: underline; + } + +/* Pagination */ +.contactList-pagination { + text-align: center; + padding: 20px 0; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; +} + +.pagination-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + color: #ffffff; + border-radius: 6px; + text-decoration: none; + transition: background-color 0.2s ease, transform 0.1s ease; +} + + .pagination-btn:visited { + color: #ffffff; + } + + .pagination-btn:hover { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + transform: scale(1.05); + } + + .pagination-btn .fa { + font-size: 16px; + } + +.pagination-text { + font-size: 15px; + color: #333333; + font-weight: 600; +} + +/* Edit Contact Form Styles */ +.contactEdit-container { + font-family: 'Open Sans', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + max-width: 800px; + margin: 30px auto; + background: #ffffff; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06); + overflow: hidden; +} + +.contactEdit-header { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + padding: 30px; + display: flex; + align-items: center; + gap: 20px; +} + +.contactEdit-logo { + width: 60px; + height: 60px; + flex-shrink: 0; +} + + .contactEdit-logo svg { + width: 100%; + height: 100%; + } + +.contactEdit-title { + color: #ffffff; + flex-grow: 1; +} + + .contactEdit-title h2 { + margin: 0 0 8px 0; + font-size: 28px; + font-weight: 700; + letter-spacing: 0.5px; + } + + .contactEdit-title p { + margin: 0; + font-size: 14px; + opacity: 0.9; + } + +.contactEdit-form { + padding: 40px; +} + +.form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + margin-bottom: 20px; +} + +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } + + .contactEdit-form { + padding: 30px 20px; + } +} + +.form-group { + margin-bottom: 24px; +} + + .form-group label { + display: block; + font-size: 14px; + font-weight: 600; + color: #333333; + margin-bottom: 8px; + } + + .form-group label .fa { + color: var(--dnn-color-tertiary-light,#3c7a9a); + margin-right: 6px; + width: 16px; + text-align: center; + } + + .form-group label .required { + color: #e80c4d; + font-weight: bold; + } + +.form-input { + width: 100%; + padding: 12px 16px; + font-size: 15px; + font-family: 'Open Sans', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + border: 2px solid #e0e0e0; + border-radius: 6px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + box-sizing: border-box; +} + + .form-input:focus { + outline: none; + border-color: var(--dnn-color-tertiary-light,#3c7a9a); + box-shadow: 0 0 0 3px rgba(60, 122, 154, 0.1); + } + + .form-input::placeholder { + color: #999999; + } + +.form-actions { + display: flex; + gap: 12px; + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid #e0e0e0; +} + +.btn-primary, +.btn-secondary { + padding: 12px 24px; + font-size: 15px; + font-weight: 600; + border-radius: 6px; + border: none; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; +} + +.btn-primary { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + color: #ffffff; +} + + .btn-primary:hover { + background-color: var(--dnn-color-tertiary-light,#2d5f78); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + } + +.btn-secondary { + background-color: #f5f5f5; + color: #666666; +} + + .btn-secondary:hover { + background-color: #e0e0e0; + color: #333333; + } + + .btn-primary .fa, + .btn-secondary .fa { + font-size: 14px; + } + +/* Legacy support */ +.buttons { + margin-top: 10px; + text-align: center; + padding-top: 20px; + clear: both; +} + +.editContact div label { + width: 200px; + display: inline-block; + margin-top: 15px; + margin-bottom: 15px; + text-align: right; + margin-right: 20px; + font-weight: bold; +} + +.editContact div input { + width: 200px; + margin-top: 20px; +} + +/* info and errors */ +.message-info { + border: 1px solid; + clear: both; + padding: 10px 20px; +} + +.message-error { + clear: both; + color: #e80c4d; + font-size: 1.1em; + font-weight: bold; + margin: 20px 0 10px 0; +} + +.message-success { + color: #7ac0da; + font-size: 1.3em; + font-weight: bold; + margin: 20px 0 10px 0; +} + +.error { + color: #e80c4d; +} + +/* styles for validation helpers */ +.field-validation-error { + color: #e80c4d; + font-weight: bold; +} + +.field-validation-valid { + display: none; +} + +input.input-validation-error { + border: 1px solid #e80c4d; +} + +input[type="checkbox"].input-validation-error { + border: 0 none; +} + +.validation-summary-errors { + color: #e80c4d; + font-weight: bold; + font-size: 1.1em; +} + +.validation-summary-valid { + display: none; +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.html b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.html index fec87fa11e7..1de46427e09 100644 --- a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.html +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.html @@ -1,5 +1,5 @@ [JavaScript:{ jsname: "Knockout" }] -[JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/contacts.js"}] +[JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/contacts.js", priority : 900}] [JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/util.js"}] [JavaScript:{ path: "~/Resources/Shared/scripts/dnn.jquery.js"}] [JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/QuickSettingsDispatcher.js"}] diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList_Spa.dnn b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList_Spa.dnn index 5c2386e7ad7..2b6b9654d72 100644 --- a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList_Spa.dnn +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList_Spa.dnn @@ -18,7 +18,7 @@ - DesktopModules\MVC\Dnn\ContactList\ + DesktopModules\Dnn\ContactList\ + } + @if (Model.StartupScripts.Any()) + { + + } + @Html.Raw(Model.ClientResourceController.RenderDependencies(ResourceType.All, "DnnFormBottomProvider", Globals.ApplicationPath)) + + diff --git a/DNN Platform/Website/Views/Default/ModuleActions.cshtml b/DNN Platform/Website/Views/Default/ModuleActions.cshtml new file mode 100644 index 00000000000..a9627fbffc6 --- /dev/null +++ b/DNN Platform/Website/Views/Default/ModuleActions.cshtml @@ -0,0 +1,204 @@ +@model DotNetNuke.Web.MvcPipeline.Models.ModuleActionsModel + +@using DotNetNuke.Web.MvcPipeline +@using DotNetNuke.UI.Modules +@using DotNetNuke.Services.Localization + +@{ + +} + + + +@if (Model.SupportsQuickSettings) +{ + +} + + \ No newline at end of file diff --git a/DNN Platform/Website/Views/PersonaBarContainer/Index.cshtml b/DNN Platform/Website/Views/PersonaBarContainer/Index.cshtml new file mode 100644 index 00000000000..8071a467bbe --- /dev/null +++ b/DNN Platform/Website/Views/PersonaBarContainer/Index.cshtml @@ -0,0 +1,44 @@ +@using DotNetNuke.Web.MvcPipeline +@model Dnn.PersonaBar.UI.Controllers.PersonaBarContainerModel +@{} + +
+
+ +
+ + +
diff --git a/DNN Platform/Website/Views/web.config b/DNN Platform/Website/Views/web.config new file mode 100644 index 00000000000..b9c29a8fc27 --- /dev/null +++ b/DNN Platform/Website/Views/web.config @@ -0,0 +1,33 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Website/admin/Portal/Views/Pricacy.cshtml b/DNN Platform/Website/admin/Portal/Views/Pricacy.cshtml new file mode 100644 index 00000000000..ed9bfb3556d --- /dev/null +++ b/DNN Platform/Website/admin/Portal/Views/Pricacy.cshtml @@ -0,0 +1,5 @@ +@model string + +
+ @Html.Raw(Model) +
\ No newline at end of file diff --git a/DNN Platform/Website/admin/Portal/Views/Terms.cshtml b/DNN Platform/Website/admin/Portal/Views/Terms.cshtml new file mode 100644 index 00000000000..7aeb1009b11 --- /dev/null +++ b/DNN Platform/Website/admin/Portal/Views/Terms.cshtml @@ -0,0 +1,5 @@ +@model string +@{ + +} +
@Html.Raw(Model)
diff --git a/DNN Platform/Website/admin/Portal/Views/web.config b/DNN Platform/Website/admin/Portal/Views/web.config new file mode 100644 index 00000000000..b9c29a8fc27 --- /dev/null +++ b/DNN Platform/Website/admin/Portal/Views/web.config @@ -0,0 +1,33 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Website/development.config b/DNN Platform/Website/development.config index fdd20d613de..26bec7844a1 100644 --- a/DNN Platform/Website/development.config +++ b/DNN Platform/Website/development.config @@ -141,7 +141,8 @@ - + + diff --git a/DNN Platform/Website/js/mvc.js b/DNN Platform/Website/js/mvc.js new file mode 100644 index 00000000000..c569d7c0bd1 --- /dev/null +++ b/DNN Platform/Website/js/mvc.js @@ -0,0 +1,181 @@ +// Enhanced registerNamespace implementation +const registerNamespace = (function () { + // Private function to handle namespace creation + function createNamespace(namespace) { + const parts = namespace.split('.'); + let current = window || global; + + for (let i = 0; i < parts.length; i++) { + // Create the namespace if it doesn't exist + current[parts[i]] = current[parts[i]] || {}; + current = current[parts[i]]; + } + + return current; + } + + // The main function with additional functionality + function registerNS(namespace, initialValue) { + const ns = createNamespace(namespace); + + // If an initial value is provided, extend the namespace + if (initialValue !== undefined) { + Object.assign(ns, initialValue); + } + + return ns; + } + + return registerNS; +})(); + +// More comprehensive implementation +function registerClass(config) { + const { + typeName, + baseType, + interfaceTypes = [], + constructor + } = config; + + // Create the class + function Type(...args) { + // Interface contract checking + interfaceTypes.forEach(interfaceType => { + for (let method in interfaceType.prototype) { + if (typeof interfaceType.prototype[method] === 'function' + && !this[method]) { + throw new Error(`Must implement interface method: ${method}`); + } + } + }); + + // Base type constructor + if (baseType) { + baseType.apply(this, args); + } + + // Own constructor + if (constructor) { + constructor.apply(this, args); + } + } + + // Inheritance + if (baseType) { + Type.prototype = Object.create(baseType.prototype); + Type.prototype.constructor = Type; + } + + // Interface implementation + interfaceTypes.forEach(interfaceType => { + Object.assign(Type.prototype, interfaceType.prototype); + }); + + return Type; +} + +if (!window) this.window = this; + +registerNamespace('Type'); + +Type.registerNamespace = registerNamespace; + +Function.prototype.registerClass = registerClass; + +Type.registerNamespace('Sys'); + +Sys.Browser = {}; + +Sys.Browser.InternetExplorer = {}; +Sys.Browser.Firefox = {}; +Sys.Browser.Safari = {}; +Sys.Browser.Opera = {}; + +Sys.Browser.agent = null; +Sys.Browser.hasDebuggerStatement = false; +Sys.Browser.name = navigator.appName; +Sys.Browser.version = parseFloat(navigator.appVersion); + +if (navigator.userAgent.indexOf(' MSIE ') > -1) { + Sys.Browser.agent = Sys.Browser.InternetExplorer; + Sys.Browser.version = parseFloat(navigator.userAgent.match(/MSIE (\d+\.\d+)/)[1]); + Sys.Browser.hasDebuggerStatement = true; +} +else if (navigator.userAgent.indexOf(' Firefox/') > -1) { + Sys.Browser.agent = Sys.Browser.Firefox; + Sys.Browser.version = parseFloat(navigator.userAgent.match(/ Firefox\/(\d+\.\d+)/)[1]); + Sys.Browser.name = 'Firefox'; + Sys.Browser.hasDebuggerStatement = true; +} +else if (navigator.userAgent.indexOf(' Safari/') > -1) { + Sys.Browser.agent = Sys.Browser.Safari; + Sys.Browser.version = parseFloat(navigator.userAgent.match(/ Safari\/(\d+\.\d+)/)[1]); + Sys.Browser.name = 'Safari'; +} +else if (navigator.userAgent.indexOf('Opera/') > -1) { + Sys.Browser.agent = Sys.Browser.Opera; +} + + +Sys.WebForms = {}; +Sys.WebForms.PageRequestManager = { + getInstance: function () { + return { + add_beginRequest: function (par) { }, + add_endRequest: function (par) { } + } + } +}; + +Sys.Application = { + add_load: function () { + + } +} + +window.$find = function (id) { + throw new Error('Not implemented'); // for making popups working +} + +window.$get = function (id, element) { + + if (!element) return document.getElementById(id); + if (element.getElementById) return element.getElementById(id); + + var nodeQueue = []; + var childNodes = element.childNodes; + for (var i = 0; i < childNodes.length; i++) { + var node = childNodes[i]; + if (node.nodeType == 1) { + nodeQueue[nodeQueue.length] = node; + } + } + + while (nodeQueue.length) { + node = nodeQueue.shift(); + if (node.id == id) { + return node; + } + childNodes = node.childNodes; + for (i = 0; i < childNodes.length; i++) { + node = childNodes[i]; + if (node.nodeType == 1) { + nodeQueue[nodeQueue.length] = node; + } + } + } + + return null; +} + +Sys.Serialization = { + JavaScriptSerializer: { + serialize : function (object) { + return JSON.stringify(object); + }, + deserialize : function (data) { + return JSON.parse(data); + } + } +} \ No newline at end of file diff --git a/DNN Platform/Website/release.config b/DNN Platform/Website/release.config index 031c8293596..6a4d8cd1fca 100644 --- a/DNN Platform/Website/release.config +++ b/DNN Platform/Website/release.config @@ -142,7 +142,8 @@ - + + diff --git a/DNN_Platform.sln b/DNN_Platform.sln index 74e310c263f..11d5f18fdbb 100644 --- a/DNN_Platform.sln +++ b/DNN_Platform.sln @@ -629,6 +629,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dnn.Skins.Aperture", "DNN P EndProject Project("{54A90642-561A-4BB1-A94E-469ADEE60C69}") = "Dnn.ClientSide", "DNN Platform\Dnn.ClientSide\Dnn.ClientSide.esproj", "{549CCB04-6321-4E6B-88C1-06FAC574D061}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetNuke.Web.MvcPipeline", "DNN Platform\DotNetNuke.Web.MvcPipeline\DotNetNuke.Web.MvcPipeline.csproj", "{AA3EE19B-81A0-3766-E8F4-424C2425A8D3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetNuke.Modules.NewDDRMenu", "DNN Platform\Modules\NewDDRMenu\DotNetNuke.Modules.NewDDRMenu.csproj", "{4366908E-2852-40EB-8675-610E0DA2AB5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetNuke.Web.MvcWebsite", "DNN Platform\DotNetNuke.Web.MvcWebsite\DotNetNuke.Web.MvcWebsite.csproj", "{78CA212D-B471-A777-9C3D-B4E43280181E}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetNuke.Web.Client.ResourceManager", "DNN Platform\DotNetNuke.Web.Client.ResourceManager\DotNetNuke.Web.Client.ResourceManager.csproj", "{039BAFBD-E2DB-40C4-B565-C27467D9B5E1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample Modules", "Sample Modules", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" @@ -641,6 +647,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dnn.ContactList.Spa", "DNN EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dnn.ContactList.SpaReact", "DNN Platform\Modules\Samples\Dnn.ContactList.SpaReact\Dnn.ContactList.SpaReact.csproj", "{19727E7F-6D3E-917D-920A-112FD03CA96F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dnn.ContactList.Razor", "DNN Platform\Modules\Samples\Dnn.ContactList.Razor\Dnn.ContactList.Razor.csproj", "{B899C9A5-C118-176D-A189-9959DD38C53F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Cloud_Debug|Any CPU = Cloud_Debug|Any CPU @@ -2361,6 +2369,78 @@ Global {549CCB04-6321-4E6B-88C1-06FAC574D061}.Release-Net45|x86.ActiveCfg = Release|Any CPU {549CCB04-6321-4E6B-88C1-06FAC574D061}.Release-Net45|x86.Build.0 = Release|Any CPU {549CCB04-6321-4E6B-88C1-06FAC574D061}.Release-Net45|x86.Deploy.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Debug|Any CPU.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Debug|Any CPU.Build.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Debug|x86.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Debug|x86.Build.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Release|Any CPU.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Release|Any CPU.Build.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Release|x86.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Cloud_Release|x86.Build.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug|x86.Build.0 = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug-Net45|Any CPU.ActiveCfg = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug-Net45|Any CPU.Build.0 = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug-Net45|x86.ActiveCfg = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Debug-Net45|x86.Build.0 = Debug|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release|Any CPU.Build.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release|x86.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release|x86.Build.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release-Net45|Any CPU.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release-Net45|Any CPU.Build.0 = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release-Net45|x86.ActiveCfg = Release|Any CPU + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3}.Release-Net45|x86.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Debug|Any CPU.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Debug|Any CPU.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Debug|x86.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Debug|x86.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Release|Any CPU.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Release|Any CPU.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Release|x86.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Cloud_Release|x86.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug|x86.ActiveCfg = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug|x86.Build.0 = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug-Net45|Any CPU.ActiveCfg = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug-Net45|Any CPU.Build.0 = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug-Net45|x86.ActiveCfg = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Debug-Net45|x86.Build.0 = Debug|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release|Any CPU.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release|x86.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release|x86.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release-Net45|Any CPU.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release-Net45|Any CPU.Build.0 = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release-Net45|x86.ActiveCfg = Release|Any CPU + {4366908E-2852-40EB-8675-610E0DA2AB5D}.Release-Net45|x86.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Debug|Any CPU.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Debug|Any CPU.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Debug|x86.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Debug|x86.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Release|Any CPU.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Release|Any CPU.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Release|x86.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Cloud_Release|x86.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug|x86.ActiveCfg = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug|x86.Build.0 = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug-Net45|Any CPU.ActiveCfg = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug-Net45|Any CPU.Build.0 = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug-Net45|x86.ActiveCfg = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Debug-Net45|x86.Build.0 = Debug|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release|Any CPU.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release|x86.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release|x86.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release-Net45|Any CPU.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release-Net45|Any CPU.Build.0 = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release-Net45|x86.ActiveCfg = Release|Any CPU + {78CA212D-B471-A777-9C3D-B4E43280181E}.Release-Net45|x86.Build.0 = Release|Any CPU {039BAFBD-E2DB-40C4-B565-C27467D9B5E1}.Cloud_Debug|Any CPU.ActiveCfg = Release|Any CPU {039BAFBD-E2DB-40C4-B565-C27467D9B5E1}.Cloud_Debug|Any CPU.Build.0 = Release|Any CPU {039BAFBD-E2DB-40C4-B565-C27467D9B5E1}.Cloud_Debug|x86.ActiveCfg = Release|Any CPU @@ -2481,6 +2561,30 @@ Global {19727E7F-6D3E-917D-920A-112FD03CA96F}.Release-Net45|Any CPU.Build.0 = Release|Any CPU {19727E7F-6D3E-917D-920A-112FD03CA96F}.Release-Net45|x86.ActiveCfg = Release|Any CPU {19727E7F-6D3E-917D-920A-112FD03CA96F}.Release-Net45|x86.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Debug|Any CPU.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Debug|Any CPU.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Debug|x86.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Debug|x86.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Release|Any CPU.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Release|Any CPU.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Release|x86.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Cloud_Release|x86.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug|x86.Build.0 = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug-Net45|Any CPU.ActiveCfg = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug-Net45|Any CPU.Build.0 = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug-Net45|x86.ActiveCfg = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Debug-Net45|x86.Build.0 = Debug|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release|Any CPU.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release|x86.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release|x86.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release-Net45|Any CPU.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release-Net45|Any CPU.Build.0 = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release-Net45|x86.ActiveCfg = Release|Any CPU + {B899C9A5-C118-176D-A189-9959DD38C53F}.Release-Net45|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2567,7 +2671,7 @@ Global {D6AE2D97-CCAA-4655-BE6F-6A9267431712} = {DBFB707E-85D1-462F-8EB8-84C21ACFA9D6} {9D9B15DD-E7CE-476C-BD3B-FF44E64EFB67} = {DBFB707E-85D1-462F-8EB8-84C21ACFA9D6} {5E147FE4-58B5-4C27-98C3-8ED178E107B7} = {392B91E8-C85D-4475-A169-1D4E33B06A4A} - {9CCA271F-CFAA-42A3-B577-7D5CBB38C646} = {5E147FE4-58B5-4C27-98C3-8ED178E107B7} + {9CCA271F-CFAA-42A3-B577-7D5CBB38C646} = {5F3D7934-4DE3-4E9B-9AF8-789FDB7D2404} {5F3D7934-4DE3-4E9B-9AF8-789FDB7D2404} = {392B91E8-C85D-4475-A169-1D4E33B06A4A} {8B50BA8B-0A08-41B8-81B8-EA70707C7379} = {5F3D7934-4DE3-4E9B-9AF8-789FDB7D2404} {7D92D57E-EB66-4816-A0FA-B39213452539} = {5F3D7934-4DE3-4E9B-9AF8-789FDB7D2404} @@ -2619,12 +2723,16 @@ Global {4FBA4C58-559B-4CD2-9926-CDA3498FE20E} = {FBD3B3FB-C9A6-43D2-8FE7-6A0A19DF0D0C} {9F20422E-76EB-4B24-B721-B1CAF17407F4} = {04F3856F-18A5-4916-A0EB-D3CFE0858443} {549CCB04-6321-4E6B-88C1-06FAC574D061} = {29273BE6-1AA8-4970-98A0-41BFFEEDA67B} + {AA3EE19B-81A0-3766-E8F4-424C2425A8D3} = {1DFA65CE-5978-49F9-83BA-CFBD0C7A1814} + {4366908E-2852-40EB-8675-610E0DA2AB5D} = {FDDC95FE-3341-4AED-A93E-7A5DF85A55C2} + {78CA212D-B471-A777-9C3D-B4E43280181E} = {1DFA65CE-5978-49F9-83BA-CFBD0C7A1814} {039BAFBD-E2DB-40C4-B565-C27467D9B5E1} = {1DFA65CE-5978-49F9-83BA-CFBD0C7A1814} {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {FDDC95FE-3341-4AED-A93E-7A5DF85A55C2} {8C8F79B4-AEA9-437E-9673-F4AD57E22949} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {A1615D6C-F956-AB32-5B07-C3CC75888398} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {735974ED-BE2B-6C25-CD75-1528175F6206} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {19727E7F-6D3E-917D-920A-112FD03CA96F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {B899C9A5-C118-176D-A189-9959DD38C53F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46B6A641-57EB-4B19-B199-23E6FC2AB40B} diff --git a/Dnn.AdminExperience/ClientSide/Extensions.Web/src/components/EditExtension/CustomSettings/Module/ModuleDefinitions/ModuleDefinitionRow/Controls/ControlFields.jsx b/Dnn.AdminExperience/ClientSide/Extensions.Web/src/components/EditExtension/CustomSettings/Module/ModuleDefinitions/ModuleDefinitionRow/Controls/ControlFields.jsx index 6a2e4d5b21a..a5ee6e2a22a 100644 --- a/Dnn.AdminExperience/ClientSide/Extensions.Web/src/components/EditExtension/CustomSettings/Module/ModuleDefinitions/ModuleDefinitionRow/Controls/ControlFields.jsx +++ b/Dnn.AdminExperience/ClientSide/Extensions.Web/src/components/EditExtension/CustomSettings/Module/ModuleDefinitions/ModuleDefinitionRow/Controls/ControlFields.jsx @@ -65,6 +65,13 @@ class ControlFields extends Component { error={props.triedToSave && props.error.source} onSelect={this.onSelect.bind(this, "source")} /> + +
diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/More/More.jsx b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/More/More.jsx index 5709657e6bb..cdcecc56446 100644 --- a/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/More/More.jsx +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/components/More/More.jsx @@ -21,6 +21,19 @@ class More extends Component { onChangeField(key, event.target.value); } + onPagePipelineSelected(option) { + const {onChangeField} = this.props; + onChangeField("pagePipeline", option.value); + } + + getPagePipelineOptions() { + const options = []; + options.push({ value: "", label: Localization.get("Inherited") }); + options.push({ value: "webforms", label: Localization.get("WebForms") }); + options.push({ value: "mvc", label: Localization.get("Mvc") }); + return options; + } + onCacheProviderSelected(option) { this.props.onChangeField("cacheProvider", option.value); if (!this.props.page.cacheProvider && option.value) { @@ -214,8 +227,33 @@ class More extends Component { withBorder={true} />} } + + + + + + } +
+ {Localization.get("PipelineSettings")} +
+ + + + + + + +
); } diff --git a/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js b/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js index 4565633c985..08a4ff87418 100644 --- a/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js +++ b/Dnn.AdminExperience/ClientSide/Pages.Web/src/services/pageService.js @@ -93,6 +93,7 @@ const PageService = function () { page.iconFile = null; page.iconFileLarge = null; page.sitemapPriority = 0.5; + page.pagePipeline = ""; return page; }); }; diff --git a/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/moreSettings/index.jsx b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/moreSettings/index.jsx index 6810c10771e..b5a6fc5799b 100644 --- a/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/moreSettings/index.jsx +++ b/Dnn.AdminExperience/ClientSide/SiteSettings.Web/src/components/moreSettings/index.jsx @@ -302,6 +302,13 @@ class MoreSettingsPanelBody extends Component { } return options; } + + getPagePipelineOptions() { + const options = []; + options.push({ value: "webforms", label: resx.get("WebForms") }); + options.push({ value: "mvc", label: resx.get("Mvc") }); + return options; + } onDropDownChange(key, option) { let { state, props } = this; @@ -535,6 +542,25 @@ class MoreSettingsPanelBody extends Component { }
} +
+ {resx.get("PipelineSettings")} +
+ + +
+
+
+ +
+
").css({padding:0,margin:0}).append(i),r.append(u),u},DDR.Menu.setTableColumns=function(t,i){var u=t.children("tbody");u.length==0&&(u=t);var f=u.children("tr"),e=f.children("td"),r=n("