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/Build.csproj b/Build/Build.csproj index 5659c6aa218..5ff5ad436ff 100644 --- a/Build/Build.csproj +++ b/Build/Build.csproj @@ -1,7 +1,7 @@ Exe - net9.0 + net10.0 true $(MSBuildProjectDirectory) @@ -15,17 +15,17 @@ - + - + - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Build/BuildScripts/Module.build b/Build/BuildScripts/Module.build index e489b7ef034..af55719d8c1 100644 --- a/Build/BuildScripts/Module.build +++ b/Build/BuildScripts/Module.build @@ -18,7 +18,7 @@ - + diff --git a/Build/ContextExtensions.cs b/Build/ContextExtensions.cs index 46a95c6d7ff..185c6683f59 100644 --- a/Build/ContextExtensions.cs +++ b/Build/ContextExtensions.cs @@ -4,8 +4,9 @@ namespace DotNetNuke.Build; +using System; using System.Diagnostics; - + using Cake.Common.IO; using Cake.Core.IO; @@ -18,6 +19,8 @@ public static class ContextExtensions /// The file version. public static string GetAssemblyFileVersion(this Context context, FilePath assemblyPath) { - return FileVersionInfo.GetVersionInfo(context.MakeAbsolute(assemblyPath).FullPath).FileVersion; + var versionInfo = FileVersionInfo.GetVersionInfo(context.MakeAbsolute(assemblyPath).FullPath); + var fileVersion = versionInfo.FileVersion; + return Version.TryParse(fileVersion, out _) ? fileVersion : $"{versionInfo.FileMajorPart}.{versionInfo.FileMinorPart}.{versionInfo.FileBuildPart}"; } } diff --git a/Build/LocalSettings.cs b/Build/LocalSettings.cs index 7149575b931..192c13b53c7 100644 --- a/Build/LocalSettings.cs +++ b/Build/LocalSettings.cs @@ -33,6 +33,9 @@ public class LocalSettings /// Gets or sets the path to the database files. public string DatabasePath { get; set; } = string.Empty; + /// Gets or sets a value indicating whether to copy the sample projects to the build output. + public bool CopySampleProjects { get; set; } = false; + /// Gets or sets the version to use for the build. public string Version { get; set; } = "auto"; } diff --git a/Build/Tasks/BuildToTempFolder.cs b/Build/Tasks/BuildToTempFolder.cs index 0b431b01959..63bdb30d47a 100644 --- a/Build/Tasks/BuildToTempFolder.cs +++ b/Build/Tasks/BuildToTempFolder.cs @@ -3,10 +3,10 @@ // See the LICENSE file in the project root for more information namespace DotNetNuke.Build.Tasks { - using System; - using System.Linq; - + using Cake.Common.Diagnostics; + using Cake.Common.IO; using Cake.Frosting; + using Dnn.CakeUtils; /// A cake task to build the platform. [IsDependentOn(typeof(SetVersion))] @@ -16,5 +16,20 @@ namespace DotNetNuke.Build.Tasks [IsDependentOn(typeof(OtherPackages))] public sealed class BuildToTempFolder : FrostingTask { + /// + public override void Run(Context context) + { + if (context.Settings.CopySampleProjects) + { + context.Information("Copying Sample Projects to Temp Folder"); + var files = context.GetFilesByPatterns(context.ArtifactsFolder, new[] { "SampleModules/*.zip" }); + foreach (var file in files) + { + var destination = context.File(System.IO.Path.Combine(context.WebsiteFolder, "Install", "Module", file.GetFilename().ToString())); + context.CopyFile(file, destination); + context.Information($" Copied {file.GetFilename()} to {destination}"); + } + } + } } } diff --git a/Build/Tasks/OtherPackages.cs b/Build/Tasks/OtherPackages.cs index ac1b9997f63..7b685731433 100644 --- a/Build/Tasks/OtherPackages.cs +++ b/Build/Tasks/OtherPackages.cs @@ -18,6 +18,7 @@ namespace DotNetNuke.Build.Tasks /// A cake task to include other 3rd party packages. [IsDependentOn(typeof(PackageNewtonsoft))] [IsDependentOn(typeof(PackageMailKit))] + [IsDependentOn(typeof(PackageHtmlSanitizer))] [IsDependentOn(typeof(PackageAspNetWebApi))] [IsDependentOn(typeof(PackageAspNetWebPages))] [IsDependentOn(typeof(PackageAspNetMvc))] diff --git a/Build/Tasks/PackageHtmlSanitizer.cs b/Build/Tasks/PackageHtmlSanitizer.cs new file mode 100644 index 00000000000..fa6ff57759b --- /dev/null +++ b/Build/Tasks/PackageHtmlSanitizer.cs @@ -0,0 +1,14 @@ +// 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.Build.Tasks; + +/// A cake task to generate the MailKit package. +public sealed class PackageHtmlSanitizer : PackageComponentTask +{ + /// Initializes a new instance of the class. + public PackageHtmlSanitizer() + : base("HtmlSanitizer") + { + } +} 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/Build/Tasks/unversionedManifests.txt b/Build/Tasks/unversionedManifests.txt index 48ff39fc92b..4540db7a9a1 100644 --- a/Build/Tasks/unversionedManifests.txt +++ b/Build/Tasks/unversionedManifests.txt @@ -1,8 +1,4 @@ -DNN Platform/Components/MailKit/*.dnn -DNN Platform/Components/Microsoft.*/**/*.dnn -DNN Platform/Components/Newtonsoft/*.dnn -DNN Platform/Components/WebFormsMvp/*.dnn -DNN Platform/Components/SharpZipLib/*.dnn +DNN Platform/Components/**/*.dnn DNN Platform/JavaScript Libraries/HoverIntent/*.dnn DNN Platform/JavaScript Libraries/jQuery*/*.dnn DNN Platform/JavaScript Libraries/Knockout*/*.dnn diff --git a/DNN Platform/Components/HtmlSanitizer/HtmlSanitizer.dnn b/DNN Platform/Components/HtmlSanitizer/HtmlSanitizer.dnn new file mode 100644 index 00000000000..caac4cd9d42 --- /dev/null +++ b/DNN Platform/Components/HtmlSanitizer/HtmlSanitizer.dnn @@ -0,0 +1,55 @@ + + + + HtmlSanitizer Components + Provides AngleSharp and HtmlSanitizer assemblies for the platform. + + + .NET Foundation and Contributors + DNN Community + https://dnncommunity.org + info@dnncommunity.org + + + + This package includes AngleSharp, AngleSharp.Css and HtmlSanitizer assemblies. + + + + + + bin + AngleSharp.dll + + + + bin + AngleSharp.Css.dll + + + + bin + HtmlSanitizer.dll + + + + bin + System.Collections.Immutable.dll + + + + bin + System.Text.Encoding.CodePages.dll + + + + bin + System.Runtime.CompilerServices.Unsafe.dll + + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Components/HtmlSanitizer/License.txt b/DNN Platform/Components/HtmlSanitizer/License.txt new file mode 100644 index 00000000000..73c0a599158 --- /dev/null +++ b/DNN Platform/Components/HtmlSanitizer/License.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2012-2020 .NET Foundation and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file 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.ContentSecurityPolicy/BaseCspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/BaseCspContributor.cs new file mode 100644 index 00000000000..44b12f1738c --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/BaseCspContributor.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.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Base class for all CSP directive contributors. + /// + public abstract class BaseCspContributor + { + /// + /// Gets unique identifier for the contributor. + /// + public Guid Id { get; } = Guid.NewGuid(); + + /// + /// Gets or sets type of the CSP directive. + /// + public CspDirectiveType DirectiveType { get; protected set; } + + /// + /// Generates the directive string. + /// + /// The directive string. + public abstract string GenerateDirective(); + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicy.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicy.cs new file mode 100644 index 00000000000..93fd9a20689 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ContentSecurityPolicy.cs @@ -0,0 +1,393 @@ +// 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.ContentSecurityPolicy +{ + using System.Collections.Generic; + using System.Linq; + + /// + /// Manages the entire Content Security Policy. + /// + public class ContentSecurityPolicy : IContentSecurityPolicy + { + private string nonce; + + /// Initializes a new instance of the class. + public ContentSecurityPolicy() + { + } + + /// + /// Gets a cryptographically secure random nonce value for use in CSP policies. + /// + public string Nonce + { + get + { + if (this.nonce == null) + { + var nonceBytes = new byte[32]; + var generator = System.Security.Cryptography.RandomNumberGenerator.Create(); + generator.GetBytes(nonceBytes); + this.nonce = System.Convert.ToBase64String(nonceBytes); + } + + return this.nonce; + } + } + + /// + /// Gets the default source contributor for managing default-src directives. + /// + public SourceCspContributor DefaultSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.DefaultSrc); + } + } + + /// + /// Gets the script source contributor for managing script-src directives. + /// + public SourceCspContributor ScriptSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ScriptSrc); + } + } + + /// + /// Gets the style source contributor for managing style-src directives. + /// + public SourceCspContributor StyleSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.StyleSrc); + } + } + + /// + /// Gets the image source contributor for managing img-src directives. + /// + public SourceCspContributor ImgSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ImgSrc); + } + } + + /// + /// Gets the connect source contributor for managing connect-src directives. + /// + public SourceCspContributor ConnectSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ConnectSrc); + } + } + + /// + /// Gets the connect frame ancestors for managing connect-src directives. + /// + public SourceCspContributor FrameAncestors + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FrameAncestors); + } + } + + /// + /// Gets the font source contributor for managing font-src directives. + /// + public SourceCspContributor FontSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FontSrc); + } + } + + /// + /// Gets the object source contributor for managing object-src directives. + /// + public SourceCspContributor ObjectSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.ObjectSrc); + } + } + + /// + /// Gets the media source contributor for managing media-src directives. + /// + public SourceCspContributor MediaSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.MediaSrc); + } + } + + /// + /// Gets the frame source contributor for managing frame-src directives. + /// + public SourceCspContributor FrameSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FrameSrc); + } + } + + /// + /// Gets the Form Action source contributor for managing frame-src directives. + /// + public SourceCspContributor FormAction + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.FormAction); + } + } + + /// + /// Gets the base URI source contributor for managing base-uri directives. + /// + public SourceCspContributor BaseUriSource + { + get + { + return this.GetOrCreateDirective(CspDirectiveType.BaseUri); + } + } + + /// + /// Gets collection of CSP contributors. + /// + private List ContentSecurityPolicyContributors { get; } = new List(); + + /// + /// Gets collection of CSP contributors. + /// + private List ReportingEndpointsContributors { get; } = new List(); + + /// + /// Supprime les sources de script du type spécifié de la politique CSP. + /// + /// Le type de source CSP à supprimer. + public void RemoveScriptSources(CspSourceType cspSourceType) + { + this.RemoveSources(CspDirectiveType.ScriptSrc, cspSourceType); + } + + /// + /// Ajoute des types de plugins autorisés à la politique CSP. + /// + /// Le type de plugin à autoriser. + public void AddPluginTypes(string value) + { + this.AddDocumentDirective(CspDirectiveType.PluginTypes, value); + } + + /// + /// Ajoute une directive sandbox à la politique CSP. + /// + /// La valeur de la directive sandbox. + public void AddSandboxDirective(string value) + { + this.SetDocumentDirective(CspDirectiveType.SandboxDirective, value); + } + + /// + /// Ajoute une directive form-action à la politique CSP. + /// + /// Le type de source CSP à ajouter. + /// La valeur associée à la source. + public void AddFormAction(CspSourceType sourceType, string value) + { + this.AddSource(CspDirectiveType.FormAction, sourceType, value); + } + + /// + /// Ajoute une directive frame-ancestors à la politique CSP. + /// + /// Le type de source CSP à ajouter. + /// La valeur associée à la source. + public void AddFrameAncestors(CspSourceType sourceType, string value) + { + this.AddSource(CspDirectiveType.FrameAncestors, sourceType, value); + } + + /// + /// Ajoute une URI de rapport à la politique CSP. + /// + /// Le nom où les rapports de violation seront envoyés. + /// L'URI où les rapports de violation seront envoyés. + public void AddReportEndpoint(string name, string value) + { + this.AddReportingDirective(CspDirectiveType.ReportUri, value); + this.AddReportingEndpointsDirective(name, value); + } + + /// + /// Ajoute un endpoint de rapport à la politique CSP. + /// + /// L'endpoint où les rapports seront envoyés. + public void AddReportTo(string value) + { + this.AddReportingDirective(CspDirectiveType.ReportTo, value); + } + + /// + /// Upgrade Insecure Requests. + /// + public void UpgradeInsecureRequests() + { + this.SetDocumentDirective(CspDirectiveType.UpgradeInsecureRequests, string.Empty); + } + + /// + /// Generates the complete Content Security Policy. + /// + /// The complete Content Security Policy. + public string GeneratePolicy() + { + return string.Join( + "; ", + this.ContentSecurityPolicyContributors + .Select(c => c.GenerateDirective()) + .Where(d => !string.IsNullOrEmpty(d))); + } + + /// + /// Génère la politique de sécurité complète. + /// + /// Reporting Endpoints sous forme de chaîne. + public string GenerateReportingEndpoints() + { + return string.Join( + "; ", + this.ReportingEndpointsContributors + .Select(c => c.GenerateDirective()) + .Where(d => !string.IsNullOrEmpty(d))); + } + + private SourceCspContributor GetOrCreateDirective(CspDirectiveType directiveType) + { + var directive = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as SourceCspContributor; + if (directive == null) + { + directive = new SourceCspContributor(directiveType); + this.AddContributor(directive); + } + + return directive; + } + + /// + /// Adds a contributor to the policy. + /// + private void AddContributor(BaseCspContributor contributor) + { + // Remove any existing contributor of the same directive type + this.ContentSecurityPolicyContributors.RemoveAll(c => c.DirectiveType == contributor.DirectiveType); + this.ContentSecurityPolicyContributors.Add(contributor); + } + + /// + /// Adds a contributor to the policy. + /// + private void AddReportingEndpointsContributors(BaseCspContributor contributor) + { + // Remove any existing contributor of the same directive type + this.ReportingEndpointsContributors.RemoveAll(c => c.DirectiveType == contributor.DirectiveType); + this.ReportingEndpointsContributors.Add(contributor); + } + + private void AddSource(CspDirectiveType directiveType, CspSourceType sourceType, string value = null) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as SourceCspContributor; + if (contributor == null) + { + contributor = new SourceCspContributor(directiveType); + this.AddContributor(contributor); + } + + if (sourceType == CspSourceType.Nonce && string.IsNullOrEmpty(value)) + { + value = this.Nonce; + } + + contributor.AddSource(new CspSource(sourceType, value)); + } + + private void RemoveSources(CspDirectiveType directiveType, CspSourceType sourceType) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as SourceCspContributor; + if (contributor == null) + { + contributor = new SourceCspContributor(directiveType); + this.AddContributor(contributor); + } + + contributor.RemoveSources(sourceType); + } + + private void SetDocumentDirective(CspDirectiveType directiveType, string value) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as DocumentCspContributor; + if (contributor == null) + { + contributor = new DocumentCspContributor(directiveType, value); + this.AddContributor(contributor); + } + + contributor.SetDirectiveValue(value); + } + + private void AddDocumentDirective(CspDirectiveType directiveType, string value) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as DocumentCspContributor; + if (contributor == null) + { + contributor = new DocumentCspContributor(directiveType, value); + this.AddContributor(contributor); + } + + contributor.SetDirectiveValue(value); + } + + private void AddReportingDirective(CspDirectiveType directiveType, string value) + { + var contributor = this.ContentSecurityPolicyContributors.FirstOrDefault(c => c.DirectiveType == directiveType) as ReportingCspContributor; + if (contributor == null) + { + contributor = new ReportingCspContributor(directiveType); + this.AddContributor(contributor); + } + + contributor.AddReportingEndpoint(value); + } + + private void AddReportingEndpointsDirective(string name, string value) + { + var contributor = this.ReportingEndpointsContributors.FirstOrDefault(c => c.DirectiveType == CspDirectiveType.ReportUri) as ReportingEndpointContributor; + if (contributor == null) + { + contributor = new ReportingEndpointContributor(CspDirectiveType.ReportUri); + this.AddReportingEndpointsContributors(contributor); + } + + contributor.AddReportingEndpoint(name, value); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspContributor.cs new file mode 100644 index 00000000000..27279e58df3 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspContributor.cs @@ -0,0 +1,81 @@ +// 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.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Manages Content Security Policy contributors for a specific directive. + /// + public class CspContributor + { + /// + /// Initializes a new instance of the class. + /// + /// The directive to create the contributor for. + public CspContributor(string directive) + { + this.Directive = directive ?? throw new ArgumentNullException(nameof(directive)); + } + + /// + /// Gets name of the directive (e.g., 'script-src', 'style-src'). + /// + public string Directive { get; } + + /// + /// Gets collection of sources for this directive. + /// + private List Sources { get; } = new List(); + + /// + /// Adds a source to the directive. + /// + /// The source to add. + public void AddSource(CspSource source) + { + if (!this.Sources.Any(s => s.Type == source.Type && s.Value == source.Value)) + { + this.Sources.Add(source); + } + } + + /// + /// Removes a source from the directive. + /// + /// The source to remove. + public void RemoveSource(CspSource source) + { + this.Sources.RemoveAll(s => s.Type == source.Type && s.Value == source.Value); + } + + /// + /// Generates the complete directive string. + /// + /// The directive string. + public string GenerateDirective() + { + if (!this.Sources.Any()) + { + return string.Empty; + } + + return $"{this.Directive} {string.Join(" ", this.Sources.Select(s => s.ToString()))}"; + } + + /// + /// Gets all sources of a specific type. + /// + /// The type of sources to get. + /// The sources of the specified type. + public IEnumerable GetSourcesByType(CspSourceType type) + { + return this.Sources.Where(s => s.Type == type); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveNameMapper.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveNameMapper.cs new file mode 100644 index 00000000000..dd1abeab348 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveNameMapper.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.ContentSecurityPolicy +{ + using System; + + /// + /// Utility class for converting directive types to their string representations. + /// + public static class CspDirectiveNameMapper + { + /// + /// Gets the directive name string. + /// + /// The directive type to get the name for. + /// The directive name string. + public static string GetDirectiveName(CspDirectiveType directiveType) + { + return directiveType switch + { + CspDirectiveType.DefaultSrc => "default-src", + CspDirectiveType.ScriptSrc => "script-src", + CspDirectiveType.StyleSrc => "style-src", + CspDirectiveType.ImgSrc => "img-src", + CspDirectiveType.ConnectSrc => "connect-src", + CspDirectiveType.FontSrc => "font-src", + CspDirectiveType.ObjectSrc => "object-src", + CspDirectiveType.MediaSrc => "media-src", + CspDirectiveType.FrameSrc => "frame-src", + CspDirectiveType.BaseUri => "base-uri", + CspDirectiveType.PluginTypes => "plugin-types", + CspDirectiveType.SandboxDirective => "sandbox", + CspDirectiveType.FormAction => "form-action", + CspDirectiveType.FrameAncestors => "frame-ancestors", + CspDirectiveType.ReportUri => "report-uri", + CspDirectiveType.ReportTo => "report-to", + CspDirectiveType.UpgradeInsecureRequests => "upgrade-insecure-requests", + _ => throw new ArgumentException("Unknown directive type") + }; + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveType.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveType.cs new file mode 100644 index 00000000000..889db63ea4e --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspDirectiveType.cs @@ -0,0 +1,97 @@ +// 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.ContentSecurityPolicy +{ + /// + /// Represents different types of Content Security Policy directives. + /// + public enum CspDirectiveType + { + /// + /// Directive qui définit la politique par défaut pour les types de ressources non spécifiés. + /// + DefaultSrc, + + /// + /// Directive qui contrôle les sources de scripts autorisées. + /// + ScriptSrc, + + /// + /// Directive qui contrôle les sources de styles autorisées. + /// + StyleSrc, + + /// + /// Directive qui contrôle les sources d'images autorisées. + /// + ImgSrc, + + /// + /// Directive qui contrôle les destinations de connexion autorisées. + /// + ConnectSrc, + + /// + /// Directive qui contrôle les sources de polices autorisées. + /// + FontSrc, + + /// + /// Directive qui contrôle les sources d'objets autorisées. + /// + ObjectSrc, + + /// + /// Directive qui contrôle les sources de médias autorisées. + /// + MediaSrc, + + /// + /// Directive qui contrôle les sources de frames autorisées. + /// + FrameSrc, + + /// + /// Directive qui restreint les URLs pouvant être utilisées dans la base URI du document. + /// + BaseUri, + + /// + /// Directive qui restreint les types de plugins pouvant être chargés. + /// + PluginTypes, + + /// + /// Directive qui active un bac à sable pour la ressource demandée. + /// + SandboxDirective, + + /// + /// Directive qui restreint les URLs pouvant être utilisées comme cible de formulaire. + /// + FormAction, + + /// + /// Directive qui spécifie les parents autorisés à intégrer une page dans un frame. + /// + FrameAncestors, + + /// + /// Directive qui spécifie l'URI où envoyer les rapports de violation. + /// + ReportUri, + + /// + /// Directive qui spécifie où envoyer les rapports de violation au format JSON. + /// + ReportTo, + + /// + /// Directive qui spécifie UpgradeInsecureRequests. + /// + UpgradeInsecureRequests, +} +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspPolicyExample.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspPolicyExample.cs new file mode 100644 index 00000000000..c8d424e6d98 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspPolicyExample.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.ContentSecurityPolicy +{ + using System; + + /// + /// Démontre l'utilisation de la Content Security Policy en configurant différentes directives. + /// + public class CspPolicyExample + { + /// + /// Démontre l'utilisation de la Content Security Policy en configurant différentes directives. + /// + public static void Example() + { + // Create a Content Security Policy + var csp = new ContentSecurityPolicy(); + + // Add a source-based contributor for script sources + csp.ScriptSource + .AddSelf() + .AddHost("https://trusted-cdn.com"); + + // Add a document-based contributor for sandbox + csp.AddSandboxDirective("allow-scripts allow-same-origin"); + + // Add a reporting contributor + csp.AddReportEndpoint("name", "https://example.com/csp-report"); + + // Generate the complete policy + string policy = csp.GeneratePolicy(); + + Console.WriteLine(policy); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSource.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSource.cs new file mode 100644 index 00000000000..3de637d965e --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSource.cs @@ -0,0 +1,165 @@ +// 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.ContentSecurityPolicy +{ + using System; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Represents a single source in a Content Security Policy. + /// + public class CspSource + { + /// + /// Initializes a new instance of the class. + /// + /// Type of the source. + /// Value of the source. + public CspSource(CspSourceType type, string value = null) + { + this.Type = type; + this.Value = this.ValidateSource(type, value); + } + + /// + /// Gets type of the CSP source. + /// + public CspSourceType Type { get; } + + /// + /// Gets the actual source value. + /// + public string Value { get; } + + /// + /// Returns the string representation of the source. + /// + /// The string representation of the source. + public override string ToString() => this.Value ?? CspSourceTypeNameMapper.GetSourceTypeName(this.Type); + + /// + /// Validates the source based on its type. + /// + private string ValidateSource(CspSourceType type, string value) + { + switch (type) + { + case CspSourceType.Host: + return this.ValidateHostSource(value); + case CspSourceType.Scheme: + return this.ValidateSchemeSource(value); + case CspSourceType.Nonce: + return this.ValidateNonceSource(value); + case CspSourceType.Hash: + return this.ValidateHashSource(value); + case CspSourceType.Self: + return "'self'"; + case CspSourceType.Inline: + case CspSourceType.Eval: + return "'unsafe-" + type.ToString().ToLowerInvariant() + "'"; + case CspSourceType.None: + return "'none'"; + case CspSourceType.StrictDynamic: + return "'strict-dynamic'"; + default: + throw new ArgumentException("Invalid source type"); + } + } + + /// + /// Validates host source (domain or IP). + /// + private string ValidateHostSource(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Host source cannot be empty"); + } + + // Basic domain validation + var domainRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$"); + if (!domainRegex.IsMatch(value)) + { + throw new ArgumentException($"Invalid host source: {value}"); + } + + return value.StartsWith("http") ? value : $"https://{value}"; + } + + /// + /// Validates scheme source (protocol). + /// + private string ValidateSchemeSource(string value) + { + string[] validSchemes = { "http:", "https:", "data:", "blob:", "filesystem:" }; + if (!validSchemes.Contains(value)) + { + throw new ArgumentException($"Invalid scheme: {value}"); + } + + return value; + } + + /// + /// Validates nonce source. + /// + private string ValidateNonceSource(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Nonce cannot be empty"); + } + + // Basic nonce validation (base64 encoded) + if (!this.IsBase64String(value)) + { + throw new ArgumentException("Invalid nonce format"); + } + + return $"'nonce-{value}'"; + } + + /// + /// Validates hash source. + /// + private string ValidateHashSource(string value) + { + string[] hashPrefixes = { "sha256-", "sha384-", "sha512-" }; + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Hash cannot be empty"); + } + + // Check if the value starts with a valid hash prefix and has a base64 encoded value + bool isValidHash = hashPrefixes.Any(prefix => + value.StartsWith(prefix) && this.IsBase64String(value.Substring(prefix.Length))); + + if (!isValidHash) + { + throw new ArgumentException($"Invalid hash format: {value}"); + } + + return $"'{value}'"; + } + + /// + /// Checks if a string is a valid Base64 string. + /// + private bool IsBase64String(string value) + { + try + { + Convert.FromBase64String(value); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceType.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceType.cs new file mode 100644 index 00000000000..496ab88dbb0 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceType.cs @@ -0,0 +1,57 @@ +// 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.ContentSecurityPolicy +{ + /// + /// Represents different types of Content Security Policy source types. + /// + public enum CspSourceType + { + /// + /// Permet de spécifier des domaines spécifiques comme source. + /// + Host, + + /// + /// Permet de spécifier des protocoles (ex: https:, data:) comme source. + /// + Scheme, + + /// + /// Autorise les ressources de la même origine ('self'). + /// + Self, + + /// + /// Autorise l'utilisation de code inline ('unsafe-inline'). + /// + Inline, + + /// + /// Autorise l'utilisation de eval() ('unsafe-eval'). + /// + Eval, + + /// + /// Utilise un nonce cryptographique pour valider les ressources. + /// + Nonce, + + /// + /// Utilise un hash cryptographique pour valider les ressources. + /// + Hash, + + /// + /// N'autorise aucune source ('none'). + /// + None, + + /// + /// Active le mode strict-dynamic pour le chargement des scripts. + /// + StrictDynamic, + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceTypeNameMapper.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceTypeNameMapper.cs new file mode 100644 index 00000000000..8e6750d7602 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/CspSourceTypeNameMapper.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.ContentSecurityPolicy +{ + using System; + + /// + /// Utility class for converting source types to their string representations. + /// + public static class CspSourceTypeNameMapper + { + /// + /// Gets the source type name string. + /// + /// The source type to get the name for. + /// The source type name string. + public static string GetSourceTypeName(CspSourceType sourceType) + { + return sourceType switch + { + CspSourceType.Host => "host", + CspSourceType.Scheme => "scheme", + CspSourceType.Self => "'self'", + CspSourceType.Inline => "'unsafe-inline'", + CspSourceType.Eval => "'unsafe-eval'", + CspSourceType.Nonce => "nonce", + CspSourceType.Hash => "hash", + CspSourceType.None => "none", + CspSourceType.StrictDynamic => "strict-dynamic", + _ => throw new ArgumentException("Unknown source type") + }; + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/DocumentCspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DocumentCspContributor.cs new file mode 100644 index 00000000000..3457dfe3ce9 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DocumentCspContributor.cs @@ -0,0 +1,114 @@ +// 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.ContentSecurityPolicy +{ + using System; + using System.Linq; + + /// + /// Contributor for document-level directives. + /// + public class DocumentCspContributor : BaseCspContributor + { + /// + /// Initializes a new instance of the class. + /// + /// The directive type to create the contributor for. + /// The value of the directive. + public DocumentCspContributor(CspDirectiveType directiveType, string value) + { + this.DirectiveType = directiveType; + this.SetDirectiveValue(value); + } + + /// + /// Gets value of the document directive. + /// + public string DirectiveValue { get; private set; } + + /// + /// Sets the directive value with validation. + /// + /// The value to set for the directive. + public void SetDirectiveValue(string value) + { + this.ValidateDirectiveValue(this.DirectiveType, value); + this.DirectiveValue = value; + } + + /// + /// Generates the directive string. + /// + /// The directive string. + public override string GenerateDirective() + { + if (this.DirectiveType == CspDirectiveType.UpgradeInsecureRequests) + { + return $"{CspDirectiveNameMapper.GetDirectiveName(this.DirectiveType)}"; + } + + if (string.IsNullOrWhiteSpace(this.DirectiveValue)) + { + return string.Empty; + } + + return $"{CspDirectiveNameMapper.GetDirectiveName(this.DirectiveType)} {this.DirectiveValue}"; + } + + /// + /// Validates directive value based on directive type. + /// + private void ValidateDirectiveValue(CspDirectiveType type, string value) + { + switch (type) + { + case CspDirectiveType.PluginTypes: + this.ValidatePluginTypes(value); + break; + case CspDirectiveType.SandboxDirective: + this.ValidateSandboxDirective(value); + break; + + // Add more specific validations as needed + } + } + + /// + /// Validates plugin types. + /// + private void ValidatePluginTypes(string value) + { + string[] validPluginTypes = { "application/pdf", "image/svg+xml" }; + var types = value.Split(' '); + + if (types.Any(t => !validPluginTypes.Contains(t))) + { + throw new ArgumentException("Invalid plugin type"); + } + } + + /// + /// Validates sandbox directive values. + /// + private void ValidateSandboxDirective(string value) + { + string[] validSandboxValues = + { + "allow-forms", + "allow-scripts", + "allow-same-origin", + "allow-top-navigation", + "allow-popups", + }; + + var values = value.Split(' '); + + if (values.Any(v => !validSandboxValues.Contains(v))) + { + throw new ArgumentException("Invalid sandbox directive value"); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/DotNetNuke.ContentSecurityPolicy.csproj b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DotNetNuke.ContentSecurityPolicy.csproj new file mode 100644 index 00000000000..14e0b2ee646 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/DotNetNuke.ContentSecurityPolicy.csproj @@ -0,0 +1,41 @@ + + + + netstandard2.0 + false + true + $(MSBuildProjectDirectory)\..\.. + bin/$(Configuration)/$(TargetFramework)/DotNetNuke.ContentSecurityPolicy.xml + + + true + latest + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + SolutionInfo.cs + + + + + + + + + + + + + diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/IContentSecurityPolicy.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/IContentSecurityPolicy.cs new file mode 100644 index 00000000000..08f9a531776 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/IContentSecurityPolicy.cs @@ -0,0 +1,139 @@ +// 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.ContentSecurityPolicy +{ + /// + /// Interface définissant les opérations de gestion de la Content Security Policy. + /// + public interface IContentSecurityPolicy + { + /// + /// Gets a cryptographically secure nonce value for the CSP policy. + /// + string Nonce { get; } + + /// + /// Gets the default source contributor. + /// + SourceCspContributor DefaultSource { get; } + + /// + /// Gets the script source contributor. + /// + SourceCspContributor ScriptSource { get; } + + /// + /// Gets the style source contributor. + /// + SourceCspContributor StyleSource { get; } + + /// + /// Gets the image source contributor. + /// + SourceCspContributor ImgSource { get; } + + /// + /// Gets the connect source contributor. + /// + SourceCspContributor ConnectSource { get; } + + /// + /// Gets the font source contributor. + /// + SourceCspContributor FontSource { get; } + + /// + /// Gets the object source contributor. + /// + SourceCspContributor ObjectSource { get; } + + /// + /// Gets the media source contributor. + /// + SourceCspContributor MediaSource { get; } + + /// + /// Gets the frame source contributor. + /// + SourceCspContributor FrameSource { get; } + + /// + /// Gets the frame ancestors contributor. + /// + SourceCspContributor FrameAncestors { get; } + + /// + /// Gets the Form action source contributor. + /// + SourceCspContributor FormAction { get; } + + /// + /// Gets the base URI source contributor. + /// + SourceCspContributor BaseUriSource { get; } + + /// + /// Supprimer une source de script à la politique. + /// + /// Le type de source CSP à supprimer. + void RemoveScriptSources(CspSourceType cspSourceType); + + /// + /// Ajoute des types de plugins à la politique. + /// + /// Le type de plugin à autoriser. + void AddPluginTypes(string value); + + /// + /// Ajoute une directive sandbox à la politique. + /// + /// Les options de la directive sandbox. + void AddSandboxDirective(string value); + + /// + /// Ajoute une action de formulaire à la politique. + /// + /// Le type de source CSP à ajouter. + /// L'URL autorisée pour la soumission du formulaire. + void AddFormAction(CspSourceType sourceType, string value); + + /// + /// Ajoute des ancêtres de frame à la politique. + /// + /// Le type de source CSP à ajouter. + /// L'URL autorisée comme ancêtre de frame. + void AddFrameAncestors(CspSourceType sourceType, string value); + + /// + /// Ajoute une URI de rapport à la politique. + /// + /// Le nom où les rapports de violation seront envoyés. + /// L'URI où les rapports de violation seront envoyés. + public void AddReportEndpoint(string name, string value); + + /// + /// Ajoute une destination de rapport à la politique. + /// + /// L'endpoint où envoyer les rapports. + void AddReportTo(string value); + + /// + /// Génère la politique de sécurité complète. + /// + /// La politique de sécurité complète sous forme de chaîne. + string GeneratePolicy(); + + /// + /// Génère la politique de sécurité complète. + /// + /// Reporting Endpoints sous forme de chaîne. + string GenerateReportingEndpoints(); + + /// + /// Upgrade Insecure Requests. + /// + void UpgradeInsecureRequests(); + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/README.md b/DNN Platform/DotNetNuke.ContentSecurityPolicy/README.md new file mode 100644 index 00000000000..5fbe0b22543 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/README.md @@ -0,0 +1,278 @@ +# DotNetNuke.ContentSecurityPolicy + +This library provides a comprehensive Content Security Policy (CSP) implementation for DotNetNuke Platform. It offers a fluent API for building, managing, and generating CSP headers to enhance web application security by preventing various types of attacks such as Cross-Site Scripting (XSS) and data injection attacks. + +## Overview + +Content Security Policy (CSP) is a security standard that helps prevent cross-site scripting (XSS), clickjacking, and other code injection attacks by declaring which dynamic resources are allowed to load. This library provides a robust implementation for managing CSP directives in DotNetNuke applications. + +## Core Interface: IContentSecurityPolicy + +The `IContentSecurityPolicy` interface is the main entry point for managing Content Security Policy directives. It provides access to various source contributors and methods for configuring security policies. + +### Key Features + +- **Cryptographic Nonce Generation**: Automatically generates secure nonce values for CSP policies +- **Multiple Source Types**: Support for various CSP source types (self, inline, eval, host, scheme, nonce, hash, etc.) +- **Directive Management**: Comprehensive support for all major CSP directives +- **Fluent API**: Easy-to-use method chaining for building policies +- **Reporting**: Built-in support for CSP violation reporting + +## Supported Directives + +The interface provides access to the following CSP directive contributors: + +### Source-based Directives +- **DefaultSource**: Fallback for other source directives (`default-src`) +- **ScriptSource**: Controls script execution (`script-src`) +- **StyleSource**: Controls stylesheet loading (`style-src`) +- **ImgSource**: Controls image sources (`img-src`) +- **ConnectSource**: Controls AJAX, WebSocket, and EventSource connections (`connect-src`) +- **FontSource**: Controls font loading (`font-src`) +- **ObjectSource**: Controls plugins and embedded objects (`object-src`) +- **MediaSource**: Controls audio and video elements (`media-src`) +- **FrameSource**: Controls iframe sources (`frame-src`) +- **FrameAncestors**: Controls embedding in frames (`frame-ancestors`) +- **FormAction**: Controls form submission targets (`form-action`) +- **BaseUriSource**: Controls base URI restrictions (`base-uri`) + +### Document Directives +- **Plugin Types**: Restrict allowed plugin types +- **Sandbox**: Apply sandbox restrictions +- **Upgrade Insecure Requests**: Automatically upgrade HTTP to HTTPS + +### Reporting +- **Report Endpoints**: Configure violation reporting endpoints +- **Report To**: Specify report destination + +## Usage Examples + +### Basic Setup + +```csharp +// Create a new CSP instance +IContentSecurityPolicy csp = new ContentSecurityPolicy(); + +// Configure basic security policy +csp.DefaultSource.AddSelf(); +csp.ScriptSource.AddSelf().AddNonce(csp.Nonce); +csp.StyleSource.AddSelf().AddInline(); +csp.ImgSource.AddSelf().AddScheme("data:"); +``` + +### Advanced Configuration + +```csharp +// Configure script sources with strict security +csp.ScriptSource + .AddSelf() + .AddHost("cdn.example.com") + .AddNonce(csp.Nonce) + .AddStrictDynamic(); + +// Configure style sources +csp.StyleSource + .AddSelf() + .AddHost("fonts.googleapis.com") + .AddHash("sha256-abc123..."); + +// Configure frame restrictions +csp.FrameAncestors.AddNone(); // Prevent embedding in frames + +// Configure form actions +csp.AddFormAction(CspSourceType.Self, null); +csp.AddFormAction(CspSourceType.Host, "secure.example.com"); +``` + +### Working with Nonces + +```csharp +// Get the cryptographically secure nonce +string nonce = csp.Nonce; + +// Use nonce in script tags +// + +// Nonce is automatically applied when using AddNonce() +csp.ScriptSource.AddNonce(nonce); +``` + +### Removing Sources + +```csharp +// Remove unsafe script sources +csp.RemoveScriptSources(CspSourceType.Inline); +csp.RemoveScriptSources(CspSourceType.Eval); +``` + +### Reporting Configuration + +```csharp +// Configure violation reporting +csp.AddReportEndpoint("default", "/api/csp-reports"); +csp.AddReportTo("default"); +``` + +### Generating Policy Headers + +```csharp +// Generate the complete CSP header value +string cspHeader = csp.GeneratePolicy(); +// Result: "default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'" + +// Generate reporting endpoints header +string reportingHeader = csp.GenerateReportingEndpoints(); +``` + +## Source Types + +The library supports all standard CSP source types through the `CspSourceType` enumeration: + +- **Host**: Specific domains (e.g., `example.com`, `*.example.com`) +- **Scheme**: Protocol schemes (e.g., `https:`, `data:`, `blob:`) +- **Self**: Same origin (`'self'`) +- **Inline**: Inline scripts/styles (`'unsafe-inline'`) +- **Eval**: Dynamic code evaluation (`'unsafe-eval'`) +- **Nonce**: Cryptographic nonce (`'nonce-abc123'`) +- **Hash**: Cryptographic hash (`'sha256-abc123'`) +- **None**: Block all sources (`'none'`) +- **StrictDynamic**: Enable strict dynamic loading (`'strict-dynamic'`) + +## Security Best Practices + +1. **Avoid `'unsafe-inline'`**: Use nonces or hashes instead +2. **Avoid `'unsafe-eval'`**: Prevents code injection attacks +3. **Use `'strict-dynamic'`**: For modern browsers with script loading +4. **Implement reporting**: Monitor CSP violations +5. **Start restrictive**: Begin with strict policies and relax as needed +6. **Test thoroughly**: Ensure all legitimate resources load correctly + +## Implementation Details + +### Key Classes + +- **ContentSecurityPolicy**: Main implementation of `IContentSecurityPolicy` +- **SourceCspContributor**: Manages source-based directives +- **CspSource**: Represents individual CSP sources +- **CspDirectiveType**: Enumeration of all CSP directive types +- **CspSourceType**: Enumeration of all CSP source types + +### Thread Safety + +The CSP implementation is designed to be used within a single request context. For multi-threaded scenarios, create separate instances per thread or request. + +### Performance Considerations + +- Nonce generation uses cryptographically secure random number generation +- Policy generation is optimized for minimal string operations +- Source deduplication prevents duplicate entries + +## Class Diagram + +```mermaid +classDiagram + class IContentSecurityPolicy { + <> + +string Nonce + +SourceCspContributor DefaultSource + +SourceCspContributor ScriptSource + +SourceCspContributor StyleSource + +SourceCspContributor ImgSource + +SourceCspContributor ConnectSource + +SourceCspContributor FontSource + +SourceCspContributor ObjectSource + +SourceCspContributor MediaSource + +SourceCspContributor FrameSource + +SourceCspContributor FrameAncestors + +SourceCspContributor FormAction + +SourceCspContributor BaseUriSource + +RemoveScriptSources(CspSourceType) void + +AddPluginTypes(string) void + +AddSandboxDirective(string) void + +AddFormAction(CspSourceType, string) void + +AddFrameAncestors(CspSourceType, string) void + +AddReportEndpoint(string, string) void + +AddReportTo(string) void + +GeneratePolicy() string + +GenerateReportingEndpoints() string + +UpgradeInsecureRequests() void + } + + class ContentSecurityPolicy { + + } + + class BaseCspContributor { + <> + +Guid Id + +CspDirectiveType DirectiveType + +GenerateDirective()* string + } + + class SourceCspContributor { + -List~CspSource~ Sources + +bool InlineForBackwardCompatibility + +AddInline() SourceCspContributor + +AddSelf() SourceCspContributor + +AddSourceEval() SourceCspContributor + +AddHost(string) SourceCspContributor + +AddScheme(string) SourceCspContributor + +AddNonce(string) SourceCspContributor + +AddHash(string) SourceCspContributor + +AddNone() SourceCspContributor + +AddStrictDynamic() SourceCspContributor + +AddSource(CspSource) SourceCspContributor + +RemoveSources(CspSourceType) void + +GenerateDirective() string + +GetSourcesByType(CspSourceType) IEnumerable~CspSource~ + } + + class ReportingCspContributor { + -List~string~ reportingEndpoints + +AddReportingEndpoint(string) void + +GenerateDirective() string + } + + class ReportingEndpointContributor { + -List~ReportingEndpoint~ reportingEndpoints + +AddReportingEndpoint(string, string) void + +GenerateDirective() string + } + + class CspSource { + +CspSourceType Type + +string Value + +CspSource(CspSourceType, string) + +ToString() string + -ValidateSource(CspSourceType, string) string + -ValidateHostSource(string) string + -ValidateSchemeSource(string) string + -ValidateNonceSource(string) string + -ValidateHashSource(string) string + -IsBase64String(string) bool + } + + + + IContentSecurityPolicy <|.. ContentSecurityPolicy : implements + BaseCspContributor <|-- SourceCspContributor : extends + BaseCspContributor <|-- DocumentCspContributor : extends + BaseCspContributor <|-- ReportingCspContributor : extends + BaseCspContributor <|-- ReportingEndpointContributor : extends + ContentSecurityPolicy *-- BaseCspContributor : contains + SourceCspContributor *-- CspSource : contains + + + +``` + +## Integration with DotNetNuke + +This library integrates seamlessly with the DotNetNuke Platform's security infrastructure and can be used within: + +- Custom modules +- Skin implementations +- HTTP modules and handlers +- API controllers +- Page rendering pipelines + diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/ReportingCspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ReportingCspContributor.cs new file mode 100644 index 00000000000..d217b472c5d --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ReportingCspContributor.cs @@ -0,0 +1,114 @@ +// 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.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Text.RegularExpressions; + + /// + /// Contributor for reporting directives. + /// + public class ReportingCspContributor : BaseCspContributor + { + /// + /// Initializes a new instance of the class. + /// + /// Le type de directive de rapport (ReportUri ou ReportTo). + public ReportingCspContributor(CspDirectiveType directiveType) + { + if (directiveType != CspDirectiveType.ReportUri && directiveType != CspDirectiveType.ReportTo) + { + throw new ArgumentException("Invalid reporting directive type"); + } + + this.DirectiveType = directiveType; + } + + /// + /// Gets collection of reporting endpoints. + /// + private List ReportingEndpoints { get; } = new List(); + + /// + /// Adds a reporting endpoint. + /// + /// L'URL de l'endpoint où envoyer les rapports. + public void AddReportingEndpoint(string endpoint) + { + this.ValidateReportingEndpoint(endpoint); + if (!this.ReportingEndpoints.Contains(endpoint)) + { + this.ReportingEndpoints.Add(endpoint); + } + } + + /// + /// Removes a reporting endpoint. + /// + /// The endpoint to remove. + public void RemoveReportingEndpoint(string endpoint) + { + this.ReportingEndpoints.Remove(endpoint); + } + + /// + /// Generates the directive string. + /// + /// The directive string. + public override string GenerateDirective() + { + if (!this.ReportingEndpoints.Any()) + { + return string.Empty; + } + + return $"{CspDirectiveNameMapper.GetDirectiveName(this.DirectiveType)} {string.Join(" ", this.ReportingEndpoints)}"; + } + + /// + /// Validates reporting endpoint. + /// + private void ValidateReportingEndpoint(string value) + { + switch (this.DirectiveType) + { + case CspDirectiveType.ReportUri: + this.ValidateReportUri(value); + break; + case CspDirectiveType.ReportTo: + this.ValidateReportTo(value); + break; + + // Add more specific validations as needed + } + } + + private void ValidateReportTo(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Reporting to cannot be empty"); + } + } + + private void ValidateReportUri(string endpoint) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + throw new ArgumentException("Reporting endpoint cannot be empty"); + } + + // URL validation regex + var urlRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(:\d+)?(/.*)?$"); + if (!urlRegex.IsMatch(endpoint)) + { + throw new ArgumentException($"Invalid reporting endpoint: {endpoint}"); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/ReportingEndpointContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ReportingEndpointContributor.cs new file mode 100644 index 00000000000..16fe0a89dcf --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/ReportingEndpointContributor.cs @@ -0,0 +1,116 @@ +// 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.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Text.RegularExpressions; + + /// + /// Contributor for reporting directives. + /// + public class ReportingEndpointContributor : BaseCspContributor + { + /// + /// Initializes a new instance of the class. + /// + /// Le type de directive de rapport (ReportUri). + public ReportingEndpointContributor(CspDirectiveType directiveType) + { + if (directiveType != CspDirectiveType.ReportUri) + { + throw new ArgumentException("Invalid reporting directive type"); + } + + this.DirectiveType = directiveType; + } + + /// + /// Gets collection of reporting endpoints. + /// + private Dictionary ReportingEndpoints { get; } = new Dictionary(); + + /// + /// Adds a reporting endpoint. + /// + /// Le nom de l'endpoint où envoyer les rapports. + /// L'URL de l'endpoint où envoyer les rapports. + public void AddReportingEndpoint(string name, string endpoint) + { + this.ValidateReportingEndpoint(endpoint); + if (!this.ReportingEndpoints.ContainsKey(name)) + { + this.ReportingEndpoints.Add(name, endpoint); + } + } + + /// + /// Removes a reporting endpoint. + /// + /// The endpoint to remove. + public void RemoveReportingEndpoint(string name) + { + this.ReportingEndpoints.Remove(name); + } + + /// + /// Generates the directive string. + /// + /// The directive string. + public override string GenerateDirective() + { + if (!this.ReportingEndpoints.Any()) + { + return string.Empty; + } + + var endpoints = this.ReportingEndpoints.Select(ep => $"{ep.Key}=\"{ep.Value}\"").ToList(); + return $"{string.Join(" ", endpoints)}"; + } + + /// + /// Validates reporting endpoint. + /// + private void ValidateReportingEndpoint(string value) + { + switch (this.DirectiveType) + { + case CspDirectiveType.ReportUri: + this.ValidateReportUri(value); + break; + case CspDirectiveType.ReportTo: + this.ValidateReportTo(value); + break; + + // Add more specific validations as needed + } + } + + private void ValidateReportTo(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Reporting to cannot be empty"); + } + } + + private void ValidateReportUri(string endpoint) + { + if (string.IsNullOrWhiteSpace(endpoint)) + { + throw new ArgumentException("Reporting endpoint cannot be empty"); + } + + // URL validation regex + var urlRegex = new Regex(@"^(https?://)?([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(:\d+)?(/.*)?$"); + if (!urlRegex.IsMatch(endpoint)) + { + throw new ArgumentException($"Invalid reporting endpoint: {endpoint}"); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/SourceCspContributor.cs b/DNN Platform/DotNetNuke.ContentSecurityPolicy/SourceCspContributor.cs new file mode 100644 index 00000000000..b18f84e8028 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/SourceCspContributor.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.ContentSecurityPolicy +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + /// + /// Contributor for fetch directives (sources-based directives). + /// + public class SourceCspContributor : BaseCspContributor + { + /// + /// Initializes a new instance of the class. + /// + /// The directive type to create the contributor for. + public SourceCspContributor(CspDirectiveType directiveType) + { + this.DirectiveType = directiveType; + } + + /// + /// Gets or sets a value indicating whether the inline source is used for backward compatibility. + /// + public bool InlineForBackwardCompatibility { get; set; } + + /// + /// Gets collection of allowed sources. + /// + private List Sources { get; } = new List(); + + /// + /// Adds a source with inline type to the contributor. + /// + /// The current instance for method chaining. + public SourceCspContributor AddInline() + { + return this.AddSource(new CspSource(CspSourceType.Inline)); + } + + /// + /// Ajoute une source 'self' qui autorise les ressources de la même origine. + /// + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddSelf() + { + return this.AddSource(new CspSource(CspSourceType.Self)); + } + + /// + /// Ajoute une source 'unsafe-eval' qui autorise l'utilisation de eval(). + /// + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddSourceEval() + { + return this.AddSource(new CspSource(CspSourceType.Eval)); + } + + /// + /// Ajoute un hôte spécifique comme source autorisée. + /// + /// L'hôte à autoriser (ex: example.com). + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddHost(string host) + { + return this.AddSource(new CspSource(CspSourceType.Host, host)); + } + + /// + /// Ajoute un schéma comme source autorisée. + /// + /// Le schéma à autoriser (ex: https:, data:). + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddScheme(string scheme) + { + return this.AddSource(new CspSource(CspSourceType.Scheme, scheme)); + } + + /// + /// Ajoute un nonce cryptographique comme source autorisée. + /// + /// La valeur du nonce à utiliser. + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddNonce(string nonce) + { + return this.AddSource(new CspSource(CspSourceType.Nonce, nonce)); + } + + /// + /// Ajoute un hash cryptographique comme source autorisée. + /// + /// La valeur du hash à utiliser. + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddHash(string hash) + { + return this.AddSource(new CspSource(CspSourceType.Hash, hash)); + } + + /// + /// Ajoute une source 'none' qui bloque toutes les sources. + /// + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddNone() + { + return this.AddSource(new CspSource(CspSourceType.None)); + } + + /// + /// Ajoute une source 'strict-dynamic' qui active le chargement dynamique strict des scripts. + /// + /// L'instance courante pour chaîner les méthodes. + public SourceCspContributor AddStrictDynamic() + { + return this.AddSource(new CspSource(CspSourceType.StrictDynamic)); + } + + /// + /// Adds a source to the contributor. + /// + /// The source to add. + /// The current instance for method chaining. + public SourceCspContributor AddSource(CspSource source) + { + if (!this.Sources.Any(s => s.Type == source.Type && s.Value == source.Value)) + { + this.Sources.Add(source); + } + + return this; + } + + /// + /// Removes a source from the contributor. + /// + /// The type of the source to remove. + public void RemoveSources(CspSourceType sourceType) + { + this.Sources.RemoveAll(s => s.Type == sourceType); + } + + /// + /// Generates the directive string. + /// + /// The directive string. + public override string GenerateDirective() + { + if (!this.Sources.Any()) + { + return string.Empty; + } + + if (this.Sources.Any(s => s.Type == CspSourceType.Inline) && !this.InlineForBackwardCompatibility) + { + this.RemoveSources(CspSourceType.Nonce); + this.RemoveSources(CspSourceType.StrictDynamic); + } + + return $"{CspDirectiveNameMapper.GetDirectiveName(this.DirectiveType)} {string.Join(" ", this.Sources.Select(s => s.ToString()))}"; + } + + /// + /// Gets sources by type. + /// + /// The type of sources to get. + /// The sources of the specified type. + public IEnumerable GetSourcesByType(CspSourceType type) + { + return this.Sources.Where(s => s.Type == type); + } + } +} diff --git a/DNN Platform/DotNetNuke.ContentSecurityPolicy/dnn.json b/DNN Platform/DotNetNuke.ContentSecurityPolicy/dnn.json new file mode 100644 index 00000000000..c4d47cd6957 --- /dev/null +++ b/DNN Platform/DotNetNuke.ContentSecurityPolicy/dnn.json @@ -0,0 +1,16 @@ +{ + "projectType": "library", + "name": "Dnn_DotNetNukeContentSecurityPolicy", + "friendlyName": "Dnn DotNetNukeContentSecurityPolicy", + "description": "Dnn DotNetNukeContentSecurityPolicy Library", + "packageName": "Dnn_DotNetNukeContentSecurityPolicy", + "folder": "Dnn/DotNetNukeContentSecurityPolicy", + "library": {}, + "pathsAndFiles": { + "pathToAssemblies": "./bin", + "pathToScripts": "./Server/SqlScripts", + "assemblies": ["DotNetNuke.ContentSecurityPolicy.dll"], + "releaseFiles": [], + "zipName": "Dnn.DotNetNukeContentSecurityPolicy" + } +} diff --git a/DNN Platform/DotNetNuke.Web.Client/DotNetNuke.Web.Client.csproj b/DNN Platform/DotNetNuke.Web.Client/DotNetNuke.Web.Client.csproj index 57f9470a362..09d6a3fb056 100644 --- a/DNN Platform/DotNetNuke.Web.Client/DotNetNuke.Web.Client.csproj +++ b/DNN Platform/DotNetNuke.Web.Client/DotNetNuke.Web.Client.csproj @@ -63,6 +63,9 @@ ..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + @@ -75,6 +78,24 @@ + + ..\..\packages\Microsoft.AspNet.WebPages.3.3.0\lib\net45\System.Web.Helpers.dll + + + ..\..\packages\Microsoft.AspNet.Mvc.5.3.0\lib\net45\System.Web.Mvc.dll + + + ..\..\packages\Microsoft.AspNet.Razor.3.3.0\lib\net45\System.Web.Razor.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 + @@ -105,6 +126,7 @@ + diff --git a/DNN Platform/DotNetNuke.Web.Client/Providers/DnnStandardRenderer.cs b/DNN Platform/DotNetNuke.Web.Client/Providers/DnnStandardRenderer.cs new file mode 100644 index 00000000000..83c0d9bb86d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.Client/Providers/DnnStandardRenderer.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.Client.Providers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + using ClientDependency.Core.FileRegistration.Providers; + + public class DnnStandardRenderer : StandardRenderer + { + private readonly ClientResourceSettings dnnSettingsHelper = new ClientResourceSettings(); + + /// + /// Gets a value indicating whether checks if the composite files option is set for the current portal (DNN site settings). + /// If not enabled at the portal level it defers to the core CDF setting (web.config). + /// + public override bool EnableCompositeFiles + { + get + { + var settingsVersion = this.dnnSettingsHelper.AreCompositeFilesEnabled(); + return settingsVersion.HasValue ? settingsVersion.Value : base.EnableCompositeFiles; + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.Client/packages.config b/DNN Platform/DotNetNuke.Web.Client/packages.config index 3a9bbb1430d..88c577c2a34 100644 --- a/DNN Platform/DotNetNuke.Web.Client/packages.config +++ b/DNN Platform/DotNetNuke.Web.Client/packages.config @@ -1,6 +1,9 @@  + + + 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 1e709c4ff37..5e1e2323bcc 100644 --- a/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs +++ b/DNN Platform/DotNetNuke.Web.Mvc/Framework/Controllers/DnnController.cs @@ -2,61 +2,61 @@ // 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.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.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; } } /// @@ -78,78 +78,114 @@ public ActionResult ResultOfLastExecute public ViewEngineCollection ViewEngineCollectionEx { get; set; } /// - public string LocalizeString(string key) - { - return Localization.GetString(key, this.LocalResourceFile); - } - - 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); + } + + 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..9a085659605 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/PropertyHelper.cs @@ -0,0 +1,138 @@ +// 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; + + 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); + + 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. + /// propertyInfo to extract the getter for. + /// a fast getter. + /// 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; + } + + public object GetValue(object instance) + { + // Contract.Assert(valueGetter != null, "Must call Initialize before using this object"); + return this.valueGetter(instance); + } + + 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..762ad68e3c4 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Commons/TypeHelper.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.Commons +{ + using System.Web.Routing; + + 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. + /// + 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..6368bb8e3f6 --- /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..8e77d24f19a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.Title.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.Containers +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..893b75e0384 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Containers/SkinHelpers.cs @@ -0,0 +1,10 @@ +using DotNetNuke.Abstractions.ClientResources; +using Microsoft.Extensions.DependencyInjection; + +namespace DotNetNuke.Web.MvcPipeline.Containers +{ + 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..ba617a5402a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/DnnPageController.cs @@ -0,0 +1,29 @@ +// 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; + + public abstract class DnnPageController : Controller, IMvcController + { + /// Initializes a new instance of the class. + protected DnnPageController(IServiceProvider dependencyProvider) + { + this.DependencyProvider = dependencyProvider; + } + public IServiceProvider DependencyProvider { get; private set; } + + 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..6a9d9db23cb --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/IMvcController.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 + +namespace DotNetNuke.Web.MvcPipeline.Controllers +{ + using System.Web.Mvc; + + 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..6d04d98b24c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleControllerBase.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.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; + + public class ModuleControllerBase : DnnPageController, IMvcController + { + private readonly Lazy activeModule; + + public ModuleControllerBase(IServiceProvider dependencyProvider) : + base(dependencyProvider) + { + this.activeModule = new Lazy(this.InitModuleInfo); + } + + public PortalSettings PortalSettings + { + get + { + return PortalController.Instance.GetCurrentPortalSettings(); + } + } + + /// Gets userInfo for the current user. + public UserInfo UserInfo + { + get { return this.PortalSettings.UserInfo; } + } + + 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..a3dcc4acecf --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Controllers/ModuleViewControllerBase.cs @@ -0,0 +1,295 @@ +// 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; + + public abstract class ModuleViewControllerBase : Controller, IMvcController + { + private string localResourceFile; + private ModuleInstanceContext moduleContext; + + public ModuleViewControllerBase() + { + } + + public bool IsHostMenu + { + get + { + return Globals.IsHostTab(this.PortalSettings.ActiveTab.TabID); + } + } + + 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; + } + } + + public bool IsEditable + { + get + { + return this.ModuleContext.IsEditable; + } + } + + public int PortalId + { + get + { + return this.ModuleContext.PortalId; + } + } + + public int TabId + { + get + { + return this.ModuleContext.TabId; + } + } + + public UserInfo UserInfo + { + get + { + return this.PortalSettings.UserInfo; + } + } + + public int UserId + { + get + { + return this.PortalSettings.UserId; + } + } + + public PortalAliasInfo PortalAlias + { + get + { + return this.PortalSettings.PortalAlias; + } + } + + 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; + } + } + public string HelpURL + { + get + { + return this.ModuleContext.HelpURL; + } + + set + { + this.ModuleContext.HelpURL = value; + } + } + + public ModuleInfo ModuleConfiguration + { + get + { + return this.ModuleContext.Configuration; + } + + set + { + this.ModuleContext.Configuration = value; + } + } + + public int TabModuleId + { + get + { + return this.ModuleContext.TabModuleId; + } + + set + { + this.ModuleContext.TabModuleId = value; + } + } + + 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; + } + } + + public virtual string FolderName + { + get + { + return this.moduleContext.Configuration.DesktopModule.FolderName; + } + } + public virtual string ResourceName + { + get + { + return this.GetType().Name.Replace("ViewController", ""); + } + } + + public string EditUrl() + { + return this.ModuleContext.EditUrl(); + } + + public string EditUrl(string controlKey) + { + return this.ModuleContext.EditUrl(controlKey); + } + + public string EditUrl(string keyName, string keyValue) + { + return this.ModuleContext.EditUrl(keyName, keyValue); + } + + public string EditUrl(string keyName, string keyValue, string controlKey) + { + return this.ModuleContext.EditUrl(keyName, keyValue, controlKey); + } + + public string EditUrl(string keyName, string keyValue, string controlKey, params string[] additionalParameters) + { + return this.ModuleContext.EditUrl(keyName, keyValue, controlKey, additionalParameters); + } + + public string EditUrl(int tabID, string controlKey, bool pageRedirect, params string[] additionalParameters) + { + return this.ModuleContext.NavigateUrl(tabID, controlKey, pageRedirect, additionalParameters); + } + + public int GetNextActionID() + { + return this.ModuleContext.GetNextActionID(); + } + + protected string LocalizeString(string key) + { + return Localization.GetString(key, this.LocalResourceFile); + } + + protected string LocalizeSafeJsString(string key) + { + return Localization.GetSafeJSString(key, this.LocalResourceFile); + } + + [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; + } + moduleContext.Configuration = activeModule; + var model = this.ViewModel(); + return this.PartialView(ViewName(), model); + } + + protected abstract object ViewModel(); + + protected abstract string ViewName(); + + + } +} 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..853f80f9c17 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/DotNetNuke.Web.MvcPipeline.csproj @@ -0,0 +1,62 @@ + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..bd97ffa7c0f --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/AccesDeniedException.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.Exceptions +{ + using System; + using System.Runtime.Serialization; + + [Serializable] + public class AccesDeniedException : MvcPageException + { + public AccesDeniedException() + { + } + + public AccesDeniedException(string message, string redirectUrl) + : base(message, redirectUrl) + { + } + + public AccesDeniedException(string message, Exception innerException) + : base(message, innerException) + { + } + + 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..cf9a19d9825 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/DisabledPageException.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.Exceptions +{ + using System; + using System.Runtime.Serialization; + + [Serializable] + public class DisabledPageException : MvcPageException + { + public DisabledPageException() + { + } + + public DisabledPageException(string message) + : base(message) + { + } + + public DisabledPageException(string message, string redirectUrl) + : base(message, redirectUrl) + { + } + + public DisabledPageException(string message, Exception innerException) + : base(message, innerException) + { + } + + 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..3362d937139 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/MvcPageException.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.Exceptions +{ + using System; + using System.Runtime.Serialization; + + public class MvcPageException : Exception + { + public MvcPageException() + { + } + + public MvcPageException(string message) + : base(message) + { + } + + public MvcPageException(string message, string redirectUrl) + : base(message) + { + this.RedirectUrl = redirectUrl; + } + + public MvcPageException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected MvcPageException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + 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..74156548b42 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Exceptions/NotFoundException.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.Exceptions +{ + using System; + using System.Runtime.Serialization; + + [Serializable] + public class NotFoundException : MvcPageException + { + public NotFoundException() + { + } + + public NotFoundException(string message) + : base(message) + { + } + + public NotFoundException(string message, string redirectUrl) + : base(message, redirectUrl) + { + } + + public NotFoundException(string message, Exception innerException) + : base(message, innerException) + { + } + + 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..0539d3c3641 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServiceFrameworkInternals.cs @@ -0,0 +1,13 @@ +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + using System.Web.Mvc; + + internal interface IMvcServiceFrameworkInternals + { + bool IsAjaxAntiForgerySupportRequired { get; } + + bool IsAjaxScriptSupportRequired { get; } + void RegisterAjaxAntiForgery(); + 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..cd01a95527f --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/IMvcServicesFramework.cs @@ -0,0 +1,15 @@ +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + /// + /// Do not implement. This interface is only implemented by the DotNetNuke core framework. Outside the framework it should used as a type and for unit test purposes only. + /// There is no guarantee that this interface will not change. + /// + public interface IMvcServicesFramework + { + /// Will cause anti forgery tokens to be included in the current page. + void RequestAjaxAntiForgerySupport(); + + /// Will cause ajax scripts to 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..6390dd8a9d3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/JavascriptLibraries/MvcJavaScript.cs @@ -0,0 +1,330 @@ +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..500f390e19b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFramework.cs @@ -0,0 +1,43 @@ +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 + { + 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..68a0b17c907 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkImpl.cs @@ -0,0 +1,120 @@ +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; + + 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..e3f28756e6c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Framework/MvcServicesFrameworkInternal.cs @@ -0,0 +1,14 @@ +namespace DotNetNuke.Web.MvcPipeline.Framework +{ + using System; + using DotNetNuke.Framework; + + 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..2e3e5399c43 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/HtmlHelpers.cs @@ -0,0 +1,97 @@ +// 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; + + public static partial class HtmlHelpers + { + 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); + } + + 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); + } + + public static IHtmlString Control(this HtmlHelper htmlHelper, ModuleInfo module) + { + return htmlHelper.Control(module.ModuleControl.ControlSrc, module); + } + + 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); + } + + public static IHtmlString RegisterAjaxScriptIfRequired(this HtmlHelper htmlHelper) + { + if (MvcServicesFrameworkInternal.Instance.IsAjaxScriptSupportRequired) + { + MvcServicesFrameworkInternal.Instance.RegisterAjaxScript(); + } + return new MvcHtmlString(string.Empty); + } + + public static IHtmlString AntiForgeryIfRequired(this HtmlHelper htmlHelper) + { + if (ServicesFrameworkInternal.Instance.IsAjaxAntiForgerySupportRequired) + { + return AntiForgery.GetHtml(); + } + + return new MvcHtmlString(string.Empty); + } + + 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; + } + internal static IClientResourceController GetClientResourcesController(HtmlHelper htmlHelper) + { + return GetDependencyProvider(htmlHelper).GetRequiredService(); + + //var serviceProvider = Common.Globals.GetCurrentServiceProvider(); + //return serviceProvider.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..30c10af1b8a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ContainerModelFactory.cs @@ -0,0 +1,229 @@ +// 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; + + public class ContainerModelFactory : IContainerModelFactory + { + private readonly IClientResourceController clientResourceController; + + 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..a06d9e1dd8e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IContainerModelFactory.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.ModelFactories +{ + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Web.MvcPipeline.Models; + + public interface IContainerModelFactory + { + 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..8053207baf3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPageModelFactory.cs @@ -0,0 +1,14 @@ +// 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; + + public interface IPageModelFactory + { + 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..46be767d049 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/IPaneModelFactory.cs @@ -0,0 +1,19 @@ +// 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; + + public interface IPaneModelFactory + { + PaneModel CreatePane(string name); + + PaneModel InjectModule(PaneModel pane, ModuleInfo module, PortalSettings portalSettings); + + 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..3183943c37a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/ISkinModelFactory.cs @@ -0,0 +1,14 @@ +// 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; + + public interface ISkinModelFactory + { + 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..d3f223c2929 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PageModelFactory.cs @@ -0,0 +1,269 @@ +// 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; + + 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; + + 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..58652d6d396 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/PaneModelFactory.cs @@ -0,0 +1,314 @@ +// 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; + + public class PaneModelFactory : IPaneModelFactory + { + private readonly IContainerModelFactory containerModelFactory; + + 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; + } + + 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..5c9cb3862b2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModelFactories/SkinModelFactory.cs @@ -0,0 +1,727 @@ +// 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; + + 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; + + 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..108914a4dc5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ContainerModel.cs @@ -0,0 +1,141 @@ +// 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; + + public class ContainerModel + { + private ModuleInfo moduleConfiguration; + private ModuleHostModel moduleHost; + private PortalSettings portalSettings; + + public ContainerModel(ModuleInfo moduleConfiguration, PortalSettings portalSettings) + { + this.moduleConfiguration = moduleConfiguration; + this.moduleHost = new ModuleHostModel(moduleConfiguration); + this.portalSettings = portalSettings; + } + + public PortalSettings PortalSettings + { + get + { + return this.portalSettings; + } + } + public ModuleHostModel ModuleHost + { + get + { + return this.moduleHost; + } + } + + public IModuleControl ModuleControl + { + get + { + IModuleControl moduleControl = null; + if (this.ModuleHost != null) + { + moduleControl = this.ModuleHost.ModuleControl; + } + + return moduleControl; + } + } + + public string ID { get; internal set; } + + public string ContainerPath + { + get + { + return Path.GetDirectoryName(this.ContainerSrc) + "/"; + } + } + + public string ContainerSrc { get; internal set; } + + public string ActionName + { + get + { + if (this.moduleConfiguration.ModuleControl.ControlKey == "Module") + { + return "LoadDefaultSettings"; + } + else + { + return string.IsNullOrEmpty(this.ModuleName) ? "Index" : this.FileNameWithoutExtension; + } + } + } + + public string ControllerName + { + get + { + if (this.moduleConfiguration.ModuleControl.ControlKey == "Module") + { + return "ModuleSettings"; + } + else + { + return string.IsNullOrEmpty(this.ModuleName) ? this.FileNameWithoutExtension : this.ModuleName; + } + } + } + + public string ContainerRazorFile + { + get + { + return "~" + Path.GetDirectoryName(this.ContainerSrc) + "/Views/" + Path.GetFileName(this.ContainerSrc).Replace(".ascx", ".cshtml"); + } + } + + public ModuleInfo ModuleConfiguration + { + get + { + return this.moduleConfiguration; + } + } + + public bool EditMode { get; internal set; } + + public string Footer { get; internal set; } + + public string Header { get; internal set; } + + public string ContentPaneCssClass { get; internal set; } + + public string ContentPaneStyle { get; internal set; } + + 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..471571b6589 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ControlViewModel.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.Models +{ + public class ControlViewModel + { + public int ModuleId { get; set; } + + public int TabId { get; set; } + + public int ModuleControlId { get; set; } + + public string PanaName { get; set; } + + public string ContainerSrc { get; set; } + + public string ContainerPath { get; set; } + + 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..3c4bdc4a436 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleActionsModel.cs @@ -0,0 +1,38 @@ +namespace DotNetNuke.Web.MvcPipeline.Models +{ + using System.Collections.Generic; + + using DotNetNuke.Entities.Modules; + + public class ModuleActionsModel + { + // public ModuleInstanceContext ModuleContext { get; internal set; } + public ModuleInfo ModuleContext { get; set; } + + public bool SupportsQuickSettings { get; set; } + + public bool DisplayQuickSettings { get; set; } + + public object QuickSettingsModel { get; set; } + + public string CustomActionsJSON { get; set; } + + public string AdminActionsJSON { get; set; } + + public string Panes { get; set; } + + public string CustomText { get; set; } + + public string AdminText { get; set; } + + public string MoveText { get; set; } + + public bool SupportsMove { get; set; } + + public bool IsShared { get; set; } + + public string ModuleTitle { get; set; } + + 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..5f2245dcbe2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleHostModel.cs @@ -0,0 +1,85 @@ +// 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; + + /// Project : DotNetNuke + /// Namespace: DotNetNuke.UI.Modules + /// Class : ModuleHost + /// ModuleHost hosts a Module Control (or its cached Content). + 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; + + 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 ModuleControl. + /// An IModuleControl. + public IModuleControl ModuleControl + { + get + { + // Make sure the Control tree has been created + // this.EnsureChildControls(); + return this.control as IModuleControl; + } + } + + public string CssClass { get; private set; } + + /// Gets a flag that indicates whether the Module is in View Mode. + /// A Boolean. + 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..5cb289baa55 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/ModuleModelBase.cs @@ -0,0 +1,14 @@ +// 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 +{ + public class ModuleModelBase + { + public int ModuleId { get; set; } + + public int TabId { get; set; } + + 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..f39ef010125 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PageModel.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.Models +{ + using System.Collections.Generic; + + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Abstractions.Pages; + + public class PageModel + { + public int? TabId { get; set; } + + public string Language { get; set; } + + public int? PortalId { get; set; } + + public SkinModel Skin { get; set; } + + public string AntiForgery { get; set; } + + public Dictionary ClientVariables { get; set; } + + public string PageHeadText { get; set; } + + public string PortalHeadText { get; set; } + + public string Title { get; set; } + + public string BackgroundUrl { get; set; } + + public string MetaRefresh { get; set; } + + public string Description { get; set; } + + public string KeyWords { get; set; } + + public string Copyright { get; set; } + + public string Generator { get; set; } + + public string MetaRobots { get; set; } + + public Dictionary StartupScripts { get; set; } + + public bool IsEditMode { get; set; } + + public string FavIconLink { get; set; } + + public string CanonicalLinkUrl { get; set; } + + //TODO: CSP - enable when CSP implementation is ready + //public IContentSecurityPolicy ContentSecurityPolicy { get; set; } + + public INavigationManager NavigationManager { get; set; } + + public IClientResourceController ClientResourceController { get; set; } + 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..bf163a5c918 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/PaneModel.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.Models +{ + using System.Collections.Generic; + + public class PaneModel + { + private Dictionary containers; + + public PaneModel(string name) + { + this.Name = name; + } + + public string CssClass { get; set; } + + 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..8cf21f4f69b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredScript.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 + +using DotNetNuke.Abstractions.ClientResources; + +namespace DotNetNuke.Web.MvcPipeline.Models +{ + public class RegisteredScript + { + public string Script { get; set; } + + 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..455a30d2f00 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/RegisteredStylesheet.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 + +using DotNetNuke.Abstractions.ClientResources; + +namespace DotNetNuke.Web.MvcPipeline.Models +{ + public class RegisteredStylesheet + { + public string Stylesheet { get; set; } + + 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..4c1ed128974 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Models/SkinModel.cs @@ -0,0 +1,78 @@ +// 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; + + public class SkinModel + { + private Dictionary panes; + + public string SkinSrc { get; set; } + + public Dictionary Panes + { + get + { + return this.panes ?? (this.panes = new Dictionary()); + } + } + + public string RazorFile + { + get + { + return Path.GetDirectoryName(this.SkinSrc).Replace("\\", "/") + "/Views/" + Path.GetFileName(this.SkinSrc).Replace(".ascx", ".cshtml"); + } + } + + public string SkinPath + { + get + { + return Path.GetDirectoryName(this.SkinSrc).Replace("\\", "/") + "/"; + } + } + + public string ControlPanelRazor { get; set; } + + public string PaneCssClass + { + get + { + /* + if (Globals.IsEditMode()) + { + return "dnnSortable"; + } + */ + return string.Empty; + } + } + + public string BodyCssClass + { + get + { + if (Globals.IsEditMode()) + { + return "dnnEditState"; + } + + return string.Empty; + } + } + + public string SkinError { get; set; } + + public List RegisteredStylesheets { get; set; } = new List(); + + 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..93396e4852f --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/DefaultMvcModuleControlBase.cs @@ -0,0 +1,255 @@ +// 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; + + public abstract class DefaultMvcModuleControlBase : IMvcModuleControl, IDisposable + { + private readonly Lazy serviceScopeContainer = new Lazy(ServiceScopeContainer.GetRequestOrCreateScope); + private string localResourceFile; + private ModuleInstanceContext moduleContext; + + public ModuleInfo ModuleConfiguration + { + get + { + return ModuleContext.Configuration; + } + } + + public int TabId + { + get + { + return this.ModuleContext.TabId; + } + } + + public int ModuleId + { + get + { + return this.ModuleContext.ModuleId; + } + } + + public int TabModuleId + { + get + { + return this.ModuleContext.TabModuleId; + } + } + + public bool IsHostMenu + { + get + { + return Globals.IsHostTab(this.PortalSettings.ActiveTab.TabID); + } + } + + public PortalSettings PortalSettings + { + get + { + return PortalController.Instance.GetCurrentPortalSettings(); + } + } + + public int PortalId + { + get + { + return this.ModuleContext.PortalId; + } + } + + public UserInfo UserInfo + { + get + { + return this.PortalSettings.UserInfo; + } + } + + public int UserId + { + get + { + return this.PortalSettings.UserId; + } + } + + public PortalAliasInfo PortalAlias + { + get + { + return this.PortalSettings.PortalAlias; + } + } + + public Hashtable Settings + { + get + { + return this.ModuleContext.Settings; + } + } + + /// Gets the underlying base control for this ModuleControl. + /// A String. + public Control Control { get; set; } + + /// Gets the Name for this control. + /// A String. + public virtual string ControlName + { + get + { + if (string.IsNullOrEmpty(this.ModuleConfiguration.ModuleControl.ControlKey)) + { + return "View"; + } + else + { + return this.ModuleConfiguration.ModuleControl.ControlKey; + } + } + } + + /// Gets or Sets the Path for this control (used primarily for UserControls). + /// A String. + public virtual string ControlPath + { + get + { + if (this.ModuleConfiguration.DesktopModule != null) + { + return "DesktopModules/" + this.ModuleConfiguration.DesktopModule.FolderName; + } + return string.Empty; + } + } + + 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; + } + } + + public ModuleActionCollection Actions + { + get + { + return this.ModuleContext.Actions; + } + + set + { + this.ModuleContext.Actions = value; + } + } + + 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. + 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; + + public int GetNextActionID() + { + return this.ModuleContext.GetNextActionID(); + } + + /// + public void Dispose() + { + // base.Dispose(); + if (this.serviceScopeContainer.IsValueCreated) + { + this.serviceScopeContainer.Value.Dispose(); + } + } + + public abstract IHtmlString Html(HtmlHelper htmlHelper); + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Demo/DemoModuleControl.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Demo/DemoModuleControl.cs new file mode 100644 index 00000000000..bc8a0d1b2ac --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Demo/DemoModuleControl.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Demo +{ + public class DemoModuleControl : RazorModuleControlBase + { + public override string ControlName => "DemoModuleControl"; + + public override string ControlPath => "DesktopModules/Demo"; + + public override IRazorModuleResult Invoke() + { + if (Request.QueryString["view"] == "Terms") + { + return Terms(); + } + else if (Request.QueryString["view"] == "Privacy") + { + return Privacy(); + } + else + { + return View("~/admin/Portal/Views/Terms.cshtml", "Hello from DemoModuleControl - Default view"); + } + } + + private IRazorModuleResult Privacy() + { + return View("~/admin/Portal/Views/Privacy.cshtml", "Hello from DemoModuleControl - Privacy view"); + } + + private IRazorModuleResult Terms() + { + return View("~/Views/Default/Terms.cshtml", "Hello from DemoModuleControl - Terms view"); + } + } +} 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..d4139dc5252 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/IMvcModuleControl.cs @@ -0,0 +1,14 @@ +using System; +using System.Web; +using System.Web.Mvc; +using DotNetNuke.UI.Modules; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + public interface IMvcModuleControl : IModuleControl + { + /// Gets the control Html + 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..c7027221a6c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/ModuleControlFactory.cs @@ -0,0 +1,55 @@ +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + using System; + using DotNetNuke.Common; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Framework; + + internal class ModuleControlFactory + { + public static IMvcModuleControl CreateModuleControl(ModuleInfo module) + { + return CreateModuleControl(module, module.ModuleControl.ControlSrc); + } + + 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..5ab7f61af1e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/MvcModuleControl.cs @@ -0,0 +1,225 @@ +// 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; + + public class MvcModuleControl : DefaultMvcModuleControlBase + { + + private const string ExcludedQueryStringParams = "tabid,mid,ctl,language,popup,action,controller"; + private const string ExcludedRouteValues = "mid,ctl,popup"; + + public override string ControlName + { + get + { + return RouteActionName; + } + } + + + /// Gets or Sets the Path for this control (used primarily for UserControls). + /// A String. + public override string ControlPath + { + get + { + return string.Format("~/DesktopModules/MVC/{0}", ModuleConfiguration.DesktopModule.FolderName); + } + } + + /// Gets or sets the action name for the MVC route. + /// A String. + public string RouteActionName + { + get + { + var segments = GetSegments(); + if (segments.Length < 2) + { + return ""; + } + if (segments.Length == 3) + { + return segments[2]; + } + else + { + return segments[1]; + } + } + } + + /// Gets or sets the controller name for the MVC route. + /// A String. + public string RouteControllerName + { + get + { + var segments = GetSegments(); + if (segments.Length < 2) + { + return string.Empty; + } + return segments[1]; + } + } + + public string RouteNamespace + { + get + { + var segments = GetSegments(); + if (segments.Length < 1) + { + return string.Empty; + } + return segments[0]; + } + } + + public string[] GetSegments() + { + return this.ModuleConfiguration.ModuleControl.ControlSrc.Replace(".mvc", string.Empty).Split('/'); + } + + 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 = 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", RouteActionName); + controllerName = queryString.GetValueOrDefault("controller", 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(RouteNamespace)) + { + // routeData.DataTokens.Add("namespaces", new string[] { routeNamespace }); + } + } + + string[] routeValues = { $"moduleId={ModuleConfiguration.ModuleID}", $"controller={RouteControllerName}", $"action={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 == ModuleConfiguration.ModuleID; + + var url = isMyRoute ? + request.Url.ToString() : + TestableGlobals.Instance.NavigateURL(ModuleConfiguration.TabID, TestableGlobals.Instance.IsHostTab(ModuleConfiguration.TabID), 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..051934d5b20 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/IPageContributor.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 + +using DotNetNuke.Abstractions.ClientResources; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Page +{ + public interface IPageContributor + { + // ModuleResources ModuleResources {get;} + + 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..96f2b15a605 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Page/PageConfigurationContext.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 + +using System; +using DotNetNuke.Abstractions.ClientResources; +using DotNetNuke.Abstractions.Pages; +using DotNetNuke.Framework; +using DotNetNuke.Framework.JavaScriptLibraries; +using Microsoft.Extensions.DependencyInjection; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Page +{ + public class PageConfigurationContext + { + public PageConfigurationContext(IServiceProvider serviceProvider) + { + ClientResourceController = serviceProvider.GetService(); + PageService = serviceProvider.GetService(); + ServicesFramework = DotNetNuke.Framework.ServicesFramework.Instance; + JavaScriptLibraryHelper = serviceProvider.GetService(); + } + + public IClientResourceController ClientResourceController { get; private set; } + + public IPageService PageService { get; private set; } + + public IServicesFramework ServicesFramework { get; private set; } + + 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..8568c73a74b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ContentRazorModuleResult.cs @@ -0,0 +1,20 @@ +using System.Web; +using System.Web.Mvc; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + public class ContentRazorModuleResult : IRazorModuleResult + { + public ContentRazorModuleResult(string content) + { + this.content = content; + } + + 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..048b282c8ca --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ErrorRazorModuleResult.cs @@ -0,0 +1,24 @@ +using System.Web; +using System.Web.Mvc; +using DotNetNuke.Services.Mail; +using DotNetNuke.Web.MvcPipeline.Modules; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + public class ErrorRazorModuleResult : IRazorModuleResult + { + public ErrorRazorModuleResult(string heading, string message) + { + this.Heading = heading; + this.Message = message; + } + + public string Heading { get; private set; } + 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..7de42810294 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IRazorModuleResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using DotNetNuke.Web.MvcPipeline.Utils; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + public interface IRazorModuleResult + { + 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..53358d4255c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/IViewRenderer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + public interface IViewRenderer + { + 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..979de5f5211 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/RazorModuleViewContext.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + public class RazorModuleViewContext + { + public HttpContextBase HttpContext { get; internal set; } + + /// + /// Gets the . + /// + /// + /// This is an alias for ViewContext.ViewData. + /// + 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..b38d1baa5de --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/Razor/ViewRazorModuleResult.cs @@ -0,0 +1,32 @@ +using System; +using System.Web; +using System.Web.Mvc; +using System.Web.Mvc.Html; +using DotNetNuke.Web.MvcPipeline.Utils; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.Razor +{ + public class ViewRazorModuleResult : IRazorModuleResult + { + public ViewRazorModuleResult(string viewName, object model, ViewDataDictionary ViewData) + { + this.ViewName = viewName; + this.Model = model; + this.ViewData = ViewData ; + } + + public string ViewName { get; private set; } + public object Model { get; private set; } + + /// + /// Gets or sets the . + /// + public ViewDataDictionary ViewData { get; private set; } + + public IHtmlString Execute(HtmlHelper htmlHelper) + { + return htmlHelper.Partial(ViewName, Model, 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..114690a116c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/RazorModuleControlBase.cs @@ -0,0 +1,127 @@ +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; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl +{ + public abstract class RazorModuleControlBase : DefaultMvcModuleControlBase + { + private RazorModuleViewContext _viewContext; + 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); + } + + protected virtual string DefaultViewName + { + get + { + return "~/" + this.ControlPath.Replace('\\', '/').Trim('/') + "/Views/" + this.ControlName + ".cshtml"; + } + } + + 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); + } + + public IRazorModuleResult Error(string heading, string message) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + return new ErrorRazorModuleResult(heading, message); + } + + public IRazorModuleResult View() + { + return View(null); + } + + public IRazorModuleResult View(string viewName) + { + return View(viewName, null); + } + + public IRazorModuleResult View(object model) + { + return View(null, model); + } + public IRazorModuleResult View(string viewName, object model) + { + if (string.IsNullOrEmpty(viewName)) + { + viewName = this.DefaultViewName; + } + return new ViewRazorModuleResult(viewName, model, ViewData); + } + + public RazorModuleViewContext ViewContext + { + get + { + if (_viewContext == null) + { + _viewContext = new RazorModuleViewContext(); + _viewContext.HttpContext = new System.Web.HttpContextWrapper(System.Web.HttpContext.Current); + } + + return _viewContext; + } + set + { + if (value == null) + { + throw new ArgumentNullException(); + } + + _viewContext = value; + } + } + + /// + /// Gets the . + /// + public HttpContextBase HttpContext => ViewContext.HttpContext; + + /// + /// Gets the . + /// + public HttpRequestBase Request => ViewContext.HttpContext.Request; + + /// + /// Gets the . + /// + public ViewDataDictionary ViewData => 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..d7a876a9382 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/SpaModuleControl.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.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; + + public class SpaModuleControl : DefaultMvcModuleControlBase, IPageContributor + { + private readonly IBusinessControllerProvider businessControllerProvider; + private readonly IClientResourceController clientResourceController; + + public SpaModuleControl(): base() + { + this.businessControllerProvider = Globals.DependencyProvider.GetRequiredService(); + var serviceProvider = Common.Globals.GetCurrentServiceProvider(); + this.clientResourceController = serviceProvider.GetRequiredService(); + } + + public SpaModuleControl(IBusinessControllerProvider businessControllerProvider, IClientResourceController clientResourceController) : base() + { + this.businessControllerProvider = businessControllerProvider; + this.clientResourceController = clientResourceController; + } + + public string html5File => ModuleConfiguration.ModuleControl.ControlSrc; + + 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(); + } + } + } + + 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))); + } + + + } +} 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..e9f35da0f80 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/ModuleControl/WebForms/WrapperModule.cs @@ -0,0 +1,73 @@ +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; + +namespace DotNetNuke.Web.MvcPipeline.ModuleControl.WebForms +{ + + 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(); + + // Make sure child controls are created when needed + protected override void CreateChildControls() + { + Controls.Clear(); + Controls.Add(new LiteralControl(html)); + // important so ASP.NET tracks the created controls across postbacks + ChildControlsCreated = true; + base.CreateChildControls(); + } + + // ensure child controls exist early in page lifecycle + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + try + { + var mc = ModuleControlFactory.CreateModuleControl(this.ModuleConfiguration); + html = MvcViewEngine.RenderHtmlHelperToString(helper => mc.Html(helper)); + if (mc is IActionable) + { + var moduleControl = (IActionable)mc; + this.ModuleActions = moduleControl.ModuleActions; + } + if (mc is IPageContributor) + { + var moduleControl = (IPageContributor)mc; + moduleControl.ConfigurePage(new PageConfigurationContext(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); + html = "
" + ex.Message + "
"; + } + 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..4c47f221542 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleActionsControl.cs @@ -0,0 +1,268 @@ +// 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.Razor; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Page; + + + public class ModuleActionsControl : RazorModuleControlBase, IPageContributor + { + private ILog Logger = LoggerSource.Instance.GetLogger(typeof(ModuleActionsControl)); + private readonly List validIDs = new List(); + private ModuleAction actionRoot; + + private Dictionary actionScripts = new Dictionary(); + + public bool EditMode + { + get + { + return Personalization.GetUserMode() != PortalSettings.Mode.View; + } + } + + 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; + } + } + + protected string AdminText + { + get { return Localization.GetString("ModuleGenericActions.Action", Localization.GlobalResourceFile); } + } + + protected string CustomText + { + get { return Localization.GetString("ModuleSpecificActions.Action", Localization.GlobalResourceFile); } + } + + protected string MoveText + { + get { return Localization.GetString(ModuleActionType.MoveRoot, Localization.GlobalResourceFile); } + } + + protected PortalSettings PortalSettings + { + get + { + return this.ModuleContext.PortalSettings; + } + } + + protected string AdminActionsJSON { get; set; } + + protected string CustomActionsJSON { get; set; } + + protected bool DisplayQuickSettings { get; set; } + + protected string Panes { get; set; } + + protected bool SupportsMove { get; set; } + + protected bool SupportsQuickSettings { get; set; } + + protected bool IsShared { get; set; } + + protected string ModuleTitle { get; set; } + + protected ModuleActionCollection Actions + { + get + { + return this.ModuleControl.ModuleContext.Actions; + } + } + + public IModuleControl ModuleControl { get; set; } + + 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); + } + + protected string LocalizeString(string key) + { + return Localization.GetString(key, Localization.GlobalResourceFile); + } + + 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; + } + } + + 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..f5e0d1f0bd5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.DnnLabel.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.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; + + public static partial class ModuleHelpers + { + 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..cc9d8dcf8b1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.ModuleMessage.cs @@ -0,0 +1,144 @@ +// 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.Client.ClientResourceManagement; + using DotNetNuke.Web.MvcPipeline.Framework.JavascriptLibraries; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + + 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..2a3cd1220bb --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/ModuleHelpers.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.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; + + public static partial class ModuleHelpers + { + public static IHtmlString LocalizeString(this HtmlHelper htmlHelper, string key, string localResourceFile) + { + return MvcHtmlString.Create(Localization.GetString(key, localResourceFile)); + } + + 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)); + } + + 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()); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + 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); + } + + 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..b659cc4a9ed --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/MoluleHelpers.Actions.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.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; + + public static partial class ModuleHelpers + { + 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..75a6ab7ba01 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Modules/PermissionTriStateHelper.cs @@ -0,0 +1,152 @@ +// 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; + + public static class PermissionTriStateHelper + { + 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)); + } + + 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..dddfe3151fa --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Pages/HtmlHelpers.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.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; + + public static partial class HtmlHelpers + { + + 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()); + } + + 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..8033169a705 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ConstraintValidation.cs @@ -0,0 +1,41 @@ +// 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 + { + 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..52d05d4481c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageHandler.cs @@ -0,0 +1,55 @@ +// 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; + + public class DnnMvcPageHandler : MvcHandler, IHttpHandler, IRequiresSessionState + { + 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); + } + + 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..556e29d916e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/DnnMvcPageRouteHandler.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.Routing +{ + using System; + using System.Web; + using System.Web.Mvc; + using System.Web.Routing; + using System.Web.SessionState; + + 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); + } + + protected virtual IHttpHandler GetHttpHandler(RequestContext requestContext) + { + requestContext.HttpContext.SetSessionStateBehavior(this.GetSessionStateBehavior(requestContext)); + return new DnnMvcPageHandler(requestContext); + } + + 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..b31e7f1ffcd --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpConfigurationExtensions.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.Collections.Concurrent; + using System.Collections.Generic; + using System.Web.Http; + + using DotNetNuke.Common; + + public static class HttpConfigurationExtensions + { + private const string Key = "MvcPipelineTabAndModuleInfoProvider"; + + 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); + } + + 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..7d4c8128d41 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/HttpRequestExtensions.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.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; + + internal static class HttpRequestExtensions + { + private delegate bool TryMethod(ITabAndModuleInfoProvider provider, HttpRequestBase request, out T output); + + public static int FindTabId(this HttpRequestBase request) + { + return IterateTabAndModuleInfoProviders(request, TryFindTabId, -1); + } + + public static ModuleInfo FindModuleInfo(this HttpRequestBase request) + { + return IterateTabAndModuleInfoProviders(request, TryFindModuleInfo, null); + } + + public static int FindModuleId(this HttpRequestBase request) + { + return IterateTabAndModuleInfoProviders(request, TryFindModuleId, -1); + } + + 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); + } + + 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..73270b4eea9 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMapRoute.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.Routing +{ + using System; + using System.Collections.Generic; + using System.Web.Routing; + + 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..067ba2cd898 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IMvcRouteMapper.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.Web.MvcPipeline.Routing +{ + public interface IMvcRouteMapper + { + 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..da147e5eb05 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/IPortalAliasMvcRouteManager.cs @@ -0,0 +1,29 @@ +// 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; + + internal interface IPortalAliasMvcRouteManager + { + IEnumerable GetRoutePrefixCounts(); + + string GetRouteName(string moduleFolderName, string routeName, int count); + + RouteValueDictionary GetAllRouteValues(IPortalAliasInfo portalAliasInfo, object routeValues); + + string GetRouteUrl(string moduleFolderName, string url, int count); + + 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..a3ddfd6b140 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/ITabAndModuleInfoProvider.cs @@ -0,0 +1,19 @@ +// 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; + + public interface ITabAndModuleInfoProvider + { + bool TryFindTabId(HttpRequestBase request, out int tabId); + + bool TryFindModuleId(HttpRequestBase request, out int moduleId); + + 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..56320a52c7d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRouteExtensions.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.Routing +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Web.Routing; + + public static class MvcRouteExtensions + { + private const string NamespaceKey = "namespaces"; + private const string NameKey = "name"; + + /// Get the name of the route. + /// Route name. + public static string GetName(this Route route) + { + return (string)route.DataTokens[NameKey]; + } + + internal static void SetNameSpaces(this Route route, string[] namespaces) + { + route.DataTokens[NamespaceKey] = namespaces; + } + + /// Get Namespaces that are searched for controllers for this route. + /// Namespaces. + internal static string[] GetNameSpaces(this Route route) + { + return (string[])route.DataTokens[NamespaceKey]; + } + + 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..30eb111e20f --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/MvcRoutingManager.cs @@ -0,0 +1,230 @@ +// 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; + + 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; + + public MvcRoutingManager() + : this(RouteTable.Routes) + { + } + + internal MvcRoutingManager(RouteCollection routes) + { + this.routes = routes; + this.portalAliasMvcRouteManager = new PortalAliasMvcRouteManager(); + this.TypeLocator = new TypeLocator(); + } + + internal ITypeLocator TypeLocator { get; set; } + + 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..baa7ff3e0cd --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/PortalAliasMvcRouteManager.cs @@ -0,0 +1,164 @@ +// 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; + + 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); + } + + 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..48826680e11 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Routing/StandardTabAndModuleInfoProvider.cs @@ -0,0 +1,171 @@ +// 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; + + 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/Security/Controllers/ModulePermissionsGridController.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Controllers/ModulePermissionsGridController.cs new file mode 100644 index 00000000000..a8e970db223 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Controllers/ModulePermissionsGridController.cs @@ -0,0 +1,376 @@ +// 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.Security.Controllers +{ + using System.Collections.Generic; + using System.Linq; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Users; + using DotNetNuke.Framework; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Security.Roles; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Web.Client.ResourceManager; + using DotNetNuke.Web.MvcPipeline.Security.Models; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + + public class ModulePermissionsGridController : PermissionsGridController + { + private bool inheritViewPermissionsFromTab; + private int moduleId = -1; + private ModulePermissionCollection modulePermissions; + private List permissionsList; + private int viewColumnIndex; + + public ModulePermissionsGridController(IClientResourceController clientResourceController) : base(clientResourceController) + { + this.TabId = -1; + } + + public ModulePermissionCollection ModulePermissions + { + get + { + // First Update Permissions in case they have been changed + // this.UpdateModulePermissions(); + return this.modulePermissions; + } + } + + public bool InheritViewPermissionsFromTab + { + get => this.inheritViewPermissionsFromTab; + set + { + this.inheritViewPermissionsFromTab = value; + this.permissionsList = null; + } + } + + public int ModuleId + { + get => this.moduleId; + set + { + this.moduleId = value; + this.GetModulePermissions(); + } + } + + public int TabId { get; set; } + + protected override List PermissionsList + { + get + { + if (this.permissionsList == null && this.modulePermissions != null) + { + this.permissionsList = this.modulePermissions.ToList(); + } + + return this.permissionsList; + } + } + + public ActionResult Index(int tabId, int moduleId, bool inheritViewPermissionsFromTab) + { + this.inheritViewPermissionsFromTab = inheritViewPermissionsFromTab; + this.TabId = tabId; + this.ModuleId = moduleId; + + ServicesFramework.Instance.RequestAjaxAntiForgerySupport(); + + this.clientResourceController.RegisterScript("~/Resources/Shared/Components/Tokeninput/jquery.tokeninput.js"); + this.clientResourceController.RegisterScript("~/js/dnn.permissiongrid.js"); + + this.clientResourceController.RegisterStylesheet("~/Resources/Shared/Components/Tokeninput/Themes/token-input-facebook.css", FileOrder.Css.ResourceCss); + + var script = "var pgm = new dnn.permissionGridManager('ClientID');"; + MvcClientAPI.RegisterStartupScript("ClientID-PermissionGridManager", script); + + this.BindData(); + + var model = new ModulePermissionsGridViewModel + { + Permissions = this.Permissions, + Users = this.GetUsers(), + Roles = this.GetRolesComboBox(), + RoleGroups = this.GetRoleGroups(), + ModuleId = this.ModuleId, + TabId = this.TabId, + InheritViewPermissionsFromTab = this.InheritViewPermissionsFromTab, + RolePermissions = this.RolePermissions, + }; + + return this.View(model); + } + + protected override void AddPermission(List permissions, UserInfo user) + { + bool isMatch = this.modulePermissions.Cast() + .Any(objModulePermission => objModulePermission.UserID == user.UserID); + + if (!isMatch) + { + foreach (PermissionInfo objPermission in permissions) + { + if (objPermission.PermissionKey == "VIEW") + { + this.AddModulePermission( + objPermission, + int.Parse(Globals.glbRoleNothing), + Null.NullString, + user.UserID, + user.DisplayName, + true); + } + } + } + } + + protected override void AddPermission(List permissions, RoleInfo role) + { + if (this.modulePermissions.Cast().Any(p => p.RoleID == role.RoleID)) + { + return; + } + + foreach (PermissionInfo objPermission in permissions) + { + if (objPermission.PermissionKey == "VIEW") + { + this.AddModulePermission( + objPermission, + role.RoleID, + role.RoleName, + Null.NullInteger, + Null.NullString, + true); + } + } + } + + protected override void UpdateRolePermission(PermissionUpdateModel permission) + { + var permissionInfo = this.GetPermissionInfo(permission.PermissionId); + if (this.InheritViewPermissionsFromTab && permissionInfo.PermissionKey == "VIEW") + { + return; + } + + this.RemovePermission(permission.PermissionId, permission.RoleId, Null.NullInteger); + + if (permission.PermissionKey == PermissionTypeGrant) + { + var role = this.GetRole(permission.RoleId); + this.AddModulePermission( + permissionInfo, + permission.RoleId, + role.RoleName, + Null.NullInteger, + Null.NullString, + true); + } + else if (permission.PermissionKey == PermissionTypeDeny) + { + var role = this.GetRole(permission.RoleId); + this.AddModulePermission( + permissionInfo, + permission.RoleId, + role.RoleName, + Null.NullInteger, + Null.NullString, + false); + } + } + + protected override void UpdateUserPermission(PermissionUpdateModel permission) + { + var permissionInfo = this.GetPermissionInfo(permission.PermissionId); + if (this.InheritViewPermissionsFromTab && permissionInfo.PermissionKey == "VIEW") + { + return; + } + + this.RemovePermission(permission.PermissionId, Null.NullInteger, permission.UserId); + + if (permission.PermissionKey == PermissionTypeGrant || permission.PermissionKey == PermissionTypeDeny) + { + var user = UserController.GetUserById(this.PortalId, permission.UserId); + this.AddModulePermission( + permissionInfo, + Null.NullInteger, + Null.NullString, + permission.UserId, + user.DisplayName, + permission.PermissionKey == PermissionTypeGrant); + } + } + + protected void RemovePermission(int permissionID, int roleID, int userID) + { + this.modulePermissions.Remove(permissionID, roleID, userID); + + // Clear Permission List + this.permissionsList = null; + } + + protected override List GetPermissions() + { + var moduleInfo = ModuleController.Instance.GetModule(this.ModuleId, this.TabId, false); + var permissionController = new PermissionController(); + var permissions = permissionController.GetPermissionsByModule(this.ModuleId, this.TabId).Cast().ToList(); + + var permissionList = new List(); + for (int i = 0; i < permissions.Count; i++) + { + var permission = (PermissionInfo)permissions[i]; + if (permission.PermissionKey == "VIEW") + { + this.viewColumnIndex = i + 1; + permissionList.Add(permission); + } + else if (!(moduleInfo.IsShared && moduleInfo.IsShareableViewOnly)) + { + permissionList.Add(permission); + } + } + + return permissionList; + } + + protected override bool SupportsDenyPermissions(PermissionInfo permissionInfo) + { + return true; + } + + /// + protected override bool GetEnabled(PermissionInfo objPerm, RoleInfo role, int column) + { + bool enabled; + if (this.InheritViewPermissionsFromTab && column == this.viewColumnIndex) + { + enabled = false; + } + else + { + enabled = !this.IsImplicitRole(role.PortalID, role.RoleID); + } + + return enabled; + } + + /// + protected override bool GetEnabled(PermissionInfo objPerm, UserInfo user, int column) + { + bool enabled; + if (this.InheritViewPermissionsFromTab && column == this.viewColumnIndex) + { + enabled = false; + } + else + { + enabled = true; + } + + return enabled; + } + + /// + protected override string GetPermission(PermissionInfo objPerm, RoleInfo role, int column, string defaultState) + { + string permission; + if (this.InheritViewPermissionsFromTab && column == this.viewColumnIndex) + { + permission = PermissionTypeNull; + } + else + { + permission = role.RoleID == this.AdministratorRoleId + ? PermissionTypeGrant + : base.GetPermission(objPerm, role, column, defaultState); + } + + return permission; + } + + /// + protected override string GetPermission(PermissionInfo objPerm, UserInfo user, int column, string defaultState) + { + string permission; + if (this.InheritViewPermissionsFromTab && column == this.viewColumnIndex) + { + permission = PermissionTypeNull; + } + else + { + // Call base class method to handle standard permissions + permission = base.GetPermission(objPerm, user, column, defaultState); + } + + return permission; + } + + /// + protected override bool IsFullControl(PermissionInfo permissionInfo) + { + return (permissionInfo.PermissionKey == "EDIT") && PermissionProvider.Instance().SupportsFullControl(); + } + + /// + protected override bool IsViewPermisison(PermissionInfo permissionInfo) + { + return permissionInfo.PermissionKey == "VIEW"; + } + + private void AddModulePermission(PermissionInfo permission, int roleId, string roleName, int userId, string displayName, bool allowAccess) + { + var objPermission = new ModulePermissionInfo(permission) + { + ModuleID = this.ModuleId, + RoleID = roleId, + RoleName = roleName, + AllowAccess = allowAccess, + UserID = userId, + DisplayName = displayName, + }; + this.modulePermissions.Add(objPermission, true); + this.permissionsList = null; + } + + private void GetModulePermissions() + { + this.modulePermissions = new ModulePermissionCollection( + ModulePermissionController.GetModulePermissions(this.ModuleId, this.TabId)); + this.permissionsList = null; + } + + private void UpdateModulePermissions() + { + // Implementation of permission updates to the database + foreach (ModulePermissionInfo permission in this.modulePermissions) + { + // ModulePermissionController.SaveModulePermission(permission); + } + } + + private bool IsImplicitRole(int portalId, int roleId) + { + return ModulePermissionController.ImplicitRoles(portalId) + .Any(r => r.RoleID == roleId); + } + + private PermissionInfo GetPermissionInfo(int permissionId) + { + return this.GetPermissions().Cast() + .FirstOrDefault(p => p.PermissionID == permissionId); + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Controllers/PermissionsGridController.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Controllers/PermissionsGridController.cs new file mode 100644 index 00000000000..579775cfcc3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Controllers/PermissionsGridController.cs @@ -0,0 +1,652 @@ +// 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.Security.Controllers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Web.Mvc; + + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Security.Roles; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Security.Models; + + public abstract class PermissionsGridController : Controller + { + protected const string PermissionTypeGrant = "True"; + protected const string PermissionTypeDeny = "False"; + protected const string PermissionTypeNull = "Null"; + + protected readonly IClientResourceController clientResourceController; + + protected PermissionsGridController(IClientResourceController clientResourceController) + { + this.clientResourceController = clientResourceController; + } + + private List permissions; + private IList roles; + + protected List UserPermissions { get; set; } + + protected List RolePermissions { get; set; } + + protected virtual List PermissionsList + { + get { return null; } + } + + protected List Permissions + { + get { return this.permissions; } + } + + protected virtual bool RefreshGrid + { + get { return false; } + } + + protected int PortalId + { + get + { + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + return Globals.IsHostTab(portalSettings.ActiveTab.TabID) + ? Null.NullInteger + : portalSettings.PortalId; + } + } + + protected int UnAuthUsersRoleId => int.Parse(Globals.glbRoleUnauthUser); + + protected int AllUsersRoleId => int.Parse(Globals.glbRoleAllUsers); + + protected int AdministratorRoleId => PortalController.Instance.GetCurrentPortalSettings().AdministratorRoleId; + + protected int RegisteredUsersRoleId => PortalController.Instance.GetCurrentPortalSettings().RegisteredRoleId; + + public void BindData() + { + this.permissions = this.GetPermissions(); + this.BindRolesGrid(); + this.BindUsersGrid(); + } + + [HttpPost] + public virtual ActionResult UpdatePermissions(PermissionsUpdateModel model) + { + if (!this.ModelState.IsValid) + { + return this.Json(new { success = false, message = "Invalid model state" }); + } + + try + { + foreach (var permission in model.Permissions) + { + if (permission.IsRolePermission) + { + this.UpdateRolePermission(permission); + } + else + { + this.UpdateUserPermission(permission); + } + } + + return this.Json(new { success = true }); + } + catch (Exception ex) + { + return this.Json(new { success = false, message = ex.Message }); + } + } + + [HttpPost] + public virtual ActionResult AddRole(int roleId) + { + try + { + var role = this.GetRole(roleId); + if (role == null) + { + return this.Json(new { success = false, message = "Role not found" }); + } + + this.AddPermission(this.permissions, role); + + return this.Json(new { success = true }); + } + catch (Exception ex) + { + return this.Json(new { success = false, message = ex.Message }); + } + } + + [HttpPost] + public virtual ActionResult AddUser(int userId) + { + try + { + var user = UserController.GetUserById(this.PortalId, userId); + if (user == null) + { + return this.Json(new { success = false, message = "User not found" }); + } + + this.AddPermission(this.permissions, user); + + return this.Json(new { success = true }); + } + catch (Exception ex) + { + return this.Json(new { success = false, message = ex.Message }); + } + } + + protected abstract List GetPermissions(); + + protected abstract void AddPermission(List permissions, RoleInfo role); + + protected abstract void AddPermission(List permissions, UserInfo user); + + protected abstract void UpdateRolePermission(PermissionUpdateModel permission); + + protected abstract void UpdateUserPermission(PermissionUpdateModel permission); + + protected virtual IEnumerable GetRoleGroups() + { + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + var groups = RoleController.GetRoleGroups(portalSettings.PortalId).Cast(); + + // Add default items + var allGroups = new List + { + new RoleGroupInfo { RoleGroupID = -2, RoleGroupName = Localization.GetString("AllRoles") }, + new RoleGroupInfo { RoleGroupID = -1, RoleGroupName = Localization.GetString("GlobalRoles") }, + }; + + allGroups.AddRange(groups); + return allGroups; + } + + protected virtual List GetRolesComboBox(int roleGroupId = -1) + { + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + var roles = new List(); + + // Get roles based on group filter + if (roleGroupId > -2) + { + roles.AddRange(RoleController.Instance.GetRoles( + portalSettings.PortalId, + r => r.RoleGroupID == roleGroupId && + r.SecurityMode != SecurityMode.SocialGroup && + r.Status == RoleStatus.Approved)); + } + else + { + roles.AddRange(RoleController.Instance.GetRoles( + portalSettings.PortalId, + r => r.SecurityMode != SecurityMode.SocialGroup && + r.Status == RoleStatus.Approved)); + } + + // Add system roles if global roles selected + if (roleGroupId < 0) + { + roles.Add(new RoleInfo + { + RoleID = this.UnAuthUsersRoleId, + RoleName = Globals.glbRoleUnauthUserName, + }); + + roles.Add(new RoleInfo + { + RoleID = this.AllUsersRoleId, + RoleName = Globals.glbRoleAllUsersName, + }); + } + + // Ensure administrator role is always included + this.EnsureRole(roles, portalSettings.AdministratorRoleId); + + // Ensure registered users role is included + this.EnsureRole(roles, portalSettings.RegisteredRoleId); + + return roles; + } + + protected virtual List GetUsers() + { + var users = new List(); + + if (this.PermissionsList == null) + { + return users; + } + + foreach (var permission in this.PermissionsList) + { + if (!Null.IsNull(permission.UserID) && users.Cast().All(u => u.UserID != permission.UserID)) + { + var user = new UserInfo + { + UserID = permission.UserID, + Username = permission.Username, + DisplayName = permission.DisplayName, + }; + users.Add(user); + } + } + + return users; + } + + protected virtual RoleInfo GetRole(int roleId) + { + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + + if (roleId == this.AllUsersRoleId) + { + return new RoleInfo + { + RoleID = this.AllUsersRoleId, + RoleName = Globals.glbRoleAllUsersName, + PortalID = portalSettings.PortalId, + }; + } + + if (roleId == this.UnAuthUsersRoleId) + { + return new RoleInfo + { + RoleID = this.UnAuthUsersRoleId, + RoleName = Globals.glbRoleUnauthUserName, + PortalID = portalSettings.PortalId, + }; + } + + return RoleController.Instance.GetRoleById(portalSettings.PortalId, roleId); + } + + protected virtual void EnsureRole(List roles, int roleId) + { + if (roles.Cast().All(r => r.RoleID != roleId)) + { + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + var role = RoleController.Instance.GetRoleById(portalSettings.PortalId, roleId); + if (role != null) + { + roles.Add(role); + } + } + } + + protected virtual string BuildPermissionKey( + bool allowAccess, + int permissionId, + int objectPermissionId, + int roleId, + string roleName, + int userId = -1, + string displayName = null) + { + var key = allowAccess ? PermissionTypeGrant : PermissionTypeDeny; + + key += $"|{permissionId}|{(objectPermissionId > -1 ? objectPermissionId.ToString() : string.Empty)}"; + key += $"|{roleName}|{roleId}|{userId}|{displayName}"; + + return key; + } + + protected virtual bool GetEnabled(PermissionInfo permission, RoleInfo role, int column) + { + // Base implementation - override in derived classes for specific logic + return true; + } + + protected virtual bool GetEnabled(PermissionInfo permission, UserInfo user, int column) + { + // Base implementation - override in derived classes for specific logic + return true; + } + + protected virtual bool IsFullControl(PermissionInfo permissionInfo) + { + return false; + } + + protected virtual bool IsViewPermisison(PermissionInfo permissionInfo) + { + return false; + } + + protected virtual string GetPermissionState(PermissionInfo permission, RoleInfo role, string defaultState = null) + { + if (this.PermissionsList == null) + { + return defaultState ?? PermissionTypeNull; + } + + foreach (var existingPermission in this.PermissionsList) + { + if ( + existingPermission.PermissionID == permission.PermissionID && + existingPermission.RoleID == role.RoleID) + { + return existingPermission.AllowAccess ? PermissionTypeGrant : PermissionTypeDeny; + } + } + + return defaultState ?? PermissionTypeNull; + } + + protected virtual string GetPermissionState(PermissionInfo permission, UserInfo user, string defaultState = null) + { + if (this.PermissionsList == null) + { + return defaultState ?? PermissionTypeNull; + } + + foreach (var existingPermission in this.PermissionsList) + { + if ( + existingPermission.PermissionID == permission.PermissionID && + existingPermission.UserID == user.UserID) + { + return existingPermission.AllowAccess ? PermissionTypeGrant : PermissionTypeDeny; + } + } + + return defaultState ?? PermissionTypeNull; + } + + protected virtual bool SupportsDenyPermissions(PermissionInfo permissionInfo) + { + // to maintain backward compatibility the base implementation must always call the simple parameterless version of this method + return false; + } + + /// Gets the Value of the permission. + /// The permission being loaded. + /// The user. + /// The column of the Grid. + /// if the permission is granted, otherwise . + protected virtual bool GetPermission(PermissionInfo objPerm, UserInfo user, int column) + { + return Convert.ToBoolean(this.GetPermission(objPerm, user, column, PermissionTypeDeny)); + } + + /// Gets the Value of the permission. + /// The permission being loaded. + /// The user. + /// The column of the Grid. + /// Default State. + /// The permission state (one of , or ). + protected virtual string GetPermission(PermissionInfo objPerm, UserInfo user, int column, string defaultState) + { + var stateKey = defaultState; + if (this.PermissionsList != null) + { + foreach (var permission in this.PermissionsList) + { + if (permission.PermissionID == objPerm.PermissionID && permission.UserID == user.UserID) + { + if (permission.AllowAccess) + { + stateKey = PermissionTypeGrant; + } + else + { + stateKey = PermissionTypeDeny; + } + + break; + } + } + } + + return stateKey; + } + + protected virtual bool GetPermission(PermissionInfo objPerm, RoleInfo role, int column) + { + return Convert.ToBoolean(this.GetPermission(objPerm, role, column, PermissionTypeDeny)); + } + + /// Gets the Value of the permission. + /// The permission being loaded. + /// The role. + /// The column of the Grid. + /// Default State. + /// The permission state (one of , or ). + protected virtual string GetPermission(PermissionInfo objPerm, RoleInfo role, int column, string defaultState) + { + string stateKey = defaultState; + if (this.PermissionsList != null) + { + foreach (PermissionInfoBase permission in this.PermissionsList) + { + if (permission.PermissionID == objPerm.PermissionID && permission.RoleID == role.RoleID) + { + if (permission.AllowAccess) + { + stateKey = PermissionTypeGrant; + } + else + { + stateKey = PermissionTypeDeny; + } + + break; + } + } + } + + return stateKey; + } + + private void BindRolesGrid() + { + this.RolePermissions = new List(); + + /* + this.dtRolePermissions.Columns.Clear(); + this.dtRolePermissions.Rows.Clear(); + + // Add Roles Column + this.dtRolePermissions.Columns.Add(new DataColumn("RoleId")); + + // Add Roles Column + this.dtRolePermissions.Columns.Add(new DataColumn("RoleName")); + + for (int i = 0; i <= this.permissions.Count - 1; i++) + { + var permissionInfo = (PermissionInfo)this.permissions[i]; + + // Add Enabled Column + this.dtRolePermissions.Columns.Add(new DataColumn(permissionInfo.PermissionName + "_Enabled")); + + // Add Permission Column + this.dtRolePermissions.Columns.Add(new DataColumn(permissionInfo.PermissionName)); + } + */ + + this.GetRoles(); + + // this.UpdateRolePermissions(); + for (int i = 0; i <= this.roles.Count - 1; i++) + { + var role = this.roles[i]; + var roleModel = new RoleModel(); + roleModel.RoleId = role.RoleID; + roleModel.RoleName = Localization.LocalizeRole(role.RoleName); + roleModel.Permissions = new Dictionary(); + int j; + for (j = 0; j <= this.permissions.Count - 1; j++) + { + var permModel = new PermissionModel(); + PermissionInfo objPerm; + objPerm = (PermissionInfo)this.permissions[j]; + roleModel.Permissions.Add(objPerm.PermissionName, permModel); + permModel.Enabled = this.GetEnabled(objPerm, role, j + 1); + permModel.Locked = !permModel.Enabled; + if (this.SupportsDenyPermissions(objPerm)) + { + permModel.State = this.GetPermission(objPerm, role, j + 1, PermissionTypeNull); + } + else + { + if (this.GetPermission(objPerm, role, j + 1)) + { + permModel.State = PermissionTypeGrant; + } + else + { + permModel.State = PermissionTypeNull; + } + } + } + + this.RolePermissions.Add(roleModel); + } + } + + private void BindUsersGrid() + { + /* + this.dtUserPermissions.Columns.Clear(); + this.dtUserPermissions.Rows.Clear(); + + // Add Roles Column + var col = new DataColumn("UserId"); + this.dtUserPermissions.Columns.Add(col); + + // Add Roles Column + col = new DataColumn("DisplayName"); + this.dtUserPermissions.Columns.Add(col); + int i; + for (i = 0; i <= this.permissions.Count - 1; i++) + { + PermissionInfo objPerm; + objPerm = (PermissionInfo)this.permissions[i]; + + // Add Enabled Column + col = new DataColumn(objPerm.PermissionName + "_Enabled"); + this.dtUserPermissions.Columns.Add(col); + + // Add Permission Column + col = new DataColumn(objPerm.PermissionName); + this.dtUserPermissions.Columns.Add(col); + } + + if (this.userPermissionsGrid != null) + { + this.users = this.GetUsers(); + + if (this.users.Count != 0) + { + this.userPermissionsGrid.Visible = true; + DataRow row; + for (i = 0; i <= this.users.Count - 1; i++) + { + var user = (UserInfo)this.users[i]; + row = this.dtUserPermissions.NewRow(); + row["UserId"] = user.UserID; + row["DisplayName"] = user.DisplayName; + int j; + for (j = 0; j <= this.permissions.Count - 1; j++) + { + PermissionInfo objPerm; + objPerm = (PermissionInfo)this.permissions[j]; + row[objPerm.PermissionName + "_Enabled"] = this.GetEnabled(objPerm, user, j + 1); + if (this.SupportsDenyPermissions(objPerm)) + { + row[objPerm.PermissionName] = this.GetPermission(objPerm, user, j + 1, PermissionTypeNull); + } + else + { + if (this.GetPermission(objPerm, user, j + 1)) + { + row[objPerm.PermissionName] = PermissionTypeGrant; + } + else + { + row[objPerm.PermissionName] = PermissionTypeNull; + } + } + } + + this.dtUserPermissions.Rows.Add(row); + } + + this.userPermissionsGrid.DataSource = this.dtUserPermissions; + this.userPermissionsGrid.DataBind(); + } + else + { + this.dtUserPermissions.Rows.Clear(); + this.userPermissionsGrid.DataSource = this.dtUserPermissions; + this.userPermissionsGrid.DataBind(); + this.userPermissionsGrid.Visible = false; + } + } + */ + } + + private void GetRoles() + { + var checkedRoles = this.GetCheckedRoles(); + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + this.roles = RoleController.Instance.GetRoles(portalSettings.PortalId, r => r.SecurityMode != SecurityMode.SocialGroup && r.Status == RoleStatus.Approved && checkedRoles.Contains(r.RoleID)); + + if (checkedRoles.Contains(this.UnAuthUsersRoleId)) + { + this.roles.Add(new RoleInfo { RoleID = this.UnAuthUsersRoleId, RoleName = Globals.glbRoleUnauthUserName }); + } + + if (checkedRoles.Contains(this.AllUsersRoleId)) + { + this.roles.Add(new RoleInfo { RoleID = this.AllUsersRoleId, PortalID = portalSettings.PortalId, RoleName = Globals.glbRoleAllUsersName }); + } + + // Administrators Role always has implicit permissions, then it should be always in + this.EnsureRole(RoleController.Instance.GetRoleById(portalSettings.PortalId, portalSettings.AdministratorRoleId)); + + // Show also default roles + this.EnsureRole(RoleController.Instance.GetRoleById(portalSettings.PortalId, portalSettings.RegisteredRoleId)); + this.EnsureRole(new RoleInfo { RoleID = this.AllUsersRoleId, PortalID = portalSettings.PortalId, RoleName = Globals.glbRoleAllUsersName }); + + this.roles.Reverse(); + + // this.roles.Sort(new RoleComparer()); + } + + private IEnumerable GetCheckedRoles() + { + if (this.PermissionsList == null) + { + return new List(); + } + + return this.PermissionsList.Select(r => r.RoleID).Distinct(); + } + + private void EnsureRole(RoleInfo role) + { + if (this.roles.Cast().All(r => r.RoleID != role.RoleID)) + { + this.roles.Add(role); + } + } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/ModulePermissionsGridViewModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/ModulePermissionsGridViewModel.cs new file mode 100644 index 00000000000..6f3b1939c01 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/ModulePermissionsGridViewModel.cs @@ -0,0 +1,19 @@ +// 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.Security.Models +{ + using System.Collections.Generic; + + public class ModulePermissionsGridViewModel : PermissionsGridViewModel + { + public int ModuleId { get; set; } + + public int TabId { get; set; } + + public bool InheritViewPermissionsFromTab { get; set; } + + public List RolePermissions { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionModel.cs new file mode 100644 index 00000000000..0695292837c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionModel.cs @@ -0,0 +1,23 @@ +// 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.Security.Models +{ + public class PermissionModel + { + public bool Enabled { get; set; } + + public string State { get; set; } + + public bool IsFullControl { get; set; } + + public bool IsView { get; set; } + + public bool Locked { get; set; } + + public bool SupportsDenyMode { get; set; } + + public string PermissionKey { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionTriStateModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionTriStateModel.cs new file mode 100644 index 00000000000..e8eceb28310 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionTriStateModel.cs @@ -0,0 +1,23 @@ +// 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.Security.Models +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + using DotNetNuke.Security.Permissions; + + public class PermissionTriStateModel + { + public PermissionInfo Permission { get; set; } + + public int RoleId { get; set; } + + public int UserId { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionUpdateModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionUpdateModel.cs new file mode 100644 index 00000000000..570bbdb1a3d --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionUpdateModel.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.Security.Models +{ + using System; + using System.Collections; + using System.Collections.Generic; + + using DotNetNuke.Entities.Users; + using DotNetNuke.Security.Roles; + + public class PermissionUpdateModel + { + public int PermissionId { get; set; } + + public bool IsRolePermission { get; set; } + + public int RoleId { get; set; } + + public int UserId { get; set; } + + public string PermissionKey { get; set; } + + public bool AllowAccess { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionsGridViewModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionsGridViewModel.cs new file mode 100644 index 00000000000..b6ff5e7d261 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionsGridViewModel.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.Security.Models +{ + using System; + using System.Collections; + using System.Collections.Generic; + + using DotNetNuke.Entities.Users; + using DotNetNuke.Security.Permissions; + using DotNetNuke.Security.Roles; + + public class PermissionsGridViewModel + { + public List Permissions { get; set; } + + public List Users { get; set; } + + public List Roles { get; set; } + + public IEnumerable RoleGroups { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionsUpdateModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionsUpdateModel.cs new file mode 100644 index 00000000000..7da911f0426 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/PermissionsUpdateModel.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.Security.Models +{ + using System; + using System.Collections; + using System.Collections.Generic; + + using DotNetNuke.Entities.Users; + using DotNetNuke.Security.Roles; + + public class PermissionsUpdateModel + { + public List Permissions { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/RoleModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/RoleModel.cs new file mode 100644 index 00000000000..35f54b59335 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/RoleModel.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.Web.MvcPipeline.Security.Models +{ + using System.Collections.Generic; + + public class RoleModel + { + public int RoleId { get; set; } + + public string RoleName { get; set; } + + public Dictionary Permissions { get; set; } + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/UserModel.cs b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/UserModel.cs new file mode 100644 index 00000000000..67c0c86af6e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Models/UserModel.cs @@ -0,0 +1,10 @@ +// 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.Security.Models +{ + public class UserModel + { + } +} diff --git a/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Scripts/PermissionsGrid.js b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Scripts/PermissionsGrid.js new file mode 100644 index 00000000000..c9ea1ff55df --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Security/Scripts/PermissionsGrid.js @@ -0,0 +1,267 @@ +class PermissionsGrid { + constructor(options) { + this.options = { + container: '.dnnPermissionsGrid', + updateUrl: '', + addRoleUrl: '', + addUserUrl: '', + ...options + }; + + this.init(); + } + + init() { + this.container = $(this.options.container); + + // Initialize tokeninput for user search + this.initUserSearch(); + + // Bind event handlers + this.bindEvents(); + + // Initialize tri-state checkboxes + this.initTriStateCheckboxes(); + } + + bindEvents() { + // Role group dropdown change + this.container.find('.roleGroupsDropDown').on('change', (e) => { + this.onRoleGroupChange(e); + }); + + // Add role button click + this.container.find('.addRoleBtn').on('click', (e) => { + e.preventDefault(); + this.addRole(); + }); + + // Add user button click + this.container.find('.addUserBtn').on('click', (e) => { + e.preventDefault(); + this.addUser(); + }); + + // Delete role click + this.container.find('.deleteRole').on('click', (e) => { + e.preventDefault(); + this.deleteRole($(e.currentTarget)); + }); + + // Delete user click + this.container.find('.deleteUser').on('click', (e) => { + e.preventDefault(); + this.deleteUser($(e.currentTarget)); + }); + + // Permission change + this.container.find('.permissionTriState').on('change', (e) => { + this.updatePermissions(); + }); + } + + initUserSearch() { + const $userSearch = this.container.find('.userSearchInput'); + const serviceFramework = $.ServicesFramework(); + + $userSearch.tokenInput('/DesktopModules/Admin/Security/API/Users/Search', { + theme: 'facebook', + resultsFormatter: (item) => { + return `
  • ${item.displayName} (${item.userName})
  • `; + }, + tokenFormatter: (item) => { + return `
  • ${item.displayName}
  • `; + }, + preventDuplicates: true, + tokenLimit: 1, + onAdd: (item) => { + this.container.find('#hiddenUserIds').val(item.id); + }, + onDelete: () => { + this.container.find('#hiddenUserIds').val(''); + }, + hintText: 'Type to search users', + noResultsText: 'No results', + searchingText: 'Searching...', + tokenValue: 'id', + propertyToSearch: 'displayName', + prePopulate: null, + animateDropdown: false, + processPrePopulate: false + }); + } + + initTriStateCheckboxes() { + this.container.find('.permissionTriState').each((i, el) => { + const $triState = $(el); + $triState.triState({ + state: $triState.data('state'), + enabled: $triState.data('enabled') + }); + }); + } + + onRoleGroupChange(e) { + const groupId = $(e.target).val(); + const $rolesDropDown = this.container.find('.rolesDropDown'); + + $.ajax({ + url: '/API/Security/GetRoles', + data: { groupId: groupId }, + success: (roles) => { + $rolesDropDown.empty(); + roles.forEach(role => { + $rolesDropDown.append(new Option(role.RoleName, role.RoleID)); + }); + } + }); + } + + addRole() { + const roleId = this.container.find('.rolesDropDown').val(); + + $.ajax({ + url: this.options.addRoleUrl, + type: 'POST', + data: { roleId: roleId }, + success: (response) => { + if (response.success) { + window.location.reload(); + } else { + alert(response.message); + } + } + }); + } + + addUser() { + const userId = this.container.find('#hiddenUserIds').val(); + if (!userId) { + alert('Please select a user'); + return; + } + + $.ajax({ + url: this.options.addUserUrl, + type: 'POST', + data: { userId: userId }, + success: (response) => { + if (response.success) { + window.location.reload(); + } else { + alert(response.message); + } + } + }); + } + + deleteRole(button) { + if (!confirm('Are you sure you want to delete this role permission?')) { + return; + } + + const row = button.closest('tr'); + const roleId = row.data('role-id'); + this.deletePermissions('role', roleId, row); + } + + deleteUser(button) { + if (!confirm('Are you sure you want to delete this user permission?')) { + return; + } + + const row = button.closest('tr'); + const userId = row.data('user-id'); + this.deletePermissions('user', userId, row); + } + + deletePermissions(type, id, row) { + const permissions = []; + row.find('.permissionTriState').each((i, el) => { + const $triState = $(el); + permissions.push({ + permissionId: $triState.data('permission-id'), + isRolePermission: type === 'role', + roleId: type === 'role' ? id : null, + userId: type === 'user' ? id : null, + permissionKey: 'Null' + }); + }); + + this.updatePermissionsOnServer(permissions, () => { + row.remove(); + }); + } + + updatePermissions() { + const permissions = []; + + // Collect role permissions + this.container.find('.rolePermissions tr[data-role-id]').each((i, row) => { + const $row = $(row); + const roleId = $row.data('role-id'); + + $row.find('.permissionTriState').each((j, checkbox) => { + const $checkbox = $(checkbox); + permissions.push({ + permissionId: $checkbox.data('permission-id'), + isRolePermission: true, + roleId: roleId, + userId: null, + permissionKey: $checkbox.triState('state') + }); + }); + }); + + // Collect user permissions + this.container.find('.userPermissions tr[data-user-id]').each((i, row) => { + const $row = $(row); + const userId = $row.data('user-id'); + + $row.find('.permissionTriState').each((j, checkbox) => { + const $checkbox = $(checkbox); + permissions.push({ + permissionId: $checkbox.data('permission-id'), + isRolePermission: false, + roleId: null, + userId: userId, + permissionKey: $checkbox.triState('state') + }); + }); + }); + + this.updatePermissionsOnServer(permissions); + } + + updatePermissionsOnServer(permissions, callback) { + $.ajax({ + url: this.options.updateUrl, + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ permissions: permissions }), + success: (response) => { + if (response.success) { + if (callback) { + callback(); + } + } else { + alert(response.message); + } + } + }); + } +} + +// jQuery plugin +(function($) { + $.fn.permissionsGrid = function(options) { + return this.each(function() { + if (!$.data(this, 'permissionsGrid')) { + $.data(this, 'permissionsGrid', new PermissionsGrid({ + container: this, + ...options + })); + } + }); + }; +})(jQuery); \ No newline at end of file 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..2ee8b823400 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.BreadCrumb.cs @@ -0,0 +1,195 @@ +// 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; + + public static partial class SkinHelpers + { + private const string UrlRegex = "(href|src)=(\\\"|'|)(.[^\\\"']*)(\\\"|'|)"; + + 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()); + } + + 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..4923677b6e8 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Copyright.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.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..0faad0c10ca --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.CurrentDate.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; + using System.Web; + using System.Web.Mvc; + using DotNetNuke.Entities.Users; + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..79b931fc247 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssExclude.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.Skins +{ + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..ec3aadb6ac8 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnCssInclude.cs @@ -0,0 +1,47 @@ +// 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; + + public static partial class SkinHelpers + { + 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..df480008706 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsExclude.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.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..6f67f12289b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnJsInclude.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.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; + + public static partial class SkinHelpers + { + 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..caf2b0230ba --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DnnLink.cs @@ -0,0 +1,77 @@ +// 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; + + public static partial class SkinHelpers + { + 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..47a51d9902b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.DotNetNuke.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.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; + + public static partial class SkinHelpers + { + 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..bbe39bb3270 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Help.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.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; + + public static partial class SkinHelpers + { + 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..212af5f5a82 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.HostName.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.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; + + public static partial class SkinHelpers + { + 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..df858066deb --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.JavaScriptLibraryInclude.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.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; + + public static partial class SkinHelpers + { + 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..455eea8109c --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Language.cs @@ -0,0 +1,230 @@ +// 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; + + public static partial class SkinHelpers + { + 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, "", 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..913c80ec88e --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.LeftMenu.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.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..8f77c6e77c8 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Links.cs @@ -0,0 +1,134 @@ +// 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; + + public static partial class SkinHelpers + { + private static readonly Regex SrcRegex = new Regex("src=[']?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + 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..c3eba7cb4c3 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Login.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.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; + + public static partial class SkinHelpers + { + private const string LoginFileName = "Login.ascx"; + + 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..4126a330e61 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Logo.cs @@ -0,0 +1,157 @@ +// 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; + + public static partial class SkinHelpers + { + 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..889dc8382df --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Meta.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.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..a41a97085d2 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Pane.cs @@ -0,0 +1,100 @@ +// 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; + + public static partial class SkinExtensions + { + 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..44a9f97253a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Privacy.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.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; + + public static partial class SkinHelpers + { + 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..31f7d4ccec8 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Search.cs @@ -0,0 +1,404 @@ +// 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; + + public static partial class SkinHelpers + { + private const string SearchFileName = "Search.ascx"; + + 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..b87de47f19b --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.SkinPartial.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; + 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; + + public static partial class SkinHelpers + { + 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..6ffcc56e7a1 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Styles.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.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.UI.Skins; + + using DotNetNuke.Web.MvcPipeline.Models; + + 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..1d0001a80c5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Tags.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.MvcPipeline.Skins +{ + using System; + using System.Web; + using System.Web.Mvc; + + using DotNetNuke.Entities.Portals; + using DotNetNuke.Web.MvcPipeline.Models; + + public static partial class SkinHelpers + { + 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..8cffefa10a5 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Terms.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.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; + + public static partial class SkinHelpers + { + 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..074e193a316 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Text.cs @@ -0,0 +1,51 @@ +// 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; + + public static partial class SkinHelpers + { + 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..1e2b18982cc --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.Toast.cs @@ -0,0 +1,202 @@ +// 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; + + public static partial class SkinHelpers + { + private static readonly string ToastCacheKey = "DNN_Toast_Config"; + + 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..382ba62c453 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.User.cs @@ -0,0 +1,356 @@ +// 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; + + public static partial class SkinHelpers + { + private static string userResourceFile = GetSkinsResourceFile("User.ascx"); + + 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..9bf5ba6a680 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.UserAndLogin.cs @@ -0,0 +1,145 @@ +// 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; + + public static partial class SkinHelpers + { + 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..2bc931935d7 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.cs @@ -0,0 +1,29 @@ +// 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; + + public static partial class SkinHelpers + { + public static string GetResourceFile(string templateSourceDirectory, string fileName) + { + return templateSourceDirectory + "/" + Localization.LocalResourceDirectory + "/" + fileName; + } + + 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..75a2a907177 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Skins/SkinHelpers.jQuery.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.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; + + public static partial class SkinHelpers + { + 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..a263622210f --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Startup.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.MvcPipeline +{ + 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; + using System.Web.Mvc; + + 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..14fed3fe403 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/UI/Utilities/MvcClientAPI.cs @@ -0,0 +1,60 @@ +namespace DotNetNuke.Web.MvcPipeline.UI.Utilities +{ + using System; + using System.Collections.Generic; + using System.Web; + + public class MvcClientAPI + { + 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; + } + + 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; + } + + public static void RegisterClientVariable(string key, string value, bool overwrite) + { + GetClientVariableList().Add(key, value); + } + + public static void RegisterEmbeddedResource(string fileName, Type assemblyType) + { + // RegisterClientVariable(FileName + ".resx", ThePage.ClientScript.GetWebResourceUrl(AssemblyType, FileName), true); + throw new NotImplementedException(); + } + + public static void RegisterStartupScript(string key, string value) + { + if (!GetClientStartupScriptList().ContainsKey(key)) + { + GetClientStartupScriptList().Add(key, value); + } + } + + public static void RegisterScript(string key, string value) + { + if (!GetClientStartupScriptList().ContainsKey(key)) + { + GetClientStartupScriptList().Add(key, value); + } + } + } +} 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..0909ea4ba7f --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcPipeline/Utils/MvcViewEngine.cs @@ -0,0 +1,464 @@ +using System; +using System.Web; +using System.Web.Mvc; +using System.IO; +using System.Web.Routing; + + +namespace DotNetNuke.Web.MvcPipeline.Utils +{ + /// + /// Class that renders MVC views to a string using the + /// standard MVC View Engine to render the view. + /// + /// Requires that ASP.NET HttpContext is present to + /// work, but works outside of the context of MVC + /// credits to https://weblog.west-wind.com/posts/2012/may/30/rendering-aspnet-mvc-views-to-string + /// + public class MvcViewEngine + { + /// + /// Required Controller Context + /// + protected ControllerContext Context { get; set; } + + /// + /// Initializes the ViewRenderer with a Context. + /// + /// + /// 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."); + } + Context = controllerContext; + } + + /// + /// 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 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 + /// String of the rendered view or null on error + public void RenderView(string viewPath, object model, TextWriter writer) + { + 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 viewRenderer + /// String of the rendered view or null on error + public string RenderPartialViewToString(string viewPath, object model = null) + { + return 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 viewRenderer + /// Writer to render the view to + public void RenderPartialView(string viewPath, object model, TextWriter writer) + { + RenderViewToWriterInternal(viewPath, writer, model, true); + } + + /// + /// 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 pass to the viewRenderer + /// Active Controller context + /// String of the rendered view or null on error + public static string RenderView(string viewPath, object model = null, + ControllerContext controllerContext = null) + { + MvcViewEngine renderer = new MvcViewEngine(controllerContext); + return renderer.RenderViewToString(viewPath, model); + } + + /// + /// 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 pass to the viewRenderer + /// Writer to render the view to + /// Active Controller context + /// String of the rendered view or null on error + public static void RenderView(string viewPath, TextWriter writer, object model, + ControllerContext controllerContext) + { + MvcViewEngine renderer = new MvcViewEngine(controllerContext); + renderer.RenderView(viewPath, model, writer); + } + + /// + /// 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 pass to the viewRenderer + /// Active Controller context + /// optional out parameter that captures an error message instead of throwing + /// String of the rendered view or null on error + public static string RenderView(string viewPath, object model, + ControllerContext controllerContext, + out string errorMessage) + { + errorMessage = null; + try + { + MvcViewEngine renderer = new MvcViewEngine(controllerContext); + return renderer.RenderViewToString(viewPath, model); + } + catch (Exception ex) + { + errorMessage = ex.GetBaseException().Message; + } + return null; + } + + /// + /// 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 pass to the viewRenderer + /// Active Controller context + /// Writer to render the view to + /// optional out parameter that captures an error message instead of throwing + /// String of the rendered view or null on error + public static void RenderView(string viewPath, object model, TextWriter writer, + ControllerContext controllerContext, + out string errorMessage) + { + errorMessage = null; + try + { + MvcViewEngine renderer = new MvcViewEngine(controllerContext); + renderer.RenderView(viewPath, model, writer); + } + catch (Exception ex) + { + errorMessage = ex.GetBaseException().Message; + } + } + + + /// + /// 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 viewRenderer + /// Active controller context + /// String of the rendered view or null on error + public static string RenderPartialView(string viewPath, object model = null, + ControllerContext controllerContext = null) + { + MvcViewEngine 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 + /// + /// The model to pass to the viewRenderer + /// Active controller context + /// Text writer to render view to + /// optional output parameter to receive an error message on failure + public static void RenderPartialView(string viewPath, TextWriter writer, object model = null, + ControllerContext controllerContext = null) + { + MvcViewEngine 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 HtmlHelper and returns MvcHtmlString or IHtmlString + /// 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)); + + MvcViewEngine 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 HtmlHelper and returns MvcHtmlString or IHtmlString + /// 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 + /// 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 + /// Text writer to render view to + 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(Context, viewPath); + else + viewEngineResult = ViewEngines.Engines.FindView(Context, viewPath, null); + + if (viewEngineResult == null) + throw new FileNotFoundException(); + + // get the view and attach the model to view data + var view = viewEngineResult.View; + Context.Controller.ViewData.Model = model; + + var ctx = new ViewContext(Context, view, + Context.Controller.ViewData, + Context.Controller.TempData, + writer); + view.Render(ctx, writer); + } + + /// + /// Internal method that handles rendering of either partial or + /// 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(Context, viewPath); + else + viewEngineResult = ViewEngines.Engines.FindView(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; + Context.Controller.ViewData.Model = model; + + string result = null; + + using (var sw = new StringWriter()) + { + var ctx = new ViewContext(Context, view, + Context.Controller.ViewData, + Context.Controller.TempData, + sw); + view.Render(ctx, sw); + result = sw.ToString(); + } + + return result; + } + + /// + /// Internal method that handles rendering of HtmlHelper delegate to a string. + /// + /// A function that takes an HtmlHelper and returns MvcHtmlString or IHtmlString + /// 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 + Context.Controller.ViewData.Model = model; + + string result = null; + + using (var sw = new StringWriter()) + { + // Create a view data dictionary for the HtmlHelper + var viewDataContainer = new ViewDataContainer(Context.Controller.ViewData); + + // Create the HtmlHelper with the proper context + var htmlHelper = new HtmlHelper( + new ViewContext(Context, new FakeView(), Context.Controller.ViewData, 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 ControllerContext is present + /// + /// Type of the controller to create + /// Controller for T + /// thrown if HttpContext 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(System.Web.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", "")); + + controller.ControllerContext = new ControllerContext(wrapper, routeData, controller); + return controller; + } + + } + + /// + /// Empty MVC Controller instance used to + /// instantiate and provide a new ControllerContext + /// for the ViewRenderer + /// + public class EmptyController : Controller + { + } + + /// + /// Simple ViewDataContainer implementation for HtmlHelper usage + /// + internal class ViewDataContainer : IViewDataContainer + { + public ViewDataDictionary ViewData { get; set; } + + public ViewDataContainer(ViewDataDictionary viewData) + { + ViewData = viewData; + } + } + + /// + /// 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/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..84cd5bd8f64 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/DefaultController.cs @@ -0,0 +1,230 @@ +// 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.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; + + public class DefaultController : DnnPageController + { + private readonly INavigationManager navigationManager; + private readonly IPageModelFactory pageModelFactory; + private readonly IClientResourceController clientResourceController; + private readonly IPageService pageService; + + public DefaultController(INavigationManager navigationManager, + IPageModelFactory pageModelFactory, + IClientResourceController clientResourceController, + IPageService pageService, + IServiceProvider serviceProvider) + :base(serviceProvider) + { + this.navigationManager = navigationManager; + this.pageModelFactory = pageModelFactory; + this.clientResourceController = clientResourceController; + this.pageService = pageService; + } + + 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); + } + } + } + + 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(); + } + } + + 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 ? Host.AuthenticatedCacheability : Host.UnauthenticatedCacheability; + + switch (cacheability) + { + case "0": + this.Response.Cache.SetCacheability(HttpCacheability.NoCache); + break; + case "1": + this.Response.Cache.SetCacheability(HttpCacheability.Private); + break; + case "2": + this.Response.Cache.SetCacheability(HttpCacheability.Public); + break; + case "3": + this.Response.Cache.SetCacheability(HttpCacheability.Server); + break; + case "4": + this.Response.Cache.SetCacheability(HttpCacheability.ServerAndNoCache); + break; + case "5": + 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..12d870d193a --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controllers/ModuleActionsController.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.MvcWebsite.Controllers +{ + using System.Web.Mvc; + + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Log.EventLog; + using DotNetNuke.Web.MvcWebsite.Models; + + public class ModuleActionsController : Controller + { + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult Delete(ModuleActionsDeleteModel model) + { + var module = ModuleController.Instance.GetModule(model.ModuleId, model.TabId, false); + if (module == null) + { + return this.HttpNotFound(); + } + + var portalSettings = PortalSettings.Current; + var user = UserController.Instance.GetCurrentUserInfo(); + if (!module.IsShared) + { + foreach (var instance in ModuleController.Instance.GetTabModulesByModule(module.ModuleID)) + { + if (instance.IsShared) + { + // HARD Delete Shared Instance + ModuleController.Instance.DeleteTabModule(instance.TabID, instance.ModuleID, false); + EventLogController.Instance.AddLog(instance, portalSettings, user.UserID, string.Empty, EventLogController.EventLogType.MODULE_DELETED); + } + } + } + + ModuleController.Instance.DeleteTabModule(model.TabId, model.ModuleId, true); + EventLogController.Instance.AddLog(module, portalSettings, user.UserID, string.Empty, EventLogController.EventLogType.MODULE_SENT_TO_RECYCLE_BIN); + return new EmptyResult(); + } + + 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..9ee24586ee0 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/PrivacyControl.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.Controls +{ + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.ModuleControl; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + + public class PrivacyControl : RazorModuleControlBase + { + public override string ControlName => "Privacy"; + + public override string ControlPath => "admin/Portal"; + public override IRazorModuleResult Invoke() + { + return View(Localization.GetSystemMessage(PortalSettings.Current, "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..fffb7e258f6 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Controls/TermsControl.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 + +using DotNetNuke.Web.MvcPipeline.ModuleControl; + +namespace DotNetNuke.Web.MvcWebsite.Controls +{ + using DotNetNuke.Entities.Portals; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.ModuleControl.Razor; + + public class TermsControl : RazorModuleControlBase + { + public override string ControlName => "Terms"; + + public override string ControlPath => "admin/Portal"; + + public override IRazorModuleResult Invoke() + { + return View(Localization.GetSystemMessage(PortalSettings.Current, "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..403449b6ceb --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/DotNetNuke.Web.MvcWebsite.csproj @@ -0,0 +1,58 @@ + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..718cf228467 --- /dev/null +++ b/DNN Platform/DotNetNuke.Web.MvcWebsite/Models/ModuleActionsDeleteModel.cs @@ -0,0 +1,9 @@ +namespace DotNetNuke.Web.MvcWebsite.Models +{ + public class ModuleActionsDeleteModel + { + public int ModuleId { get; set; } + + 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/Collections/CollectionExtensions.cs b/DNN Platform/Library/Collections/CollectionExtensions.cs index cc54702306c..2f196b1cafc 100644 --- a/DNN Platform/Library/Collections/CollectionExtensions.cs +++ b/DNN Platform/Library/Collections/CollectionExtensions.cs @@ -867,6 +867,27 @@ public static SerializablePagedList Serialize(this IPagedList list) return res; } + /// + /// Converts the specified IPagedList to a Serializable Paged List. + /// + /// The type of items in the list. + /// The type of items in the serialized list. + /// The IPagedList results from the database. + /// A function to convert from type T to type U. + /// A serialized list of type U. + public static SerializablePagedList Serialize(this IPagedList list, Func cast) + { + var res = new SerializablePagedList(); + res.PageIndex = list.PageIndex; + res.PageSize = list.PageSize; + res.IsFirstPage = list.IsFirstPage; + res.IsLastPage = list.IsLastPage; + res.PageCount = list.PageCount; + res.TotalCount = list.TotalCount; + res.Data = list.Select(x => cast(x)); + return res; + } + /// Converts the into a instance. /// The type of the value to return. /// The value to convert. diff --git a/DNN Platform/Library/Common/Utilities/Config.cs b/DNN Platform/Library/Common/Utilities/Config.cs index 0e171342626..c3f207edefe 100644 --- a/DNN Platform/Library/Common/Utilities/Config.cs +++ b/DNN Platform/Library/Common/Utilities/Config.cs @@ -468,21 +468,16 @@ public static int GetAuthCookieTimeout(IApplicationStatusInfo appStatus) { var configNav = Load(appStatus).CreateNavigator(); - // Select the location node - var locationNav = configNav.SelectSingleNode("configuration/location"); - XPathNavigator formsNav; - - // Test for the existence of the location node if it exists then include that in the nodes of the XPath Query - if (locationNav == null) - { - formsNav = configNav.SelectSingleNode("configuration/system.web/authentication/forms"); - } - else + // Try to get the forms authentication from the default location + var formsNav = configNav?.SelectSingleNode("configuration/system.web/authentication/forms"); + if (formsNav is null) { - formsNav = configNav.SelectSingleNode("configuration/location/system.web/authentication/forms"); + // If unable, look for a location node, if found try to get the settings from there + formsNav = configNav?.SelectSingleNode("configuration/location/system.web/authentication/forms"); } - return formsNav != null ? XmlUtils.GetAttributeValueAsInteger(formsNav, "timeout", 30) : 30; + const int DefaultTimeout = 30; + return formsNav is null ? DefaultTimeout : XmlUtils.GetAttributeValueAsInteger(formsNav, "timeout", DefaultTimeout); } /// Gets optional persistent cookie timeout value from web.config. diff --git a/DNN Platform/Library/Common/Utilities/HtmlUtils.cs b/DNN Platform/Library/Common/Utilities/HtmlUtils.cs index a5f57bcacfc..6d133caf9ab 100644 --- a/DNN Platform/Library/Common/Utilities/HtmlUtils.cs +++ b/DNN Platform/Library/Common/Utilities/HtmlUtils.cs @@ -13,7 +13,9 @@ namespace DotNetNuke.Common.Utilities using DotNetNuke.Internal.SourceGenerators; using DotNetNuke.Services.Upgrade; - /// HtmlUtils is a Utility class that provides Html Utility methods. + using Ganss.Xss; + + /// HtmlUtils is a Utility class that provides HTML Utility methods. public partial class HtmlUtils { // Create Regular Expression objects @@ -579,5 +581,81 @@ public static IHtmlString JavaScriptStringEncode(string value) /// public static IHtmlString JavaScriptStringEncode(string value, bool addDoubleQuotes) => new HtmlString(HttpUtility.JavaScriptStringEncode(value, addDoubleQuotes)); + + /// Sanitize the given HTML, removing element which could include JavaScript. + /// The HTML to sanitize. + /// The sanitized HTML. + public static string CleanOutOfJavascript(string htmlInput) + { + var sanitizer = new HtmlSanitizer(); + + // We need to disallow all attributes that might contain JS + sanitizer.AllowedAttributes.Remove("onclick"); + sanitizer.AllowedAttributes.Remove("onmouseover"); + sanitizer.AllowedAttributes.Remove("onmouseout"); + sanitizer.AllowedAttributes.Remove("onkeypress"); + sanitizer.AllowedAttributes.Remove("onkeydown"); + sanitizer.AllowedAttributes.Remove("onkeyup"); + + // We need to disallow tags like '' + sanitizer.AllowedSchemes.Remove("javascript"); + + // Tags like ' + + + + + + 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.Api/.gitignore b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/.gitignore new file mode 100644 index 00000000000..e645f442256 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +*.csproj.user diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Contact.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Contact.cs new file mode 100644 index 00000000000..c87d7d1c46f --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Contact.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 + +using System; +using System.ComponentModel.DataAnnotations; +using System.Web.Caching; +using DotNetNuke.ComponentModel.DataAnnotations; + +namespace Dnn.ContactList.Api +{ + [Serializable] + [TableName("Dnn_Contacts")] + [PrimaryKey("ContactId")] + [Cacheable("Contacts", CacheItemPriority.Normal, 20)] + [Scope("PortalId")] + public class Contact + { + public Contact() + { + ContactId = -1; + } + + public int ContactId { get; set; } + + [Required(AllowEmptyStrings = false)] + [EmailAddress] + public string Email { get; set; } + + [Required(AllowEmptyStrings = false)] + public string FirstName { get; set; } + + [Required(AllowEmptyStrings = false)] + public string LastName { get; set; } + + [Required(AllowEmptyStrings = false)] + [Phone] + public string Phone { get; set; } + + public int PortalId { get; set; } + + [Required(AllowEmptyStrings = true)] + public string Social { get; set; } + + public int CreatedByUserId { get; set; } + + public DateTime CreatedOnDate { get; set; } + + public int LastModifiedByUserId { get; set; } + + public DateTime LastModifiedOnDate { get; set; } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Api/ContactRepository.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/ContactRepository.cs new file mode 100644 index 00000000000..a36baa54a35 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/ContactRepository.cs @@ -0,0 +1,141 @@ +// 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.Linq; +using DotNetNuke.Collections; +using DotNetNuke.Common; +using DotNetNuke.Data; +using DotNetNuke.Framework; + +namespace Dnn.ContactList.Api +{ + /// + /// ContactRepository provides a concrete implemetation of the IContactRepository interface for interacting with the Contact Repository(Database) + /// + public class ContactRepository : ServiceLocator, IContactRepository + { + protected override Func GetFactory() + { + return () => new ContactRepository(); + } + + /// + /// AddContact adds a contact to the repository + /// + /// The contact to add + /// The Id of the contact + public int AddContact(Contact contact, int userId) + { + Requires.NotNull(contact); + Requires.PropertyNotNegative(contact, "PortalId"); + + contact.CreatedByUserId = userId; + contact.CreatedOnDate = DateTime.UtcNow; + contact.LastModifiedByUserId = userId; + contact.LastModifiedOnDate = DateTime.UtcNow; + + using (var context = DataContext.Instance()) + { + var rep = context.GetRepository(); + + rep.Insert(contact); + } + + return contact.ContactId; + } + + /// + /// DeleteContact deletes a contact from the repository + /// + /// The contact to delete + public void DeleteContact(Contact contact) + { + Requires.NotNull(contact); + Requires.PropertyNotNegative(contact, "ContactId"); + + using (var context = DataContext.Instance()) + { + var rep = context.GetRepository(); + + rep.Delete(contact); + } + } + + public Contact GetContact(int contactId, int portalId) + { + Requires.NotNegative("contactId", contactId); + Requires.NotNegative("portalId", portalId); + + return GetContacts(portalId).SingleOrDefault(c => c.ContactId == contactId); + } + + /// + /// This GetContacts overload retrieves all the Contacts for a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The Id of the portal + /// A collection of contacts + public IQueryable GetContacts(int portalId) + { + Requires.NotNegative("portalId", portalId); + + IQueryable contacts = null; + + using (var context = DataContext.Instance()) + { + var rep = context.GetRepository(); + + contacts = rep.Get(portalId).AsQueryable(); + } + + return contacts; + } + + /// + /// This GetContacts overload retrieves a page of Contacts for a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The term to search for + /// The Id of the portal + /// The page Index to fetch - this is 0 based so the first page is when pageIndex = 0 + /// The size of the page to fetch from the database + /// A paged collection of contacts + + public IPagedList GetContacts(string searchTerm, int portalId, int pageIndex, int pageSize) + { + Requires.NotNegative("portalId", portalId); + + if (string.IsNullOrEmpty(searchTerm)) + { + searchTerm = ""; + } + var contacts = GetContacts(portalId).Where(c => c.FirstName.Contains(searchTerm) + || c.LastName.Contains(searchTerm) || + c.Email.Contains(searchTerm)); + + + return new PagedList(contacts, pageIndex, pageSize); + } + + /// + /// UpdateContact updates a contact in the repository + /// + /// The contact to update + public void UpdateContact(Contact contact, int userId) + { + Requires.NotNull(contact); + Requires.PropertyNotNegative(contact, "ContactId"); + contact.LastModifiedByUserId = userId; + contact.LastModifiedOnDate = DateTime.UtcNow; + + using (var context = DataContext.Instance()) + { + var rep = context.GetRepository(); + + rep.Update(contact); + } + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Dnn.ContactList.Api.csproj b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Dnn.ContactList.Api.csproj new file mode 100644 index 00000000000..24d3c9a386b --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Dnn.ContactList.Api.csproj @@ -0,0 +1,34 @@ + + + Dnn.ContactList.Api + net48 + bin + false + false + Dnn.ContactList.Api + Dnn ContactList Api + en-US + + Library + + Portable + False + false + True + .\bin + netstandard2.0 + + + + + + + + + + + + SolutionInfo.cs + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Api/IContactRepository.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/IContactRepository.cs new file mode 100644 index 00000000000..0743e4c6a85 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/IContactRepository.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 + +using System.Linq; +using DotNetNuke.Collections; + +namespace Dnn.ContactList.Api +{ + /// + /// IContactRepository provides an interface for interacting with the Contact Repository(Database) + /// + public interface IContactRepository + { + /// + /// AddContact adds a contact to the repository + /// + /// The contact to add + /// The Id of the user making the addition + /// The Id of the contact + int AddContact(Contact contact, int userId); + + /// + /// DeleteContact deletes a contact from the repository + /// + /// The contact to delete + void DeleteContact(Contact contact); + + /// + /// This GetContact method retrieves a specific Contact in a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The Id of the contact + /// The Id of the portal + /// A single of contact + Contact GetContact(int contactId, int portalId); + + /// + /// This GetContacts overload retrieves all the Contacts for a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The Id of the portal + /// A collection of contacts + IQueryable GetContacts(int portalId); + + /// + /// This GetContacts overload retrieves a page of Contacts for a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The term to search for + /// The Id of the portal + /// The page Index to fetch - this is 0 based so the first page is when pageIndex = 0 + /// The size of the page to fetch from the database + /// A paged collection of contacts + IPagedList GetContacts(string searchTerm, int portalId, int pageIndex, int pageSize); + + /// + /// UpdateContact updates a contact in the repository + /// + /// The contact to update + /// The Id of the user making the update + void UpdateContact(Contact contact, int userId); + } +} + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Providers/DataProviders/SqlDataProvider/01.00.00.SqlDataProvider b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Providers/DataProviders/SqlDataProvider/01.00.00.SqlDataProvider new file mode 100644 index 00000000000..84d066a6c70 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Providers/DataProviders/SqlDataProvider/01.00.00.SqlDataProvider @@ -0,0 +1,53 @@ +/************************************************************/ +/***** SqlDataProvider *****/ +/***** *****/ +/***** *****/ +/***** Note: To manually execute this script you must *****/ +/***** perform a search and replace operation *****/ +/***** for {databaseOwner} and {objectQualifier} *****/ +/***** *****/ +/************************************************************/ + +/** Create Table **/ + +if not exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}Dnn_Contacts]') and OBJECTPROPERTY(id, N'IsTable') = 1) + BEGIN + CREATE TABLE {databaseOwner}[{objectQualifier}Dnn_Contacts] + ( + [ContactId] [int] IDENTITY(1,1) NOT NULL, + [PortalId] [int] NOT NULL, + [FirstName] [nvarchar](100) NOT NULL, + [LastName] [nvarchar](100) NOT NULL, + [Email] [nvarchar](100) NOT NULL, + [Phone] [nvarchar](50) NOT NULL, + [Social] [nvarchar](50) NULL, + [CreatedByUserId] [int] NOT NULL, + [CreatedOnDate] [datetime] NOT NULL, + [LastModifiedByUserId] [int] NOT NULL, + [LastModifiedOnDate] [datetime] NOT NULL, + ) + + ALTER TABLE {databaseOwner}[{objectQualifier}Dnn_Contacts] ADD CONSTRAINT [PK_{objectQualifier}Dnn_Contacts] PRIMARY KEY CLUSTERED ([ContactId]) + END +GO + +/** Fill table with current DNN users **/ + +BEGIN + INSERT INTO {databaseOwner}[{objectQualifier}Dnn_Contacts] + ([PortalId],[FirstName],[LastName],[Email],[Phone],[Social],[CreatedByUserId],[CreatedOnDate],[LastModifiedByUserId],[LastModifiedOnDate]) + SELECT + u.PortalId, + LEFT(u.FirstName,100) AS FirstName, + LEFT(u.LastName,100) AS LastName, + LEFT(u.Email,100) AS Email, + '+' + CAST(ABS(CHECKSUM(NewId())) % 100 AS VARCHAR) + ' ' + + CAST(100 + ABS(CHECKSUM(NewId())) % 900 AS VARCHAR) + ' ' + + CAST(100 + ABS(CHECKSUM(NewId())) % 900 AS VARCHAR) + ' ' + + CAST(100 + ABS(CHECKSUM(NewId())) % 900 AS VARCHAR), + '@' + LEFT(LOWER(u.FirstName), 1) + LOWER(u.LastName) + '.dnn.social', + -1, GETDATE(), -1, GETDATE() + FROM {databaseOwner}[{objectQualifier}vw_Users] u + WHERE u.IsSuperUser=0 AND u.IsDeleted = 0 +END + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Providers/DataProviders/SqlDataProvider/Uninstall.SqlDataProvider b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Providers/DataProviders/SqlDataProvider/Uninstall.SqlDataProvider new file mode 100644 index 00000000000..06d36b857ee --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Providers/DataProviders/SqlDataProvider/Uninstall.SqlDataProvider @@ -0,0 +1,5 @@ +if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}Dnn_Contacts]') and OBJECTPROPERTY(id, N'IsTable') = 1) + BEGIN + DROP TABLE {databaseOwner}[{objectQualifier}Dnn_Contacts] + END +GO diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Startup.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Startup.cs new file mode 100644 index 00000000000..11d0f32fa0e --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Api/Startup.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 + +using DotNetNuke.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Dnn.ContactList.Api +{ + public class Startup : IDnnStartup + { + public void ConfigureServices(IServiceCollection services) + { + // Register ContactService as a singleton + services.AddScoped(x => ContactRepository.Instance); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/.gitignore b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/.gitignore new file mode 100644 index 00000000000..e645f442256 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +*.csproj.user diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/App_LocalResources/Contact.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/App_LocalResources/Contact.resx new file mode 100644 index 00000000000..ffb84c8eac6 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/App_LocalResources/Contact.resx @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Edit Contact + + + 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.Mvc/ContactList_Mvc.dnn b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ContactList_Mvc.dnn new file mode 100644 index 00000000000..26eb645776d --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ContactList_Mvc.dnn @@ -0,0 +1,106 @@ + + + + Contact List Mvc + DNN Contact List using MVC + + DNN + DNN Corp. + http://www.dnnsoftware.com + support@dnnsoftware.com + + + + true + + 08.00.00 + + + + + DesktopModules\MVC\Dnn\ContactList\ + + + + + + + Dnn.ContactList.Mvc + Dnn/ContactList + + + + + Contact List Mvc + 0 + + + + Dnn.ContactList.Mvc.Controllers/Contact/Index.mvc + DotNetNuke.Web.MvcPipeline.ModuleControl.MvcModuleControl, DotNetNuke.Web.MvcPipeline + False + + View + + + False + 0 + + + Edit + Dnn.ContactList.Mvc.Controllers/Contact/Edit.mvc + True + Add/Update Contact + View + + + True + 0 + + + Settings + Dnn.ContactList.Mvc.Controllers/Settings/Index.mvc + False + Contact List Settings + Edit + + + 0 + + + + + + + + + + bin + Dnn.ContactList.Mvc.dll + + + bin + Dnn.ContactList.Api.dll + + + + + + DesktopModules/MVC/Dnn/ContactList + + Resources.zip + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/ContactController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/ContactController.cs new file mode 100644 index 00000000000..8211e6d7cd2 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/ContactController.cs @@ -0,0 +1,157 @@ +// 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.Mvc; +using Dnn.ContactList.Api; +using DotNetNuke.Abstractions.ClientResources; +using DotNetNuke.Abstractions.Pages; +using DotNetNuke.Collections; +using DotNetNuke.Common; +using DotNetNuke.Entities.Modules.Actions; +using DotNetNuke.Web.Mvc.Framework.ActionFilters; +using DotNetNuke.Web.Mvc.Framework.Controllers; + +namespace Dnn.ContactList.Mvc.Controllers +{ + /// + /// ContactController is the MVC Controller class for managing Contacts in the UI + /// + [DnnHandleError] + public class ContactController : DnnController + { + private readonly IContactRepository _repository; + private readonly IClientResourceController clientResourceController; + private readonly IPageService pageService; + + /// + /// Constructor constructs a new ContactController with a passed in repository + /// + public ContactController(IClientResourceController clientResourceController, + IPageService pageService) + { + //Requires.NotNull(repository); + Requires.NotNull(clientResourceController); + Requires.NotNull(pageService); + + this.clientResourceController = clientResourceController; + this.pageService = pageService; + _repository = ContactRepository.Instance; + + } + + /// + /// The Delete method is used to delete a Contact + /// + /// The Id of the Contact to delete + /// + [HttpGet] + public ActionResult Delete(int contactId) + { + var contact = _repository.GetContact(contactId, PortalSettings.PortalId); + + _repository.DeleteContact(contact); + + return RedirectToDefaultRoute(); + } + + /// + /// This Edit method is used to set up editing a Contact + /// + /// + [HttpGet] + public ActionResult Edit(int contactId = -1) + { + var contact = (contactId == -1) + ? new Contact { PortalId = PortalSettings.PortalId } + : _repository.GetContact(contactId, PortalSettings.PortalId); + + return View(contact); + } + + /// + /// This Edit method is used to save the posted Contact + /// + /// The contact to save + /// + [HttpPost] + [DotNetNuke.Web.Mvc.Framework.ActionFilters.ValidateAntiForgeryToken] + public ActionResult Edit(Contact contact) + { + if (ModelState.IsValid) + { + contact.PortalId = PortalSettings.PortalId; + + if (contact.ContactId == -1) + { + _repository.AddContact(contact, User.UserID); + } + else + { + var existing = _repository.GetContact(contact.ContactId, PortalSettings.PortalId); + existing.FirstName = contact.FirstName; + existing.LastName = contact.LastName; + existing.Email = contact.Email; + existing.Phone = contact.Phone; + existing.Social = contact.Social; + _repository.UpdateContact(existing, User.UserID); + } + + return RedirectToDefaultRoute(); + } + else + { + return View(contact); + } + } + + /// + /// The Index method is the default Action method. + /// + /// Term to search. + /// Index of the current page. + /// Number of records per page. + /// + [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); + } + + public JsonResult GetJsonResult() + { + return new JsonResult() + { + Data = new + { + User.UserID, + PortalSettings.PortalId, + Alias = PortalSettings.PortalAlias.HTTPAlias, + Time = DateTime.Now.ToString("HH:mm:ss ttt") + }, + JsonRequestBehavior = JsonRequestBehavior.AllowGet + }; + } + + public ActionResult GetDemoPartial() + { + TempData["UserID"] = User.UserID; + ViewData["PortalId"] = PortalSettings.PortalId; + ViewBag.Alias = PortalSettings.PortalAlias.HTTPAlias; + + return PartialView("_DemoPartial", DateTime.Now); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/SettingsController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/SettingsController.cs new file mode 100644 index 00000000000..a326e7ac82e --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Controllers/SettingsController.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 + +using System.Web.Mvc; +using Dnn.ContactList.Mvc.Models; +using DotNetNuke.Collections; +using DotNetNuke.Security; +using DotNetNuke.Web.Mvc.Framework.ActionFilters; +using DotNetNuke.Web.Mvc.Framework.Controllers; + +namespace Dnn.ContactList.Mvc.Controllers +{ + /// + /// The Settings Controller manages the modules settings + /// + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.Edit)] + [DnnHandleError] + public class SettingsController : DnnController + { + /// + /// The Index action renders the default Settings View + /// + /// + [HttpGet] + public ActionResult Index() + { + var settings = new Settings(); + settings.PageSize = ModuleContext.Configuration.ModuleSettings.GetValueOrDefault("PageSize", 9); + settings.AllowContactCreation = ModuleContext.Configuration.ModuleSettings.GetValueOrDefault("AllowContactCreation", false); + return View(settings); + } + + /// + /// + /// + /// + /// + [HttpPost] + [ValidateInput(false)] + [DotNetNuke.Web.Mvc.Framework.ActionFilters.ValidateAntiForgeryToken] + public ActionResult Index(Settings settings) + { + ModuleContext.Configuration.ModuleSettings["PageSize"] = settings.PageSize.ToString(); + ModuleContext.Configuration.ModuleSettings["AllowContactCreation"] = settings.AllowContactCreation.ToString(); + return RedirectToDefaultRoute(); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Dnn.ContactList.Mvc.csproj b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Dnn.ContactList.Mvc.csproj new file mode 100644 index 00000000000..3b67f048f39 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Dnn.ContactList.Mvc.csproj @@ -0,0 +1,46 @@ + + + Dnn.ContactList.Mvc + net48 + bin\ + false + false + Dnn.ContactList.Mvc + Dnn ContactList Mvc Module + en-US + + Library + + Portable + False + false + True + .\bin + netstandard2.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + SolutionInfo.cs + + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Models/Settings.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Models/Settings.cs new file mode 100644 index 00000000000..53be740462e --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Models/Settings.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 + +using System.ComponentModel; +using Newtonsoft.Json; + +namespace Dnn.ContactList.Mvc.Models +{ + /// + /// Settings class manages the settings for the module instance. + /// + [JsonObject(MemberSerialization.OptIn)] + public class Settings + { + /// + /// Settings constructor + /// + public Settings() + { + PageSize = 10; + AllowContactCreation = true; + + } + + /// + /// Number of contacts to show per page. + /// + [DisplayName("Page Size")] + [JsonProperty("PageSize")] + public int PageSize { get; set; } + + /// + /// Allow users add/edit the contact + /// + [DisplayName("Allow contact creation.")] + [JsonProperty("AllowContactCreation")] + public bool AllowContactCreation { get; set; } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Module.build b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Module.build new file mode 100644 index 00000000000..5d169bb1bb8 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Module.build @@ -0,0 +1,36 @@ + + + + $(MSBuildProjectDirectory)\..\..\..\.. + + + + zip + ContactList_Mvc + Dnn.ContactList.Mvc + $(WebsitePath)\DesktopModules\MVC\Dnn\ContactList + $(PathToArtifacts)\SampleModules + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Properties/AssemblyInfo.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..9aec54bfbca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/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.Mvc/Properties/launchSettings.json b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Properties/launchSettings.json new file mode 100644 index 00000000000..e40a9f5bc02 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Dnn.ContactList.Mvc": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:51707" + } + } +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ReleaseNotes.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ReleaseNotes.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/ReleaseNotes.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/RouteConfig.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/RouteConfig.cs new file mode 100644 index 00000000000..548f856afd3 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/RouteConfig.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 + +using DotNetNuke.Web.Mvc.Routing; + +namespace Dnn.ContactList.Mvc +{ + public class RouteConfig : IMvcRouteMapper + { + public void RegisterRoutes(IMapRoute mapRouteManager) + { + mapRouteManager.MapRoute("Dnn/ContactList", "ContactList", "{controller}/{action}", new[] + {"Dnn.ContactList.Mvc.Controllers"}); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/Edit.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/Edit.cshtml new file mode 100644 index 00000000000..689f864b34f --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/Edit.cshtml @@ -0,0 +1,80 @@ +@inherits DotNetNuke.Web.Mvc.Framework.DnnWebViewPage + +@using DotNetNuke.Services.Localization +@using DotNetNuke.Web.Mvc.Helpers + +
    diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/Index.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/Index.cshtml new file mode 100644 index 00000000000..93783a1264f --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/Index.cshtml @@ -0,0 +1,96 @@ +@using DotNetNuke.Collections +@using DotNetNuke.Web.Mvc.Helpers +@inherits DotNetNuke.Web.Mvc.Framework.DnnWebViewPage> + +
    +
    + @foreach (var contact in Model) + { + Html.RenderPartial("_ContactPartial", contact, null); + } +
    +
    + @if (Model.HasPreviousPage) + { + + } + @Dnn.LocalizeString("PageOf").Replace("[PageIndex]", (@Model.PageIndex + 1).ToString()).Replace("[PageCount]", @Model.PageCount.ToString()) + @if (Model.HasNextPage) + { + + } +
    + @if (@Dnn.ModuleContext.IsEditable && Convert.ToBoolean(Dnn.ModuleContext.Configuration.ModuleSettings.GetValueOrDefault("AllowContactCreation", false))) + { + + } +
    +@section scripts +{ + +} +

    Return Partial and Json Demo

    +
    Partial View
    +
    +
    Json Result
    +
    +
    Current User Id:
    +
    Portal Id:
    +
    Portal Alias:
    +
    Current Time:
    +
    +
    Update happens every 10 secs.
    +
    (View calls in browser's network tab for more details)
    \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/_ContactPartial.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/_ContactPartial.cshtml new file mode 100644 index 00000000000..f59c61c1486 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/_ContactPartial.cshtml @@ -0,0 +1,52 @@ +@using DotNetNuke.Collections +@inherits DotNetNuke.Web.Mvc.Framework.DnnWebViewPage +
    +
    + +
    +
    ACME CORP
    +
    INNOVATION & SOLUTIONS
    +
    + @{ + if (@Dnn.ModuleContext.IsEditable && Convert.ToBoolean(Dnn.ModuleContext.Configuration.ModuleSettings.GetValueOrDefault("AllowContactCreation", false))) + { + + } + } +
    +
    +
    @Model.FirstName @Model.LastName
    +
    @Dnn.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.Mvc/Views/Contact/_DemoPartial.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/_DemoPartial.cshtml new file mode 100644 index 00000000000..239415fecd4 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Contact/_DemoPartial.cshtml @@ -0,0 +1,5 @@ +@inherits DotNetNuke.Web.Mvc.Framework.DnnWebViewPage +
    Current User Id From Temp Data: @TempData["UserID"]
    +
    Portal Id From View Data: @Dnn.ViewData["PortalId"]
    +
    Portal Alias From View Bag: @Dnn.ViewBag.Alias
    +
    Current Time: @Model.ToString("HH:mm:ss ttt")
    diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Settings/Index.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Settings/Index.cshtml new file mode 100644 index 00000000000..f8d81c48225 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Settings/Index.cshtml @@ -0,0 +1,20 @@ +@inherits DotNetNuke.Web.Mvc.Framework.DnnWebViewPage + +@using DotNetNuke.Web.Mvc.Helpers + +

    Contact List Settings

    + +
    +
    + +
    + @Html.LabelFor(m => Model.PageSize, new { @class = "col-md-2 control-label" }) +
    + @Html.TextBoxFor(m => Model.PageSize, new { id = "PageSize" }) +
    +
    + @Html.LabelFor(m => Model.AllowContactCreation, new { @class = "col-md-2 control-label" }) +
    + @Html.CheckBoxFor(m => Model.AllowContactCreation, new { id = "AllowContactCreation" }) +
    +
    \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Shared/_Layout.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000000..85eb0174fc4 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/Shared/_Layout.cshtml @@ -0,0 +1,4 @@ +
    + @RenderBody() +
    +@RenderSection("scripts", required: false) diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/_ViewStart.cshtml b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/_ViewStart.cshtml new file mode 100644 index 00000000000..bbba92c79df --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/DesktopModules/MVC/Dnn/ContactList/Views/Shared/_Layout.cshtml"; +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/license.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/license.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/license.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/module.css b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/module.css new file mode 100644 index 00000000000..de7e697eaf3 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Mvc/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.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..4bfa2e4bf98 --- /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..7ffe6e044c0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Razor/Module.build @@ -0,0 +1,36 @@ + + + + $(MSBuildProjectDirectory)\..\..\..\.. + + + + zip + ContactList_Razor + DNN_ContactList_Razor + $(WebsitePath)\DesktopModules\Dnn\RazorContactList + $(PathToArtifacts)\SampleModules + + + + + + + + + + + + + + + + + + + + + + + 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")

    +
    +
    + +
    + @Html.HiddenFor(m => m.ContactId) + +
    +
    + + @Html.TextBoxFor(m => m.FirstName, new { @class = "form-input", placeholder = "Enter first name" }) + @Html.ValidationMessageFor(m => m.FirstName, @Html.LocalizeString("FirstNameRequired").ToHtmlString()) +
    + +
    + + @Html.TextBoxFor(m => m.LastName, new { @class = "form-input", placeholder = "Enter last name" }) + @Html.ValidationMessageFor(m => m.LastName, @Html.LocalizeString("LastNameRequired").ToHtmlString()) +
    +
    + +
    + + @Html.TextBoxFor(m => m.Email, new { @class = "form-input", placeholder = "email@example.com", type = "email" }) + @Html.ValidationMessageFor(m => m.Email, @Html.LocalizeString("EmailRequired").ToHtmlString()) +
    + +
    + + @Html.TextBoxFor(m => m.Phone, new { @class = "form-input", placeholder = "+1 555-123-4567", type = "tel" }) + @Html.ValidationMessageFor(m => m.Phone, @Html.LocalizeString("PhoneRequired").ToHtmlString()) +
    + +
    + + @Html.TextBoxFor(m => m.Social, new { @class = "form-input", placeholder = "@username or profile URL" }) + @Html.ValidationMessageFor(m => m.Social, @Html.LocalizeString("SocialRequired").ToHtmlString()) +
    + +
    + + + + @Html.LocalizeString("Cancel") + + + +
    +
    +
    \ 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/.gitignore b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/.gitignore new file mode 100644 index 00000000000..e645f442256 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +*.csproj.user diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/ContactList.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/ContactList.resx new file mode 100644 index 00000000000..1a7197a02da --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/ContactList.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 + + + Add New Contact + + + Cancel + + + Delete {0} + + + Delete + + + Are you sure you want to delete this contact? + + + Edit Contact + + + Email Address + + + First Name + + + Last Name + + + Phone + + + Save + + + Social + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/Settings.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/Settings.resx new file mode 100644 index 00000000000..ba099ab5323 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/Settings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Allow creation of new contacts? + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/ShowInfo.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/ShowInfo.resx new file mode 100644 index 00000000000..909a0d2f1ca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/App_LocalResources/ShowInfo.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Are you in edit mode? + + + Are you a super user? + + + ModuleId + + + Portal Identifier + + + Show module info + + + Tab Identifier + + + Tab Module identifier + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/QuickSettings.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/QuickSettings.js new file mode 100644 index 00000000000..d91bcd2090f --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/QuickSettings.js @@ -0,0 +1,78 @@ +var dnn = dnn || {}; +dnn.modules = dnn.modules || {}; +dnn.modules.spa = dnn.modules.spa || {}; +dnn.modules.spa.dnnContactListSpa = dnn.modules.spa.dnnContactListSpa || {}; + +dnn.modules.spa.dnnContactListSpa.quickSettings = function (config, root, moduleId, ko) { + var savedIsFormEnabled = config.isFormEnabled; + var quickSettingsDispatcher = config.quickSettingsDispatcher; + + var viewModel = { + isFormEnabled: ko.observable(savedIsFormEnabled) + }; + + var settings = { + servicesFramework: $.ServicesFramework(moduleId) + } + var util = contactList.utility(settings, {}); + util.settingsService = function(){ + util.sf.serviceController = "Settings"; + return util.sf; + }; + + // The function dnnQuickSettings definded in 'ModuleActions.js' requires working with promises. + var SaveSettings = function () { + var deferred = $.Deferred(); + + if (viewModel.isFormEnabled() == savedIsFormEnabled){ + deferred.resolve(); + } else { + var params = { + isFormEnabled: viewModel.isFormEnabled() + }; + + util.settingsService().post("SaveSettings", params, + function(data){ + //Success + deferred.resolve(); + savedIsFormEnabled = params.isFormEnabled; + quickSettingsDispatcher.notify(moduleId, quickSettingsDispatcher.eventTypes.SAVE, params); + }, + + function(data){ + //Failure + deferred.reject(); + } + ); + } + + return deferred.promise(); + }; + + var CancelSettings = function () { + var deferred = $.Deferred(); + viewModel.isFormEnabled(savedIsFormEnabled); + deferred.resolve(); + + quickSettingsDispatcher.notify(moduleId, quickSettingsDispatcher.eventTypes.CANCEL); + return deferred.promise(); + }; + + var init = function () { + var $root = $("#"+ root); + ko.applyBindings(viewModel, $root[0]); + + // dnnQuickSettings needs three parameters: moduleId, onSave and onCancel. + // These two functions are associated to the save and cancel buttons and the + // callbacks mechanism is based on promises. + $(root).dnnQuickSettings({ + moduleId: moduleId, + onSave: SaveSettings, + onCancel: CancelSettings + }); + } + + return { + init: init + } +}; diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/QuickSettingsDispatcher.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/QuickSettingsDispatcher.js new file mode 100644 index 00000000000..43cb73f76b7 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/QuickSettingsDispatcher.js @@ -0,0 +1,46 @@ +var dnn = dnn || {}; +dnn.modules = dnn.modules || {}; +dnn.modules.spa = dnn.modules.spa || {}; +dnn.modules.spa.dnnContactListSpa = dnn.modules.spa.dnnContactListSpa || {}; + +dnn.modules.spa.dnnContactListSpa.quickSettingsDispatcher = (function () { + "use strict"; + var addSubcriber, notify; + var subcribers; + + var EVENT_TYPES = { + SAVE: "SAVE", + CANCEL: "CANCEL" + }; + + subcribers = {} + + addSubcriber = function addSubcriberHandler(moduleId, eventType, callback) { + if (!EVENT_TYPES[eventType]) { + throw "QuickSettingsDispatcher - Subcritions to event type '" + eventType + "' not supported."; + } + + if (!subcribers[moduleId]) { + subcribers[moduleId] = {}; + } + + subcribers[moduleId][eventType] = callback; + }; + + notify = function notifyHandler(moduleId, eventType, params) { + if (!EVENT_TYPES[eventType]) { + throw "QuickSettingsDispatcher - Notifications to event type '" + eventType + "' not supported."; + } + + if (subcribers[moduleId] && subcribers[moduleId][eventType] + && typeof subcribers[moduleId][eventType] == "function") { + subcribers[moduleId][eventType](params); + } + }; + + return { + addSubcriber: addSubcriber, + notify: notify, + eventTypes: EVENT_TYPES + }; +}()); diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/contacts.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/contacts.js new file mode 100644 index 00000000000..5cc1774aed0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/contacts.js @@ -0,0 +1,264 @@ +if (typeof contactList === 'undefined' || contactList === null) { + contactList = {}; +}; + +ko.extenders.required = function (target, options) { + //add some sub-observables to our observable + target.hasError = ko.observable(); + target.validationMessage = ko.observable(); + target.validationClass = ko.observable(); + + var regEx = new RegExp(options.regEx); + //define a function to do validation + var errorMessage = options.overrideMessage || "This field is required"; + var allowEmpty = options.allowEmpty || false; + function validate(newValue) { + var validated; + if (allowEmpty) { + // For optional fields: allow empty OR match regex + validated = newValue === "" || regEx.test(newValue); + } else { + // For required fields: must match regex AND not be empty + validated = regEx.test(newValue) && newValue !== ""; + } + target.hasError(!validated); + target.validationClass(validated ? "form-control" : "form-control has-error"); + target.validationMessage(validated ? "" : errorMessage); + } + + //validate whenever the value changes + target.subscribe(validate); + + //return the original observable + return target; +}; + +function clearErrors(obsArr) { + for (var i = 0; i < obsArr.length; i++) { + obsArr[i].hasError(false); + obsArr[i].validationClass("form-control"); + } +} + +contactList.contactsViewModel = function(config) { + var self = this; + var resx = config.resx; + var util = config.util; + var preloadedData = config.preloadedData; + var $rootElement = config.$rootElement; + + var quickSettingsDispatcher = config.settings.quickSettingsDispatcher; + var moduleId = config.settings.moduleId; + + util.contactService = function(){ + util.sf.serviceController = "Contact"; + return util.sf; + }; + + self.isFormEnabled = ko.observable(config.settings.isFormEnabled); + self.isEditMode = ko.observable(false); + self.contacts = ko.observableArray([]); + self.totalResults = ko.observable(preloadedData.pageCount); + self.pageIndex = ko.observable(0); + + self.selectedContact = new contactList.contactViewModel(self, config); + + var toggleView = function() { + self.isEditMode(!self.isEditMode()); + }; + + self.addContact = function(){ + toggleView(); + self.selectedContact.init(); + clearErrors([self.selectedContact.firstName, self.selectedContact.lastName, self.selectedContact.phone, self.selectedContact.email, self.selectedContact.social]); + }; + + self.closeEdit = function() { + toggleView(); + self.refresh(); + } + + self.editContact = function(data, e) { + self.getContact(data.contactId()); + toggleView(); + }; + + self.getContact = function (contactId, cb) { + var params = { + contactId: contactId + }; + + util.contactService().get("GetContact", params, + function(data) { + if (typeof data !== "undefined" && data != null && data.success === true) { + //Success + self.selectedContact.load(data.data.contact); + } else { + //Error + } + }, + + function(){ + //Failure + } + ); + + if(typeof cb === 'function') cb(); + }; + + self.getContacts = function () { + var params = { + pageSize: self.pageSize, + pageIndex: self.pageIndex(), + searchTerm: "" + }; + + util.contactService().get("GetContacts", params, + function(data) { + if (typeof data !== "undefined" && data != null && data.success === true) { + //Success + self.totalResults(data.data.totalCount); + self.load(data.data); + } else { + //Error + } + }, + function(){ + //Failure + } + ); + }; + + var updateView = function updateViewHandler(settings) { + self.isFormEnabled(settings.isFormEnabled); + }; + + self.init = function () { + if (preloadedData) { + self.load(preloadedData); + } else { + self.getContacts(); + } + quickSettingsDispatcher.addSubcriber(moduleId, quickSettingsDispatcher.eventTypes.SAVE, updateView); + pager.init(self, 6, self.refresh, resx); + }; + + self.load = function(data) { + self.contacts.removeAll(); + for(var i=0; i < data.results.length; i++){ + var result = data.results[i]; + var contact = new contactList.contactViewModel(self, config); + contact.load(result); + self.contacts.push(contact); + } + }; + + self.refresh = function() { + self.getContacts(); + } +}; + +contactList.contactViewModel = function(parentViewModel, config) { + var self = this; + var resx = config.resx; + var util = config.util; + var $rootElement = config.$rootElement; + + self.parentViewModel = parentViewModel; + self.contactId = ko.observable(-1); + self.firstName = ko.observable('').extend({ required: { overrideMessage: "Please enter a first name" } }); + self.lastName = ko.observable('').extend({ required: { overrideMessage: "Please enter a last name" } }); + self.email = ko.observable('').extend({ required: { overrideMessage: "Please enter a valid email address", regEx: config.settings.emailRegex } }); + self.phone = ko.observable('').extend({ required: { overrideMessage: "Please enter a valid phone number (international formats accepted: +1 234 567 8900, 123-456-7890, etc.)", regEx: /^(\+?\d{1,3}[\s.-]?)?[\d\s().-]{6,}$/ } }); + self.social = ko.observable('').extend({ required: { overrideMessage: "Social handle must start with @ symbol", regEx: /^@/, allowEmpty: true } }); + + self.cancel = function () { + clearErrors([self.firstName, self.lastName, self.email, self.phone, self.social]); + parentViewModel.closeEdit(); + }; + + self.deleteContact = function (data, e) { + var opts = { + callbackTrue: function () { + var params = { + contactId: data.contactId(), + firstName: data.firstName(), + lastName: data.lastName(), + email: data.email(), + phone: data.phone(), + social: data.social() + }; + + util.contactService().post("DeleteContact", params, + function(data){ + //Success + parentViewModel.refresh(); + }, + + function (data) { + //Failure + } + ); + }, + text: resx.DeleteConfirm, + yesText: resx.Delete, + noText: resx.Cancel, + title: resx.ConfirmDeleteTitle.replace("{0}", data.firstName() + " " + data.lastName()) + }; + + $.dnnConfirm(opts); + + + }; + + self.init = function(){ + self.contactId(-1); + self.firstName(""); + self.lastName(""); + self.email(""); + self.phone(""); + self.social(""); + }; + + self.load = function(data) { + self.contactId(data.contactId); + self.firstName(data.firstName); + self.lastName(data.lastName); + self.email(data.email); + self.phone(data.phone); + self.social(data.social); + }; + + self.saveContact = function (data, e) { + self.firstName.valueHasMutated(); + self.lastName.valueHasMutated(); + self.phone.valueHasMutated(); + self.email.valueHasMutated(); + self.social.valueHasMutated(); + if ((self.firstName.hasError() || self.lastName.hasError() || self.email.hasError() || self.phone.hasError() || self.social.hasError())) { + return; + } + var params = { + contactId: data.contactId(), + firstName: data.firstName(), + lastName: data.lastName(), + email: data.email(), + phone: data.phone(), + social: data.social() + }; + + util.contactService().post("SaveContact", params, + function(data){ + //Success + self.cancel(); + }, + + function(data){ + //Failure + } + ) + + + }; +}; + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/pager.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/pager.js new file mode 100644 index 00000000000..91339ca8be9 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/pager.js @@ -0,0 +1,56 @@ +var pager = { + init: function (viewModel, pageSize, load, resx, pagerItemFormatDesc, nopagerItemFormatDesc) { + viewModel.pageSize = pageSize; + viewModel.pageIndex = ko.observable(0); + + viewModel.startIndex = ko.computed(function () { + return viewModel.pageIndex() * viewModel.pageSize + 1; + }); + + viewModel.endIndex = ko.computed(function () { + return Math.min((viewModel.pageIndex() + 1) * viewModel.pageSize, viewModel.totalResults()); + }); + + viewModel.currentPage = ko.computed(function () { + return viewModel.pageIndex() + 1; + }); + + viewModel.totalPages = ko.computed(function () { + if (typeof viewModel.totalResults === 'function' && viewModel.totalResults()) + return Math.ceil(viewModel.totalResults() / viewModel.pageSize); + return 1; + }); + + viewModel.pagerVisible = ko.computed(function () { + return viewModel.totalPages() > 1; + }); + + viewModel.pagerItemsDescription = ko.computed(function () { + return "Showing " + viewModel.startIndex() + " - " + viewModel.endIndex() + " of " + viewModel.totalResults() + " contacts"; + }); + + viewModel.pagerDescription = ko.computed(function () { + return "Page: " + viewModel.currentPage() + " of " + viewModel.totalPages(); + }); + + viewModel.pagerPrevClass = ko.computed(function () { + return 'prev' + (viewModel.pageIndex() < 1 ? ' disabled' : ''); + }); + + viewModel.pagerNextClass = ko.computed(function () { + return 'next' + (viewModel.pageIndex() >= viewModel.totalPages() - 1 ? ' disabled' : ''); + }); + + viewModel.prev = function () { + if (viewModel.pageIndex() <= 0) return; + viewModel.pageIndex(viewModel.pageIndex() - 1); + if (typeof load === 'function') load.apply(viewModel); + }; + + viewModel.next = function () { + if (viewModel.pageIndex() >= viewModel.totalPages() - 1) return; + viewModel.pageIndex(viewModel.pageIndex() + 1); + if (typeof load === 'function') load.apply(viewModel); + }; + } + }; \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/util.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/util.js new file mode 100644 index 00000000000..2eb08e84d6e --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ClientScripts/util.js @@ -0,0 +1,133 @@ +if (typeof contactList === 'undefined' || contactList === null) { + contactList = {}; +}; + +contactList.utility = function(settings, resx){ + + var resx = resx; + + var alertConfirm = function(text, confirmBtnText, cancelBtnText, confirmHandler, cancelHandler) { + $('#confirmation-dialog > div.dnnDialog').html(text); + $('#confirmation-dialog a#confirmbtn').html(confirmBtnText).unbind('click').bind('click', function() { + if (typeof confirmHandler === 'function') confirmHandler.apply(); + $('#confirmation-dialog').fadeOut(200, 'linear', function() { $('#mask').hide(); }); + }); + + var $cancelBtn = $('#confirmation-dialog a#cancelbtn'); + if(cancelBtnText !== ''){ + $cancelBtn.html(cancelBtnText).unbind('click').bind('click', function() { + if (typeof cancelHandler === 'function') cancelHandler.apply(); + $('#confirmation-dialog').fadeOut(200, 'linear', function() { $('#mask').hide(); }); + }); + $cancelBtn.show(); + } + else { + $cancelBtn.hide(); + } + + $('#mask').show(); + $('#confirmation-dialog').fadeIn(200, 'linear'); + + $(window).off('keydown.confirmDialog').on('keydown.confirmDialog', function(evt) { + + if (evt.keyCode === 27) { + $(window).off('keydown.confirmDialog'); + $('#confirmation-dialog a#cancelbtn').trigger('click'); + } + }); + }; + + var alert = function(text, closeBtnText, closeBtnHandler) { + $('#confirmation-dialog > div.dnnDialogHeader').html(resx.alert); + alertConfirm(text, closeBtnText, "", closeBtnHandler, null) + }; + + var confirm = function(text, confirmBtnText, cancelBtnText, confirmHandler, cancelHandler) { + $('#confirmation-dialog > div.dnnDialogHeader').html(resx.confirm); + alertConfirm(text, confirmBtnText, cancelBtnText, confirmHandler, cancelHandler) + }; + + var sf = contactList.sf(); + sf.init(settings); + + return { + alert: alert, + confirm: confirm, + sf: sf + } +}; + +contactList.sf = function(){ + var serviceController = ""; + 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'); + } + } + } + }; + + 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/ContactList'); + }; + + 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 + } +}; diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/BusinessController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/BusinessController.cs new file mode 100644 index 00000000000..b405c13015e --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/BusinessController.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 + +using System.Collections.Generic; +using System.Web.UI; +using DotNetNuke.Entities.Modules; +using DotNetNuke.Services.Tokens; +using DotNetNuke.UI.Modules; + +namespace Dnn.ContactList.Spa.Components +{ + /// + /// Business controller for the Contact list SPA module. + /// This class needs to implement the interface ICustomTokenProvider in order to define custom tokens that + /// will be managed by the Token Replace Engine. + /// + public class BusinessController : ICustomTokenProvider + { + + /// + /// Implemtentation of the interface ICustomTokenProvider + /// + /// + /// + /// + public IDictionary GetTokens(Page page, ModuleInstanceContext moduleContext) + { + var tokens = new Dictionary(); + tokens["preloadeddata"] = new PreloadedDataPropertyAccess(moduleContext.PortalId); + tokens["contactsettings"] = new SettingsPropertyAccess(moduleContext.ModuleId, moduleContext.TabId); + + return tokens; + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/ContactService.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/ContactService.cs new file mode 100644 index 00000000000..02b4bf73662 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/ContactService.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 + +using System; +using System.Linq; +using Dnn.ContactList.Api; +using DotNetNuke.Collections; +using DotNetNuke.Framework; + +namespace Dnn.ContactList.Spa.Components +{ + + public class ContactService : ServiceLocator, IContactService + { + private readonly IContactRepository _repository; + + /// + /// Default Constructor constructs a new ContactService + /// + public ContactService() + { + _repository = ContactRepository.Instance; + } + + + protected override Func GetFactory() + { + return () => new ContactService(); + } + + public int AddContact(Contact contact, int userId) + { + return _repository.AddContact(contact, userId); + } + + public void DeleteContact(Contact contact) + { + _repository.DeleteContact(contact); + } + + public Contact GetContact(int contactId, int portalId) + { + return _repository.GetContact(contactId, portalId); + } + + public IQueryable GetContacts(int portalId) + { + return _repository.GetContacts(portalId); + } + + public IPagedList GetContacts(string searchTerm, int portalId, int pageIndex, int pageSize) + { + return _repository.GetContacts(searchTerm, portalId, pageIndex, pageSize); + } + + public void UpdateContact(Contact contact, int userId) + { + _repository.UpdateContact(contact, userId); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/IContactService.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/IContactService.cs new file mode 100644 index 00000000000..a109acb376b --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/IContactService.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 + +using System.Linq; +using Dnn.ContactList.Api; +using DotNetNuke.Collections; + +namespace Dnn.ContactList.Spa.Components +{ + /// + /// Provides a facade to access to the ContactRepository class. + /// + public interface IContactService + { + /// + /// AddContact adds a contact to the repository + /// + /// The contact to add + /// The Id of the user making the addition + /// The Id of the contact + int AddContact(Contact contact, int userId); + + /// + /// DeleteContact deletes a contact from the repository + /// + /// The contact to delete + void DeleteContact(Contact contact); + + /// + /// This GetContact method retrieves a specific Contact in a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The Id of the contact + /// The Id of the portal + /// A single of contact + Contact GetContact(int contactId, int portalId); + + /// + /// This GetContacts overload retrieves all the Contacts for a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The Id of the portal + /// A collection of contacts + IQueryable GetContacts(int portalId); + + /// + /// This GetContacts overload retrieves a page of Contacts for a portal + /// + /// Contacts are cached by portal, so this call will check the cache before going to the Database + /// The term to search for + /// The Id of the portal + /// The page Index to fetch - this is 0 based so the first page is when pageIndex = 0 + /// The size of the page to fetch from the database + /// A paged collection of contacts + IPagedList GetContacts(string searchTerm, int portalId, int pageIndex, int pageSize); + + /// + /// UpdateContact updates a contact in the repository + /// + /// The contact to update + /// The Id of the user making the update + void UpdateContact(Contact contact, int userId); + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/ISettingsService.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/ISettingsService.cs new file mode 100644 index 00000000000..fe531ba67c8 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/ISettingsService.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 Dnn.ContactList.Spa.Components +{ + public interface ISettingsService + { + bool IsFormEnabled(int moduleId, int tabId); + + void SaveFormEnabled(bool isEnabled, int moduleId); + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/PreloadedDataPropertyAccess.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/PreloadedDataPropertyAccess.cs new file mode 100644 index 00000000000..df83b953f54 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/PreloadedDataPropertyAccess.cs @@ -0,0 +1,77 @@ +// 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.Globalization; +using System.Linq; +using Dnn.ContactList.Spa.Services.ViewModels; +using DotNetNuke.Common; +using DotNetNuke.Entities.Users; +using DotNetNuke.Services.Tokens; +using Newtonsoft.Json; + +namespace Dnn.ContactList.Spa.Components +{ + /// + /// Implements the Interface IPropertyAccess. This mechanism is used by the Token Replace + /// Engine implemented to be used in SPA modules. The method 'GetProperty' allows to access to + /// information that will be available as properties in the custom token. + /// + public class PreloadedDataPropertyAccess : IPropertyAccess + { + private readonly IContactService _service; + private readonly int _portalId; + private readonly string searchTerm; + private readonly int pageIndex; + private readonly int pageSize; + + /// + /// Default Constructor constructs a new PreloadedDataPropertyAccess + /// + public PreloadedDataPropertyAccess(int portalId) : this(portalId, ContactService.Instance) + { + + } + + /// + /// Constructor constructs a new PreloadedDataPropertyAccess with a passed in service + /// + public PreloadedDataPropertyAccess(int portalId, IContactService service) + { + Requires.NotNull(service); + + _service = service; + _portalId = portalId; + searchTerm = ""; + pageIndex = 0; + pageSize = 6; + } + + /// + /// Token Cacheability. + /// + public virtual CacheLevel Cacheability + { + get { return CacheLevel.notCacheable; } + } + + /// + /// Get Preloaded Data Property. + /// + /// property name. + /// format. + /// format provider. + /// accessing user. + /// access level. + /// Whether found the property value. + /// + public string GetProperty(string propertyName, string format, CultureInfo formatProvider, UserInfo accessingUser, Scope accessLevel, ref bool propertyNotFound) + { + var contactList = _service.GetContacts(searchTerm, _portalId, pageIndex, pageSize); + var contacts = contactList + .Select(contact => new ContactViewModel(contact)) + .ToList(); + return "{ results: " + JsonConvert.SerializeObject(contacts) + ", pageCount: " + contactList.TotalCount + "}"; + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/SettingsPropertyAccess.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/SettingsPropertyAccess.cs new file mode 100644 index 00000000000..80d8f62935b --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/SettingsPropertyAccess.cs @@ -0,0 +1,82 @@ +// 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.Globalization; +using System.Text.RegularExpressions; +using DotNetNuke.Common; +using DotNetNuke.Entities.Users; +using DotNetNuke.Services.Tokens; + +namespace Dnn.ContactList.Spa.Components +{ + /// + /// Implements the Interface IPropertyAccess. This mechanism is used by the Token Replace + /// Engine implemented to be used in SPA modules. The method 'GetProperty' allows to access to + /// information that will be available as properties in the custom token. + /// + public class SettingsPropertyAccess : IPropertyAccess + { + private readonly ISettingsService _service; + private readonly int _moduleId; + private readonly int _tabId; + + /// + /// Default Constructor constructs a new PreloadedDataPropertyAccess + /// + public SettingsPropertyAccess(int moduleId, int tabId) : this(moduleId, tabId, SettingsService.Instance) + { + + } + + /// + /// Constructor constructs a new PreloadedDataPropertyAccess with a passed in service + /// + public SettingsPropertyAccess(int moduleId, int tabId, ISettingsService service) + { + Requires.NotNull(service); + + _service = service; + _tabId = tabId; + _moduleId = moduleId; + } + + /// + /// Token Cacheability. + /// + public virtual CacheLevel Cacheability + { + get { return CacheLevel.notCacheable; } + } + + /// + /// Get Setting Property. + /// + /// property name. + /// format. + /// format provider. + /// accessing user. + /// access level. + /// Whether found the property value. + /// + public string GetProperty(string propertyName, string format, CultureInfo formatProvider, UserInfo accessingUser, Scope accessLevel, ref bool propertyNotFound) + { + string propertyValue = ""; + + switch (propertyName) + { + case "IsFormEnabled": + propertyValue = _service.IsFormEnabled(_moduleId, _tabId).ToString().ToLower(); + break; + case "EmailRegex": + propertyValue = Regex.Escape(Globals.glbEmailRegEx); + break; + default: + propertyNotFound = true; + break; + } + + return propertyValue; + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/SettingsService.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/SettingsService.cs new file mode 100644 index 00000000000..96cf75a01cb --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Components/SettingsService.cs @@ -0,0 +1,38 @@ +// 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 DotNetNuke.Entities.Modules; +using DotNetNuke.Framework; + +namespace Dnn.ContactList.Spa.Components +{ + public class SettingsService : ServiceLocator, ISettingsService + { + private readonly IModuleController _moduleController; + private const string IsFormEnabledKey = "IsFormEnabledKey"; + + public SettingsService() + { + _moduleController = ModuleController.Instance; + } + protected override Func GetFactory() + { + return () => new SettingsService(); + } + + public bool IsFormEnabled(int moduleId, int tabId) + { + var module = _moduleController.GetModule(moduleId, tabId, true); + var moduleSettings = module.ModuleSettings; + + return moduleSettings[IsFormEnabledKey] != null && Boolean.Parse((string) moduleSettings[IsFormEnabledKey]); + } + + public void SaveFormEnabled(bool isEnabled, int moduleId) + { + _moduleController.UpdateModuleSetting(moduleId, IsFormEnabledKey, isEnabled.ToString()); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.html b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.html new file mode 100644 index 00000000000..1de46427e09 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.html @@ -0,0 +1,201 @@ +[JavaScript:{ jsname: "Knockout" }] +[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"}] +[JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/pager.js"}] +[ModuleAction:{controlKey : "ShowInfo", securityAccessLevel : "Edit", titleKey: "ShowModuleInfo", localResourceFile: "~/DesktopModules/Dnn/ContactList/App_LocalResources/ShowInfo.resx" }] + +
    +
    +
    +
    +
    + +
    +
    ACME CORP
    +
    INNOVATION & SOLUTIONS
    +
    +
    + +
    +
    +
    +
    + + +
    +
    [Resx:{key:"ContactPerson"}]
    +
    + + +
    +
    + + +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + + + + + + +
    +
    + +
    +
    +

    + + +

    +
    +
    +
    +
    + + + + +
    +
    + + + + +
    +
    +
    + + + + +
    +
    + + + + +
    +
    + + + + +
    +
    + +
    +
    + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.js b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.js new file mode 100644 index 00000000000..377b7be5e32 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList.js @@ -0,0 +1,26 @@ +function ContactList($, ko, settings, resx, preloadedData){ + var $rootElement; + + var viewModel = {}; + + var init = function(element) { + $rootElement = $(element); + + var config = { + settings: settings, + preloadedData: preloadedData, + resx: resx, + util: contactList.utility(settings, resx), + $rootElement: $rootElement + }; + + viewModel = new contactList.contactsViewModel(config); + viewModel.init(); + + ko.applyBindings(viewModel, $rootElement[0]); + } + + return { + init: init + } +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList_Spa.dnn b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList_Spa.dnn new file mode 100644 index 00000000000..2b6b9654d72 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ContactList_Spa.dnn @@ -0,0 +1,102 @@ + + + + Contact List Spa + DNN Contact List SPA + + DNN + DNN Corp. + http://www.dnnsoftware.com + support@dnnsoftware.com + + + + true + + 08.00.00 + + + + + DesktopModules\Dnn\ContactList\ + + + + + + + Dnn.ContactList.Spa + Dnn/ContactList + Dnn.ContactList.Spa.Components.BusinessController + + + + Contact List + 0 + + + + DesktopModules/Dnn/ContactList/ContactList.html + False + View + False + 0 + + + ShowInfo + DesktopModules/Dnn/ContactList/ShowInfo.html + False + Edit Content + Edit + + + 0 + True + + + QuickSettings + DesktopModules/Dnn/ContactList/Settings.html + False + DnnSpaModule Settings + Edit + + + 0 + + + + + + + + + + bin + Dnn.ContactList.Spa.dll + + + bin + Dnn.ContactList.Api.dll + + + + + + DesktopModules/Dnn/ContactList + + Resources.zip + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Dnn.ContactList.Spa.csproj b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Dnn.ContactList.Spa.csproj new file mode 100644 index 00000000000..a33be68b2a1 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Dnn.ContactList.Spa.csproj @@ -0,0 +1,46 @@ + + + Dnn.ContactList.Spa + net48 + bin\ + false + false + Dnn.ContactList.Spa + Dnn ContactList Spa Module + en-US + + Library + + Portable + False + false + True + .\bin + netstandard2.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + SolutionInfo.cs + + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Module.build b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Module.build new file mode 100644 index 00000000000..764fb18992f --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Module.build @@ -0,0 +1,36 @@ + + + + $(MSBuildProjectDirectory)\..\..\..\.. + + + + zip + ContactList_Spa + Dnn.ContactList.Spa + $(WebsitePath)\DesktopModules\Dnn\ContactList + $(PathToArtifacts)\SampleModules + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Properties/AssemblyInfo.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..9aec54bfbca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/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.Spa/Properties/launchSettings.json b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Properties/launchSettings.json new file mode 100644 index 00000000000..a9c4766bbbd --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Dnn.ContactList.Spa": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:51708" + } + } +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ReleaseNotes.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ReleaseNotes.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ReleaseNotes.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ContactController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ContactController.cs new file mode 100644 index 00000000000..c2dc1c8f4f1 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ContactController.cs @@ -0,0 +1,160 @@ +// 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.Linq; +using System.Net.Http; +using System.Web.Http; +using Dnn.ContactList.Api; +using Dnn.ContactList.Spa.Components; +using Dnn.ContactList.Spa.Services.ViewModels; +using DotNetNuke.Common; +using DotNetNuke.Security; +using DotNetNuke.Web.Api; + +namespace Dnn.ContactList.Spa.Services +{ + /// + /// ContentTypeController provides the Web Services to manage Data Types + /// + [SupportedModules("Dnn.ContactList.Spa")] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)] + public class ContactController : DnnApiController + { + private readonly IContactService _contactService; + + /// + /// Default Constructor constructs a new ContactController + /// + public ContactController() : this(ContactService.Instance) + { + + } + + /// + /// Constructor constructs a new ContactController with a passed in repository + /// + public ContactController(IContactService service) + { + Requires.NotNull(service); + + _contactService = service; + } + + /// + /// The DeleteContact method deletes a single contact + /// + /// + [HttpPost] + public HttpResponseMessage DeleteContact(ContactViewModel viewModel) + { + var contact = _contactService.GetContact(viewModel.ContactId, PortalSettings.PortalId); + + _contactService.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 = new ContactViewModel(_contactService.GetContact(contactId, PortalSettings.PortalId)); + + var response = new + { + success = true, + data = new + { + contact = contact + } + }; + + return Request.CreateResponse(response); + } + + /// + /// The GetContacts method gets all the contacts + /// + /// + [HttpGet] + public HttpResponseMessage GetContacts(string searchTerm, int pageSize, int pageIndex) + { + var contactList = _contactService.GetContacts(searchTerm, PortalSettings.PortalId, pageIndex, pageSize); + var contacts = contactList + .Select(contact => new ContactViewModel(contact)) + .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] + public HttpResponseMessage SaveContact(ContactViewModel viewModel) + { + Contact contact; + + if (viewModel.ContactId == -1) + { + contact = new Contact + { + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Email = viewModel.Email, + Phone = viewModel.Phone, + Social = viewModel.Social, + PortalId = PortalSettings.PortalId + }; + _contactService.AddContact(contact, UserInfo.UserID); + } + else + { + //Update + contact = _contactService.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; + } + _contactService.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.Spa/Services/ServiceRouteMapper.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ServiceRouteMapper.cs new file mode 100644 index 00000000000..fb4421df073 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ServiceRouteMapper.cs @@ -0,0 +1,23 @@ +// 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.Web.Api; + +namespace Dnn.ContactList.Spa.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/ContactList", "default", "{controller}/{action}", new[] { "Dnn.ContactList.Spa.Services" }); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/SettingsController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/SettingsController.cs new file mode 100644 index 00000000000..dbfd4c7472e --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/SettingsController.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 + +using System.Net.Http; +using System.Web.Http; +using Dnn.ContactList.Spa.Components; +using Dnn.ContactList.Spa.Services.ViewModels; +using DotNetNuke.Security; +using DotNetNuke.Web.Api; + +namespace Dnn.ContactList.Spa.Services +{ + /// + /// ContentTypeController provides the Web Services to manage Data Types + /// + [SupportedModules("Dnn.ContactList.Spa")] + [DnnModuleAuthorize(AccessLevel = SecurityAccessLevel.View)] + public class SettingsController : DnnApiController + { + private readonly ISettingsService _setttingsService; + + /// + /// Default Constructor constructs a new SettingsController + /// + public SettingsController() + { + _setttingsService = SettingsService.Instance; + } + + /// + /// The DeleteContact method deletes a single contact + /// + /// + [HttpPost] + public HttpResponseMessage SaveSettings(SettingsViewModel settings) + { + _setttingsService.SaveFormEnabled(settings.IsFormEnabled, ActiveModule.ModuleID); + + var response = new + { + success = true + }; + + return Request.CreateResponse(response); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ViewModels/ContactViewModel.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ViewModels/ContactViewModel.cs new file mode 100644 index 00000000000..100a0c42ba0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ViewModels/ContactViewModel.cs @@ -0,0 +1,73 @@ +// 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 Dnn.ContactList.Api; +using Newtonsoft.Json; + +namespace Dnn.ContactList.Spa.Services.ViewModels +{ + /// + /// ContactViewModel represents a Contact object within the Contact Web Service API + /// + [JsonObject(MemberSerialization.OptIn)] + public class ContactViewModel + { + /// + /// Constructs a ContactViewModel + /// + public ContactViewModel() + { + } + + /// + /// Constructs a ContactViewModel from a Contact object + /// + /// + public ContactViewModel(Contact contact) + { + ContactId = contact.ContactId; + Email = contact.Email; + FirstName = contact.FirstName; + LastName = contact.LastName; + Phone = contact.Phone; + Social = contact.Social; + } + + /// + /// The Id of the contact + /// + [JsonProperty("contactId")] + public int ContactId { get; set; } + + /// + /// The Email of the contact + /// + [JsonProperty("email")] + public string Email { get; set; } + + /// + /// The First Name of the contact + /// + [JsonProperty("firstName")] + public string FirstName { get; set; } + + /// + /// The Last Name of the contact + /// + [JsonProperty("lastName")] + public string LastName { get; set; } + + /// + /// The Phone of the contact + /// + [JsonProperty("phone")] + public string Phone { get; set; } + + /// + /// The Social of the contact + /// + [JsonProperty("social")] + public string Social { get; set; } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ViewModels/SettingsViewModel.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ViewModels/SettingsViewModel.cs new file mode 100644 index 00000000000..af39c783d0c --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Services/ViewModels/SettingsViewModel.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 + +using Newtonsoft.Json; + +namespace Dnn.ContactList.Spa.Services.ViewModels +{ + [JsonObject(MemberSerialization.OptIn)] + public class SettingsViewModel + { + public SettingsViewModel() + { + + } + + [JsonProperty("isFormEnabled")] + public bool IsFormEnabled { get; set; } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Settings.css b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Settings.css new file mode 100644 index 00000000000..e3a300f4aae --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Settings.css @@ -0,0 +1,8 @@ +.contactSpa.settings { + padding: 15px; +} +.contactSpa.settings label { + font-weight: bold; + font-style: italic; + padding-right: 10px; +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Settings.html b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Settings.html new file mode 100644 index 00000000000..3f5b813e208 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/Settings.html @@ -0,0 +1,27 @@ +[JavaScript:{ jsname: "JQuery" }] +[JavaScript:{ jsname: "Knockout" }] +[JavaScript:{ path: "~/Resources/Shared/scripts/dnn.jquery.js"}] +[JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/QuickSettingsDispatcher.js"}] +[JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/QuickSettings.js"}] +[JavaScript:{ path: "~/DesktopModules/Dnn/ContactList/ClientScripts/util.js"}] + +
    +
    + +
    +
    + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ShowInfo.css b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ShowInfo.css new file mode 100644 index 00000000000..dd89ed37e56 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ShowInfo.css @@ -0,0 +1,3 @@ +.contactListSpa.settings { + background-color: lightgray; +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ShowInfo.html b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ShowInfo.html new file mode 100644 index 00000000000..2cbe520f034 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/ShowInfo.html @@ -0,0 +1,8 @@ +
    + [Resx:{key:'ModuleId'}]: [ModuleContext:ModuleId]
    + [Resx:{key:'IsSuperUser'}]: [ModuleContext:IsSuperUser]
    + [Resx:{key:'TabModuleId'}]: [ModuleContext:TabModuleId]
    + [Resx:{key:'TabId'}]: [ModuleContext:TabId]
    + [Resx:{key:'PortalId'}]: [ModuleContext:PortalId]
    + [Resx:{key:'EditMode'}]: [ModuleContext:EditMode]
    +
    diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/license.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/license.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/license.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/module.css b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/module.css new file mode 100644 index 00000000000..25f20d80a1b --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.Spa/module.css @@ -0,0 +1,410 @@ +@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 */ +.editContact { + 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; + margin-top: 30px; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.editContact-header { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + padding: 20px 30px; +} + +.editContact-title { + color: #ffffff; + font-size: 20px; + font-weight: 700; + margin: 0; + display: flex; + align-items: center; + gap: 12px; +} + +.editContact-title .fa { + font-size: 22px; +} + +.editContact-body { + padding: 30px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 0; +} + +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } +} + +.form-group { + margin-bottom: 20px; +} + +.form-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + color: #333333; + margin-bottom: 8px; +} + +.form-label .fa { + font-size: 14px; + color: var(--dnn-color-tertiary-light,#3c7a9a); + width: 16px; +} + +.form-control { + width: 100%; + padding: 12px 16px; + font-size: 14px; + font-family: 'Open Sans', sans-serif; + border: 1px solid #ddd; + border-radius: 6px; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.form-control:focus { + outline: none; + border-color: var(--dnn-color-tertiary-light,#3c7a9a); + box-shadow: 0 0 0 3px rgba(60, 122, 154, 0.1); +} + +.form-control::placeholder { + color: #999; + opacity: 0.7; +} + +input.has-error { + border-color: #d9534f; + background-color: #fff5f5; +} + +.form-control.has-error:focus { + border-color: #d9534f; + box-shadow: 0 0 0 3px rgba(217, 83, 79, 0.1); +} + +span.form-error { + display: block; + color: #d9534f; + font-size: 13px; + margin-top: 6px; + font-weight: 500; +} + +.editContact-footer { + padding: 20px 30px; + background-color: #f8f9fa; + border-top: 1px solid #e9ecef; + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.btn-cancel, +.btn-save { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + font-size: 14px; + font-weight: 600; + border-radius: 6px; + text-decoration: none; + transition: all 0.2s ease; + cursor: pointer; + border: none; +} + +.btn-cancel { + background-color: #6c757d; + color: #ffffff; +} + +.btn-cancel:hover { + background-color: #5a6268; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3); +} + +.btn-save { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + color: #ffffff; +} + +.btn-save:hover { + background-color: #2f6580; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(60, 122, 154, 0.3); +} + +.btn-cancel .fa, +.btn-save .fa { + font-size: 14px; +} + +.buttons { + margin-top: 10px; + text-align: center; + padding-top: 20px; +} + +#ControlBar, #ControlBar *, .actionMenu * { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } + + +div.right, div.left { + line-height: 30px; +} + +ul.pager { + margin: 0 0 0 25px; +} + +ul.pager li>a { + border-radius: 3px; + color: black; + font-weight: bold; + cursor: pointer; +} + +ul.pager li>a.disabled { + background: #dbdbdb; +} + +ul.pager li>a:hover { + background: #f6f6f6; +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/.gitignore b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/.gitignore new file mode 100644 index 00000000000..9b303d3a3fc --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/.gitignore @@ -0,0 +1,43 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Build output +bin/ +obj/ +**/*.user +dist/ +scripts/contact-list.js +scripts/contact-list.js.map + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# OS +.DS_Store +Thumbs.db diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Api/ContactsController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Api/ContactsController.cs new file mode 100644 index 00000000000..f91ef1cbffc --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Api/ContactsController.cs @@ -0,0 +1,105 @@ +using System.Net; +using System.Net.Http; +using System.Web.Http; +using Dnn.ContactList.Api; +using Dnn.ContactList.SpaReact.Dto; +using DotNetNuke.Collections; +using DotNetNuke.Web.Api; + +namespace Dnn.ContactList.SpaReact.Api +{ + public class ContactsController : DnnApiController + { + private readonly IContactRepository contactRepository; + + public ContactsController(IContactRepository contactRepository) + { + this.contactRepository = contactRepository; + } + + /// + /// The Page method gets a page list of contacts + /// + /// + [HttpGet] + [DnnModuleAuthorize(AccessLevel = DotNetNuke.Security.SecurityAccessLevel.View)] + public HttpResponseMessage Page(string searchTerm, int pageSize, int pageIndex) + { + var contactList = contactRepository.GetContacts(searchTerm, PortalSettings.PortalId, pageIndex, pageSize); + return Request.CreateResponse(HttpStatusCode.OK, contactList.Serialize(x => new ContactDto(x))); + } + + /// + /// The Contact method gets a single contact + /// + /// + [HttpGet] + [DnnModuleAuthorize(AccessLevel = DotNetNuke.Security.SecurityAccessLevel.View)] + public HttpResponseMessage Contact(int id) + { + return Request.CreateResponse(HttpStatusCode.OK, new ContactDto(contactRepository.GetContact(id, PortalSettings.PortalId))); + } + + /// + /// The Update method persists the Contact to the repository + /// + /// + /// + [HttpPost] + [DnnModuleAuthorize(AccessLevel = DotNetNuke.Security.SecurityAccessLevel.Edit)] + public HttpResponseMessage Contact(int id, ContactDto viewModel) + { + Contact contact; + + if (id == -1) + { + contact = new Contact + { + FirstName = viewModel.FirstName, + LastName = viewModel.LastName, + Email = viewModel.Email, + Phone = viewModel.Phone, + Social = viewModel.Social, + PortalId = PortalSettings.PortalId + }; + contact.ContactId = this.contactRepository.AddContact(contact, UserInfo.UserID); + } + else + { + //Update + contact = this.contactRepository.GetContact(id, 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; + } + this.contactRepository.UpdateContact(contact, UserInfo.UserID); + } + + return Request.CreateResponse(HttpStatusCode.OK, new ContactDto(contactRepository.GetContact(contact.ContactId, PortalSettings.PortalId))); + } + + /// + /// The Delete method deletes a single contact + /// + /// + [HttpPost] + [DnnModuleAuthorize(AccessLevel = DotNetNuke.Security.SecurityAccessLevel.Edit)] + public HttpResponseMessage Delete(int id) + { + var contact = this.contactRepository.GetContact(id, PortalSettings.PortalId); + this.contactRepository.DeleteContact(contact); + + var response = new + { + success = true + }; + + return Request.CreateResponse(HttpStatusCode.OK, response); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Api/RouteMapper.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Api/RouteMapper.cs new file mode 100644 index 00000000000..92547f68146 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Api/RouteMapper.cs @@ -0,0 +1,13 @@ +using DotNetNuke.Web.Api; + +namespace Dnn.ContactList.SpaReact.Api +{ + public class RouteMapper : IServiceRouteMapper + { + public void RegisterRoutes(IMapRoute mapRouteManager) + { + mapRouteManager.MapHttpRoute("Dnn/ContactListSpaReact", "DnnContactListSpaReact1", "{controller}/{action}", new[] { "Dnn.ContactList.SpaReact.Api" }); + mapRouteManager.MapHttpRoute("Dnn/ContactListSpaReact", "DnnContactListSpaReact2", "{controller}/{action}/{id}", null, new { id = @"-?\d+" }, new[] { "Dnn.ContactList.SpaReact.Api" }); + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/ContactList.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/ContactList.resx new file mode 100644 index 00000000000..1a7197a02da --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/ContactList.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 + + + Add New Contact + + + Cancel + + + Delete {0} + + + Delete + + + Are you sure you want to delete this contact? + + + Edit Contact + + + Email Address + + + First Name + + + Last Name + + + Phone + + + Save + + + Social + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/Settings.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/Settings.resx new file mode 100644 index 00000000000..ba099ab5323 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/Settings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Allow creation of new contacts? + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/ShowInfo.resx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/ShowInfo.resx new file mode 100644 index 00000000000..909a0d2f1ca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/App_LocalResources/ShowInfo.resx @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Are you in edit mode? + + + Are you a super user? + + + ModuleId + + + Portal Identifier + + + Show module info + + + Tab Identifier + + + Tab Module identifier + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/BusinessController.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/BusinessController.cs new file mode 100644 index 00000000000..9fac250db82 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/BusinessController.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 + +using System.Collections.Generic; +using System.Web.UI; +using DotNetNuke.Entities.Modules; +using DotNetNuke.Entities.Users; +using DotNetNuke.Services.Tokens; +using DotNetNuke.UI.Modules; + +namespace Dnn.ContactList.SpaReact.Components +{ + /// + /// Business controller for the Contact list SPA module. + /// This class needs to implement the interface ICustomTokenProvider in order to define custom tokens that + /// will be managed by the Token Replace Engine. + /// + public class BusinessController : ICustomTokenProvider + { + /// + /// Implementation of the interface ICustomTokenProvider + /// + /// + /// + /// + public IDictionary GetTokens(Page page, ModuleInstanceContext moduleContext) + { + var tokens = new Dictionary(); + tokens["context"] = new ContextTokens(moduleContext, UserController.Instance.GetCurrentUserInfo()); + + return tokens; + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/ContextTokens.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/ContextTokens.cs new file mode 100644 index 00000000000..fda4d55303b --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/ContextTokens.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 + +using System.Globalization; +using System.Web; +using DotNetNuke.Entities.Users; +using DotNetNuke.Services.Tokens; +using DotNetNuke.UI.Modules; +using Newtonsoft.Json; + +namespace Dnn.ContactList.SpaReact.Components +{ + public class ContextTokens : IPropertyAccess + { + public CacheLevel Cacheability => CacheLevel.notCacheable; + private readonly SecurityContext security; + private readonly ModuleInstanceContext moduleContext; + + public ContextTokens(ModuleInstanceContext moduleContext, UserInfo user) + { + this.moduleContext = moduleContext; + this.security = new SecurityContext(moduleContext.Configuration, user); + } + + public string GetProperty(string propertyName, string format, CultureInfo formatProvider, UserInfo accessingUser, Scope accessLevel, ref bool propertyNotFound) + { + switch (propertyName.ToLower()) + { + case "security": + return HttpUtility.HtmlAttributeEncode(JsonConvert.SerializeObject(this.security)); + case "module": + return HttpUtility.HtmlAttributeEncode(JsonConvert.SerializeObject(new + { + this.moduleContext.ModuleId, + this.moduleContext.TabId, + this.moduleContext.TabModuleId, + this.moduleContext.PortalId, + })); + default: + propertyNotFound = true; + return string.Empty; + } + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/SecurityContext.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/SecurityContext.cs new file mode 100644 index 00000000000..2a7696f6398 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Components/SecurityContext.cs @@ -0,0 +1,47 @@ +// 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.Entities.Modules; +using DotNetNuke.Entities.Users; +using DotNetNuke.Security.Permissions; + +namespace Dnn.ContactList.SpaReact.Components +{ + public class SecurityContext + { + public bool CanView { get; set; } + public bool CanEdit { get; set; } + public bool IsAdmin { get; set; } + private UserInfo user { get; set; } + public int UserId + { + get + { + return user.UserID; + } + } + + public SecurityContext(ModuleInfo objModule, UserInfo user) + { + this.user = user; + if (user.IsSuperUser) + { + CanView = CanEdit = IsAdmin = true; + } + else + { + IsAdmin = user.IsAdmin; + if (IsAdmin) + { + CanView = CanEdit = true; + } + else + { + CanView = ModulePermissionController.CanViewModule(objModule); + CanEdit = ModulePermissionController.HasModulePermission(objModule.ModulePermissions, "EDIT"); + } + } + } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ContactList.html b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ContactList.html new file mode 100644 index 00000000000..cea105aed43 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ContactList.html @@ -0,0 +1,4 @@ +[JavaScript:{ path: "~/DesktopModules/Dnn/ContactListSpaReact/scripts/contact-list.js"}] + +
    +
    diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ContactList_SpaReact.dnn b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ContactList_SpaReact.dnn new file mode 100644 index 00000000000..1480ed977e0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ContactList_SpaReact.dnn @@ -0,0 +1,81 @@ + + + + Contact List Spa React + DNN Contact List SPA+React module + + DNN + DNN Corp. + http://www.dnnsoftware.com + support@dnnsoftware.com + + + + true + + 10.01.00 + + + + + DesktopModules\MVC\Dnn\ContactList\ + + + + + + + Dnn.ContactList.SpaReact + Dnn/ContactListSpaReact + Dnn.ContactList.SpaReact.Components.BusinessController + + + + Contact List + 0 + + + + DesktopModules/Dnn/ContactListSpaReact/ContactList.html + False + View + False + 0 + + + + + + + + + + bin + Dnn.ContactList.SpaReact.dll + + + bin + Dnn.ContactList.Api.dll + + + + + + DesktopModules/Dnn/ContactListSpaReact + + Resources.zip + + + + + + + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Dnn.ContactList.SpaReact.csproj b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Dnn.ContactList.SpaReact.csproj new file mode 100644 index 00000000000..fd8c8cda863 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Dnn.ContactList.SpaReact.csproj @@ -0,0 +1,46 @@ + + + Dnn.ContactList.SpaReact + net48 + bin\ + false + false + Dnn.ContactList.SpaReact + Dnn ContactList Spa React Module + en-US + + Library + + Portable + False + false + True + .\bin + netstandard2.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + SolutionInfo.cs + + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Dto/ContactDto.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Dto/ContactDto.cs new file mode 100644 index 00000000000..48e76d61414 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Dto/ContactDto.cs @@ -0,0 +1,26 @@ +namespace Dnn.ContactList.SpaReact.Dto +{ + public class ContactDto + { + public ContactDto() + { + } + public ContactDto(Dnn.ContactList.Api.Contact contact) + { + this.ContactId = contact.ContactId; + this.PortalId = contact.PortalId; + this.FirstName = contact.FirstName; + this.LastName = contact.LastName; + this.Email = contact.Email; + this.Phone = contact.Phone; + this.Social = contact.Social; + } + public int ContactId { get; set; } + public int PortalId { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public string Social { get; set; } + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Module.build b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Module.build new file mode 100644 index 00000000000..d12d6086bf6 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Module.build @@ -0,0 +1,36 @@ + + + + $(MSBuildProjectDirectory)\..\..\..\.. + + + + zip + ContactList_SpaReact + Dnn.ContactList.SpaReact + $(WebsitePath)\DesktopModules\Dnn\ContactListSpaReact + $(PathToArtifacts)\SampleModules + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Properties/AssemblyInfo.cs b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..9aec54bfbca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/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.SpaReact/Properties/launchSettings.json b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Properties/launchSettings.json new file mode 100644 index 00000000000..06ab977e555 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Dnn.ContactList.SpaReact": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:51709" + } + } +} \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/README.md b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/README.md new file mode 100644 index 00000000000..370c880d89f --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/README.md @@ -0,0 +1,51 @@ +# ContactList SpaReact Module + +A modern React TypeScript SPA module for DNN Platform that provides contact management functionality. + +## Features + +- Display contacts in a responsive card grid layout +- Pagination support (6 contacts per page) +- Search support +- Add, edit, and delete contacts +- Role-based security (edit/delete buttons shown based on user permissions) +- Form validation with real-time feedback +- Modern UI with smooth animations + +## Technology Stack + +- React 18 +- TypeScript 5 +- React Router 6 +- Vite 5 (build tool) +- DNN ServicesFramework for API authentication + +## Project Structure + +``` +src/ +├── main.tsx # Entry point +├── App.tsx # Root component with router +├── types/ # TypeScript interfaces +│ ├── Contact.ts +│ ├── Security.ts +│ └── Module.ts +├── services/ +│ └── services.ts # API calls to DNN backend +├── pages/ +│ ├── ContactList.tsx # Main list view +│ └── ContactForm.tsx # Add/Edit form +├── components/ +│ ├── ContactCard.tsx # Contact card component +│ └── Pagination.tsx # Pagination controls +└── utils/ + └── validation.ts # Form validation +``` + +## Development + +Watch mode for development (with sourcemaps, unminified): +```bash +yarn run watch --scope dnn.contactlist.spareact +``` + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ReleaseNotes.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ReleaseNotes.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/ReleaseNotes.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/license.txt b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/license.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/license.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/module.css b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/module.css new file mode 100644 index 00000000000..02579f28718 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/module.css @@ -0,0 +1,468 @@ +@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; + cursor: pointer; +} + +.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; +} + +/* Search and Pagination Controls */ +.contactList-controls { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 0; + gap: 20px; +} + +.contactList-search { + position: relative; + flex: 0 0 auto; + min-width: 250px; +} + +.search-input { + width: 100%; + padding: 12px 16px 12px 40px; + font-size: 14px; + font-family: 'Open Sans', sans-serif; + border: 1px solid #ddd; + border-radius: 6px; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.search-input:focus { + outline: none; + border-color: var(--dnn-color-tertiary-light,#3c7a9a); + box-shadow: 0 0 0 3px rgba(60, 122, 154, 0.1); +} + +.search-input::placeholder { + color: #999; + opacity: 0.7; +} + +.search-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--dnn-color-tertiary-light,#3c7a9a); + font-size: 14px; + pointer-events: none; +} + +@media (max-width: 768px) { + .contactList-controls { + flex-direction: column; + align-items: stretch; + } + + .contactList-search { + width: 100%; + } +} + +/* Pagination */ +.contactList-pagination { + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + flex: 0 0 auto; +} + +.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; + cursor: pointer; +} + +.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 */ +.editContact { + 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; + margin-top: 30px; + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.editContact-header { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + padding: 20px 30px; +} + +.editContact-title { + color: #ffffff; + font-size: 20px; + font-weight: 700; + margin: 0; + display: flex; + align-items: center; + gap: 12px; +} + +.editContact-title .fa { + font-size: 22px; +} + +.editContact-body { + padding: 30px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 0; +} + +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } +} + +.form-group { + margin-bottom: 20px; +} + +.form-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + color: #333333; + margin-bottom: 8px; +} + +.form-label .fa { + font-size: 14px; + color: var(--dnn-color-tertiary-light,#3c7a9a); + width: 16px; +} + +.form-control { + width: 100%; + padding: 12px 16px; + font-size: 14px; + font-family: 'Open Sans', sans-serif; + border: 1px solid #ddd; + border-radius: 6px; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.form-control:focus { + outline: none; + border-color: var(--dnn-color-tertiary-light,#3c7a9a); + box-shadow: 0 0 0 3px rgba(60, 122, 154, 0.1); +} + +.form-control::placeholder { + color: #999; + opacity: 0.7; +} + +input.has-error { + border-color: #d9534f; + background-color: #fff5f5; +} + +.form-control.has-error:focus { + border-color: #d9534f; + box-shadow: 0 0 0 3px rgba(217, 83, 79, 0.1); +} + +span.form-error { + display: block; + color: #d9534f; + font-size: 13px; + margin-top: 6px; + font-weight: 500; +} + +.editContact-footer { + padding: 20px 30px; + background-color: #f8f9fa; + border-top: 1px solid #e9ecef; + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.btn-cancel, +.btn-save { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + font-size: 14px; + font-weight: 600; + border-radius: 6px; + text-decoration: none; + transition: all 0.2s ease; + cursor: pointer; + border: none; +} + +.btn-cancel { + background-color: #6c757d; + color: #ffffff; +} + +.btn-cancel:hover { + background-color: #5a6268; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(108, 117, 125, 0.3); +} + +.btn-save { + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + color: #ffffff; +} + +.btn-save:hover { + background-color: #2f6580; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(60, 122, 154, 0.3); +} + +.btn-cancel .fa, +.btn-save .fa { + font-size: 14px; +} + +.buttons { + margin-top: 10px; + text-align: center; + padding-top: 20px; +} + +.buttons .dnnPrimaryAction { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + font-size: 14px; + font-weight: 600; + border-radius: 6px; + text-decoration: none; + transition: all 0.2s ease; + cursor: pointer; + border: none; + background-color: var(--dnn-color-tertiary-light,#3c7a9a); + color: #ffffff; +} + +.buttons .dnnPrimaryAction:hover { + background-color: #2f6580; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(60, 122, 154, 0.3); +} + +#ControlBar, #ControlBar *, .actionMenu * { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/package.json b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/package.json new file mode 100644 index 00000000000..aa97b707188 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/package.json @@ -0,0 +1,24 @@ +{ + "name": "dnn.contactlist.spareact", + "version": "1.0.0", + "type": "module", + "packageManager": "yarn@4.10.3", + "scripts": { + "dev": "vite", + "build": "tsc && vite build --mode production", + "watch": "tsc && vite build --watch --mode development", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.4", + "vite": "^5.4.3" + } +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/App.tsx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/App.tsx new file mode 100644 index 00000000000..14876681b4c --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/App.tsx @@ -0,0 +1,23 @@ +import { HashRouter as Router, Routes, Route } from 'react-router-dom'; +import { SecurityContext } from './types/Security'; +import { ModuleContext } from './types/Module'; +import ContactList from './pages/ContactList'; +import ContactForm from './pages/ContactForm'; + +interface AppProps { + security: SecurityContext; + moduleContext: ModuleContext; +} + +export default function App({ security, moduleContext }: AppProps) { + return ( + + + } /> + } /> + } /> + + + ); +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/components/ContactCard.tsx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/components/ContactCard.tsx new file mode 100644 index 00000000000..0d113003e8c --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/components/ContactCard.tsx @@ -0,0 +1,99 @@ +import { Contact } from '../types/Contact'; + +interface ContactCardProps { + contact: Contact; + canEdit: boolean; + onEdit: (contact: Contact) => void; + onDelete: (contact: Contact) => void; +} + +export default function ContactCard({ contact, canEdit, onEdit, onDelete }: ContactCardProps) { + const handleEdit = () => { + onEdit(contact); + }; + + const handleDelete = () => { + if (window.confirm(`Are you sure you want to delete ${contact.FirstName} ${contact.LastName}?`)) { + onDelete(contact); + } + }; + + return ( +
    +
    +
    + + {/* Left chevron/mountain */} + + {/* Middle chevron/mountain */} + + {/* Right chevron/mountain (partial) */} + + +
    +
    +
    ACME CORP
    +
    INNOVATION & SOLUTIONS
    +
    + {canEdit && ( + + )} +
    +
    +
    + {contact.FirstName} {contact.LastName} +
    +
    Contact Person
    +
    + + {contact.Email} +
    +
    + + {contact.Phone} +
    + {contact.Social && ( + + )} +
    +
    + ); +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/components/Pagination.tsx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/components/Pagination.tsx new file mode 100644 index 00000000000..b97b3448a4a --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/components/Pagination.tsx @@ -0,0 +1,54 @@ +interface PaginationProps { + currentPage: number; + totalItems: number; + pageSize: number; + onPageChange: (page: number) => void; +} + +export default function Pagination({ currentPage, totalItems, pageSize, onPageChange }: PaginationProps) { + const totalPages = Math.ceil(totalItems / pageSize); + const startItem = totalItems === 0 ? 0 : currentPage * pageSize + 1; + const endItem = Math.min((currentPage + 1) * pageSize, totalItems); + + const handlePrev = () => { + if (currentPage > 0) { + onPageChange(currentPage - 1); + } + }; + + const handleNext = () => { + if (currentPage < totalPages - 1) { + onPageChange(currentPage + 1); + } + }; + + if (totalItems === 0) { + return null; + } + + return ( + + ); +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/main.tsx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/main.tsx new file mode 100644 index 00000000000..af0603ad201 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/main.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import { SecurityContext } from './types/Security'; +import { ModuleContext } from './types/Module'; + +// Parse context data from the root element +function initializeApp() { + const rootElement = document.querySelector('.contact-list-spa-react'); + + if (!rootElement) { + console.error('Root element .contact-list-spa-react not found'); + return; + } + + // Parse security context + const securityData = rootElement.getAttribute('data-security'); + const moduleData = rootElement.getAttribute('data-module'); + + if (!securityData || !moduleData) { + console.error('Missing data-security or data-module attributes'); + return; + } + + let security: SecurityContext; + let moduleContext: ModuleContext; + + try { + security = JSON.parse(securityData); + moduleContext = JSON.parse(moduleData); + } catch (err) { + console.error('Failed to parse context data:', err); + return; + } + + // Create React root and render + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + ); +} + +// Wait for DOM to be ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeApp); +} else { + initializeApp(); +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/pages/ContactForm.tsx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/pages/ContactForm.tsx new file mode 100644 index 00000000000..69f6aec9e3d --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/pages/ContactForm.tsx @@ -0,0 +1,263 @@ +import { useState, useEffect } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Contact } from '../types/Contact'; +import { ModuleContext } from '../types/Module'; +import { getContact, saveContact } from '../services/services'; +import { validateRequired, validateEmail, validatePhone, validateSocial } from '../utils/validation'; + +interface ContactFormProps { + moduleContext: ModuleContext; +} + +interface FormErrors { + FirstName: string; + LastName: string; + Email: string; + Phone: string; + Social: string; +} + +export default function ContactForm({ moduleContext }: ContactFormProps) { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const isEditMode = id !== undefined; + + const [contact, setContact] = useState({ + ContactId: -1, + FirstName: '', + LastName: '', + Email: '', + Phone: '', + Social: '', + CreatedByUserId: 0, + CreatedOnDate: new Date(), + LastModifiedByUserId: 0, + LastModifiedOnDate: new Date() + }); + + const [errors, setErrors] = useState({ + FirstName: '', + LastName: '', + Email: '', + Phone: '', + Social: '' + }); + + const [loading, setLoading] = useState(false); + const [touched, setTouched] = useState>({}); + + useEffect(() => { + if (isEditMode && id) { + loadContact(parseInt(id)); + } + }, [id]); + + const loadContact = async (contactId: number) => { + try { + setLoading(true); + const response = await getContact(moduleContext, contactId); + setContact(response); + } catch (err) { + console.error('Error loading contact:', err); + alert('Failed to load contact'); + navigate('/'); + } finally { + setLoading(false); + } + }; + + const validateField = (name: string, value: string): string => { + switch (name) { + case 'FirstName': + return validateRequired(value, 'First name').message; + case 'LastName': + return validateRequired(value, 'Last name').message; + case 'Email': + return validateEmail(value).message; + case 'Phone': + return validatePhone(value).message; + case 'Social': + return validateSocial(value).message; + default: + return ''; + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setContact({ ...contact, [name]: value }); + + // Validate on change if field has been touched + if (touched[name]) { + setErrors({ ...errors, [name]: validateField(name, value) }); + } + }; + + const handleBlur = (e: React.FocusEvent) => { + const { name, value } = e.target; + setTouched({ ...touched, [name]: true }); + setErrors({ ...errors, [name]: validateField(name, value) }); + }; + + const validateForm = (): boolean => { + const newErrors: FormErrors = { + FirstName: validateField('FirstName', contact.FirstName), + LastName: validateField('LastName', contact.LastName), + Email: validateField('Email', contact.Email), + Phone: validateField('Phone', contact.Phone), + Social: validateField('Social', contact.Social) + }; + + setErrors(newErrors); + setTouched({ + FirstName: true, + LastName: true, + Email: true, + Phone: true, + Social: true + }); + + return !Object.values(newErrors).some(error => error !== ''); + }; + + const handleSave = async () => { + if (!validateForm()) { + return; + } + + try { + setLoading(true); + const response = await saveContact(moduleContext, contact); + + setContact(response); + navigate('/'); + } catch (err) { + console.error('Error saving contact:', err); + alert('Failed to save contact'); + } finally { + setLoading(false); + } + }; + + const handleCancel = () => { + navigate('/'); + }; + + return ( +
    +
    +
    +

    + + {isEditMode ? 'Edit Contact' : 'Add Contact'} +

    +
    +
    +
    +
    + + + {errors.FirstName && touched.FirstName && ( + {errors.FirstName} + )} +
    +
    + + + {errors.LastName && touched.LastName && ( + {errors.LastName} + )} +
    +
    +
    + + + {errors.Email && touched.Email && ( + {errors.Email} + )} +
    +
    + + + {errors.Phone && touched.Phone && ( + {errors.Phone} + )} +
    +
    + + + {errors.Social && touched.Social && ( + {errors.Social} + )} +
    +
    + +
    +
    + ); +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/pages/ContactList.tsx b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/pages/ContactList.tsx new file mode 100644 index 00000000000..2b2fa96209c --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/pages/ContactList.tsx @@ -0,0 +1,138 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Contact } from '../types/Contact'; +import { SecurityContext } from '../types/Security'; +import { ModuleContext } from '../types/Module'; +import { getContacts, deleteContact } from '../services/services'; +import ContactCard from '../components/ContactCard'; +import Pagination from '../components/Pagination'; + +interface ContactListProps { + security: SecurityContext; + moduleContext: ModuleContext; +} + +export default function ContactList({ security, moduleContext }: ContactListProps) { + const navigate = useNavigate(); + const [contacts, setContacts] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [currentPage, setCurrentPage] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); + + const pageSize = 6; + + // Debounce search term with 0.5 second delay + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + // Reset to first page when search term changes + setCurrentPage(0); + }, 500); + + return () => clearTimeout(timer); + }, [searchTerm]); + + useEffect(() => { + loadContacts(); + }, [currentPage, debouncedSearchTerm]); + + const loadContacts = async () => { + try { + setLoading(true); + setError(null); + const response = await getContacts(moduleContext, currentPage, pageSize, debouncedSearchTerm); + + setContacts(response.Data); + setTotalCount(response.TotalCount); + } catch (err) { + console.error('Error loading contacts:', err); + setError('Failed to load contacts'); + } finally { + setLoading(false); + } + }; + + const handleEdit = (contact: Contact) => { + navigate(`/edit/${contact.ContactId}`); + }; + + const handleDelete = async (contact: Contact) => { + try { + const response = await deleteContact(moduleContext, contact); + + if (response.success) { + // Reload contacts after deletion + await loadContacts(); + } else { + alert('Failed to delete contact'); + } + } catch (err) { + console.error('Error deleting contact:', err); + alert('Failed to delete contact'); + } + }; + + const handleAddContact = () => { + navigate('/add'); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + if (loading && contacts.length === 0) { + return
    Loading...
    ; + } + + if (error) { + return
    Error: {error}
    ; + } + + return ( +
    +

    Contact List

    +
    + {contacts.map((contact) => ( + + ))} +
    + +
    +
    + setSearchTerm(e.target.value)} + className="search-input" + /> + +
    + +
    + + {security.CanEdit && ( + + )} +
    + ); +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/services/services.ts b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/services/services.ts new file mode 100644 index 00000000000..f9e3c66b752 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/services/services.ts @@ -0,0 +1,128 @@ +import { Contact } from '../types/Contact'; +import { ModuleContext } from '../types/Module'; +import { PagedList } from '../types/PagedList'; + +// DNN ServicesFramework interface +interface ServicesFramework { + getServiceRoot(moduleName: string): string; + getAntiForgeryValue(): string; +} + +declare global { + interface Window { + $: any; + } +} + +// Get DNN Services Framework +function getServicesFramework(moduleId: number): ServicesFramework { + if (window.$ && window.$.ServicesFramework) { + return window.$.ServicesFramework(moduleId); + } + throw new Error('DNN ServicesFramework not available'); +} + +// Build API headers with anti-forgery token +function getHeaders(moduleContext: ModuleContext): HeadersInit { + const sf = getServicesFramework(moduleContext.ModuleId); + return { + 'Content-Type': 'application/json', + 'ModuleId': moduleContext.ModuleId.toString(), + 'TabId': moduleContext.TabId.toString(), + 'RequestVerificationToken': sf.getAntiForgeryValue() + }; +} + +// Get base API URL +function getApiUrl(moduleContext: ModuleContext): string { + const sf = getServicesFramework(moduleContext.ModuleId); + // The API is from ContactList.Spa module + return sf.getServiceRoot('Dnn/ContactListSpaReact') + 'Contacts/'; +} + +export interface GetContactsResponse extends PagedList {} + +export interface DeleteContactResponse { + success: boolean; +} + +// Get paginated contacts +export async function getContacts( + moduleContext: ModuleContext, + pageIndex: number, + pageSize: number, + searchTerm: string = '' +): Promise { + const url = `${getApiUrl(moduleContext)}Page?searchTerm=${encodeURIComponent(searchTerm)}&pageSize=${pageSize}&pageIndex=${pageIndex}`; + + const response = await fetch(url, { + method: 'GET', + headers: getHeaders(moduleContext) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); +} + +// Get single contact +export async function getContact( + moduleContext: ModuleContext, + contactId: number +): Promise { + const url = `${getApiUrl(moduleContext)}Contact/${contactId}`; + + const response = await fetch(url, { + method: 'GET', + headers: getHeaders(moduleContext) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); +} + +// Save contact (create or update) +export async function saveContact( + moduleContext: ModuleContext, + contact: Contact +): Promise { + const url = `${getApiUrl(moduleContext)}Contact/${contact.ContactId}`; + + const response = await fetch(url, { + method: 'POST', + headers: getHeaders(moduleContext), + body: JSON.stringify(contact) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); +} + +// Delete contact +export async function deleteContact( + moduleContext: ModuleContext, + contact: Contact +): Promise { + const url = `${getApiUrl(moduleContext)}Delete/${contact.ContactId}`; + + const response = await fetch(url, { + method: 'POST', + headers: getHeaders(moduleContext), + body: JSON.stringify(contact) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Contact.ts b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Contact.ts new file mode 100644 index 00000000000..a0cf4b43e27 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Contact.ts @@ -0,0 +1,13 @@ +export interface Contact { + ContactId: number; + FirstName: string; + LastName: string; + Email: string; + Phone: string; + Social: string; + CreatedByUserId: number; + CreatedOnDate: Date; + LastModifiedByUserId: number; + LastModifiedOnDate: Date; +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Module.ts b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Module.ts new file mode 100644 index 00000000000..1b955064bca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Module.ts @@ -0,0 +1,7 @@ +export interface ModuleContext { + ModuleId: number; + TabId: number; + TabModuleId: number; + PortalId: number; +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/PagedList.ts b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/PagedList.ts new file mode 100644 index 00000000000..27f7c3cf4ba --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/PagedList.ts @@ -0,0 +1,9 @@ +export interface PagedList { + Data: T[]; + PageIndex: number; + PageSize: number; + TotalCount: number; + PageCount: number; + IsFirstPage: boolean; + IsLastPage: boolean; +} diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Security.ts b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Security.ts new file mode 100644 index 00000000000..aea0ce42718 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/types/Security.ts @@ -0,0 +1,7 @@ +export interface SecurityContext { + CanView: boolean; + CanEdit: boolean; + IsAdmin: boolean; + UserId: number; +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/utils/validation.ts b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/utils/validation.ts new file mode 100644 index 00000000000..6db1224f1ca --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/src/utils/validation.ts @@ -0,0 +1,56 @@ +export interface ValidationResult { + isValid: boolean; + message: string; +} + +export function validateRequired(value: string, fieldName: string): ValidationResult { + if (!value || value.trim() === '') { + return { isValid: false, message: `${fieldName} is required` }; + } + return { isValid: true, message: '' }; +} + +export function validateEmail(email: string): ValidationResult { + if (!email || email.trim() === '') { + return { isValid: false, message: 'Please enter a valid email address' }; + } + + // Email regex from the ContactList.Spa module + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!emailRegex.test(email)) { + return { isValid: false, message: 'Please enter a valid email address' }; + } + + return { isValid: true, message: '' }; +} + +export function validatePhone(phone: string): ValidationResult { + if (!phone || phone.trim() === '') { + return { isValid: false, message: 'Please enter a valid phone number (international formats accepted: +1 234 567 8900, 123-456-7890, etc.)' }; + } + + // Phone regex from the ContactList.Spa module + const phoneRegex = /^(\+?\d{1,3}[\s.-]?)?[\d\s().-]{6,}$/; + + if (!phoneRegex.test(phone)) { + return { isValid: false, message: 'Please enter a valid phone number (international formats accepted: +1 234 567 8900, 123-456-7890, etc.)' }; + } + + return { isValid: true, message: '' }; +} + +export function validateSocial(social: string): ValidationResult { + // Social is optional + if (!social || social.trim() === '') { + return { isValid: true, message: '' }; + } + + // Social must start with @ + if (!social.startsWith('@')) { + return { isValid: false, message: 'Social handle must start with @ symbol' }; + } + + return { isValid: true, message: '' }; +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/tsconfig.json b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/tsconfig.json new file mode 100644 index 00000000000..256e8d84860 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/tsconfig.node.json b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/tsconfig.node.json new file mode 100644 index 00000000000..41cdb7d5e94 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} + diff --git a/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/vite.config.ts b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/vite.config.ts new file mode 100644 index 00000000000..354a0d36b73 --- /dev/null +++ b/DNN Platform/Modules/Samples/Dnn.ContactList.SpaReact/vite.config.ts @@ -0,0 +1,60 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; +import { readFileSync } from 'fs'; + +// Read settings file +let settings: { WebsitePath?: string } = {}; +try { + const settingsPath = resolve(__dirname, '../../../../settings.local.json'); + const settingsContent = readFileSync(settingsPath, 'utf-8'); + settings = JSON.parse(settingsContent); +} catch (err) { + // settings.local.json is optional + console.warn('settings.local.json not found, using defaults'); +} + + +export default defineConfig(({ mode }) => { + // For development/watch mode, you can set a custom path to your local DNN installation + // For production builds, output to the scripts folder for packaging + const isDevelopment = mode === 'development'; + const outDir = isDevelopment + ? settings.WebsitePath + ? `${settings.WebsitePath}/DesktopModules/Dnn/ContactListSpaReact/scripts` + : 'scripts' + : 'scripts'; + + return { + plugins: [react()], + define: { + 'process.env': JSON.stringify({ + NODE_ENV: mode === 'development' ? 'development' : 'production' + }), + 'process.env.NODE_ENV': JSON.stringify(mode === 'development' ? 'development' : 'production'), + 'global': 'globalThis' + }, + build: { + outDir, + emptyOutDir: false, + sourcemap: isDevelopment, + minify: !isDevelopment, + lib: { + entry: resolve(__dirname, 'src/main.tsx'), + name: 'ContactList', + formats: ['iife'], + fileName: () => 'contact-list.js' + }, + rollupOptions: { + external: [], + output: { + inlineDynamicImports: true, + globals: {}, + intro: `var process = { env: { NODE_ENV: '${mode === 'development' ? 'development' : 'production'}' } };` + } + }, + watch: isDevelopment ? {} : null + } + }; +}); + diff --git a/DNN Platform/Modules/Samples/README.md b/DNN Platform/Modules/Samples/README.md new file mode 100644 index 00000000000..01ba1ee9253 --- /dev/null +++ b/DNN Platform/Modules/Samples/README.md @@ -0,0 +1,56 @@ +# DNN Sample Modules + +In this directory you'll find several sample modules. The purpose of these modules is to: + +1. Showcase how you can build modules for DNN +2. Validate any changes we make to the framework and the impact it would have on module developers + +These modules are **NOT MEANT TO BE USED IN PRODUCTION**. There will be no "upgrade path" from one version to the next! + +## Components + +### Dnn.ContactList.Api + +This is a library project that takes care of persisting data to SQL database. It is used in the other ContactList projects. +Note that when you build these projects in release mode a zip file is created for each project that includes this dll. + +### Dnn.ContactList.Mvc + +This is a DNN MVC module. The main templating launguage is Razor (cshtml). + +### Dnn.ContactList.Spa + +This is a DNN SPA module. The main templating language is DNN's token replace (plain text into HTML). This project shows +the many ways in which you can use this to create a module. + +### Dnn.ContactList.SpaReact + +This is a DNN SPA module with a React front-end. This module shows less features of SPA module development than the Spa module above. +Instead it shows how you could use this module pattern to jump to React as quickly as possible. + +## Building + +These components are built to installable zip files under /Artifacts/SampleModules in release mode. +In debug mode they build to either the /Website folder or the folder you've set up using DNN_Platform.local.build. + +Example of DNN_Platform.local.build: +``` xml + + + D:\path\to\my\dnn\www + True + + +``` + +Example of settings.local.json: +``` json +{ + ... + "CopySampleProjects": true, + ... +} +``` + +The ```CopySampleProjects``` key will determine if these projects will be built to the destination or ignored. +If you set both to true the sample projects will be included to the build to your dev folder under /Install/module. diff --git a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/DNNConnect.CKEditorProvider.csproj b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/DNNConnect.CKEditorProvider.csproj index 549bb814c6c..482b6f0288e 100644 --- a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/DNNConnect.CKEditorProvider.csproj +++ b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/DNNConnect.CKEditorProvider.csproj @@ -91,6 +91,9 @@ ..\..\..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.8.0.0\lib\net462\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + ..\..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + ..\..\..\..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll @@ -129,13 +132,31 @@ + + ..\..\..\..\packages\Microsoft.AspNet.WebPages.3.3.0\lib\net45\System.Web.Helpers.dll + ..\..\..\..\packages\Microsoft.AspNet.WebApi.Core.5.3.0\lib\net45\System.Web.Http.dll ..\..\..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.3.0\lib\net45\System.Web.Http.WebHost.dll + + ..\..\..\..\packages\Microsoft.AspNet.Mvc.5.3.0\lib\net45\System.Web.Mvc.dll + + + ..\..\..\..\packages\Microsoft.AspNet.Razor.3.3.0\lib\net45\System.Web.Razor.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 + @@ -219,11 +240,13 @@ + + @@ -445,6 +468,10 @@ {03e3afa5-ddc9-48fb-a839-ad4282ce237e} DotNetNuke.Web.Client + + {aa3ee19b-81a0-3766-e8f4-424c2425a8d3} + DotNetNuke.Web.MvcPipeline + {4912f062-f8a8-4f9d-8f8e-244ebee1acbd} DotNetNuke.WebUtility diff --git a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Utilities/SettingsLoader.cs b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Utilities/SettingsLoader.cs new file mode 100644 index 00000000000..342de1249c5 --- /dev/null +++ b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Utilities/SettingsLoader.cs @@ -0,0 +1,924 @@ +// 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 DNNConnect.CKEditorProvider.Utilities +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Configuration; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using System.Threading; + using System.Web; + using System.Web.Script.Serialization; + using System.Web.UI; + using System.Web.UI.WebControls; + using System.Xml.Serialization; + + using DNNConnect.CKEditorProvider.Constants; + using DNNConnect.CKEditorProvider.Extensions; + using DNNConnect.CKEditorProvider.Objects; + using DotNetNuke.Common; + using DotNetNuke.Common.Utilities; + using DotNetNuke.Entities.Host; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Entities.Users; + using DotNetNuke.Framework; + using DotNetNuke.Framework.Providers; + using DotNetNuke.Security; + using DotNetNuke.Security.Roles; + using DotNetNuke.Services.FileSystem; + + /// + /// Settings Loader class. + /// + public class SettingsLoader + { + /// + /// Loads the settings. + /// + /// The portal settings. + /// The parent module identifier. + /// The control identifier. + /// The configuration folder. + /// The EditorProviderSettings. + public static EditorProviderSettings LoadSettings(PortalSettings portalSettings, int parentModuleId, string controlId, string configFolder) + { + var currentEditorSettings = new EditorProviderSettings(); + + // Set File Browser Mode + var providerConfiguration = ProviderConfiguration.GetProviderConfiguration("htmlEditor"); + if (providerConfiguration != null && providerConfiguration.Providers.ContainsKey(providerConfiguration.DefaultProvider)) + { + var objProvider = (Provider)providerConfiguration.Providers[providerConfiguration.DefaultProvider]; + + if (objProvider != null && !string.IsNullOrEmpty(objProvider.Attributes["ck_browser"])) + { + switch (objProvider.Attributes["ck_browser"]) + { + case "ckfinder": + currentEditorSettings.BrowserMode = BrowserType.CKFinder; + break; + case "standard": + currentEditorSettings.BrowserMode = BrowserType.StandardBrowser; + break; + case "none": + currentEditorSettings.BrowserMode = BrowserType.None; + break; + } + } + } + + var settingsDictionary = EditorController.GetEditorHostSettings(); + var portalRoles = RoleController.Instance.GetRoles(portalSettings.PortalId); + + // Load Default Settings + currentEditorSettings = SettingsUtil.GetDefaultSettings( + portalSettings, + portalSettings.HomeDirectoryMapPath, + configFolder, + portalRoles); + + // Set Current Mode to Default + currentEditorSettings.SettingMode = SettingsMode.Default; + + var hostKey = SettingConstants.HostKey; + var portalKey = SettingConstants.PortalKey(portalSettings.PortalId); + var pageKey = $"DNNCKT#{portalSettings.ActiveTab.TabID}#"; + var moduleKey = $"DNNCKMI#{parentModuleId}#INS#{controlId}#"; + + // Load Host Settings ?! + if (SettingsUtil.CheckSettingsExistByKey(settingsDictionary, hostKey)) + { + var hostPortalRoles = RoleController.Instance.GetRoles(Host.HostPortalID); + currentEditorSettings = SettingsUtil.LoadEditorSettingsByKey( + portalSettings, + currentEditorSettings, + settingsDictionary, + hostKey, + hostPortalRoles); + + // Set Current Mode to Host + currentEditorSettings.SettingMode = SettingsMode.Host; + + // reset the roles to the correct portal + if (portalSettings.PortalId != Host.HostPortalID) + { + foreach (var toolbarRole in currentEditorSettings.ToolBarRoles) + { + var roleName = hostPortalRoles.FirstOrDefault(role => role.RoleID == toolbarRole.RoleId)?.RoleName ?? string.Empty; + var roleId = portalRoles.FirstOrDefault(role => role.RoleName.Equals(roleName))?.RoleID ?? Null.NullInteger; + toolbarRole.RoleId = roleId; + } + + foreach (var uploadRoles in currentEditorSettings.UploadSizeRoles) + { + var roleName = hostPortalRoles.FirstOrDefault(role => role.RoleID == uploadRoles.RoleId)?.RoleName ?? string.Empty; + var roleId = portalRoles.FirstOrDefault(role => role.RoleName.Equals(roleName))?.RoleID ?? Null.NullInteger; + uploadRoles.RoleId = roleId; + } + } + } + + // Load Portal Settings ?! + if (SettingsUtil.CheckSettingsExistByKey(settingsDictionary, portalKey)) + { + currentEditorSettings = SettingsUtil.LoadEditorSettingsByKey( + portalSettings, + currentEditorSettings, + settingsDictionary, + portalKey, + portalRoles); + + // Set Current Mode to Portal + currentEditorSettings.SettingMode = SettingsMode.Portal; + } + + // Load Page Settings ?! + if (SettingsUtil.CheckSettingsExistByKey(settingsDictionary, pageKey)) + { + currentEditorSettings = SettingsUtil.LoadEditorSettingsByKey( + portalSettings, currentEditorSettings, settingsDictionary, pageKey, portalRoles); + + // Set Current Mode to Page + currentEditorSettings.SettingMode = SettingsMode.Page; + } + + // Load Module Settings ?! + if (!SettingsUtil.CheckExistsModuleInstanceSettings(moduleKey, parentModuleId)) + { + return currentEditorSettings; + } + + currentEditorSettings = SettingsUtil.LoadModuleSettings( + portalSettings, currentEditorSettings, moduleKey, parentModuleId, portalRoles); + + // Set Current Mode to Module Instance + currentEditorSettings.SettingMode = SettingsMode.ModuleInstance; + + return currentEditorSettings; + } + + /// Load the Settings from the web.config file. + /// The provider type. + /// The settings collection. + public static NameValueCollection LoadConfigSettings(string providerType) + { + var settings = new NameValueCollection(); + + var providerConfiguration = ProviderConfiguration.GetProviderConfiguration(providerType); + if (providerConfiguration.Providers.ContainsKey(providerConfiguration.DefaultProvider)) + { + var objProvider = (Provider)providerConfiguration.Providers[providerConfiguration.DefaultProvider]; + + foreach (string key in objProvider.Attributes) + { + if (key.IndexOf("ck_", StringComparison.OrdinalIgnoreCase) == 0) + { + string sAdjustedKey = key.Substring(3, key.Length - 3); + + // Do not ToLower settingKey, because CKConfig is case-Sensitive, exp: image2_prefillDimension + ////.ToLower(); + + if (!string.IsNullOrEmpty(sAdjustedKey)) + { + settings[sAdjustedKey] = objProvider.Attributes[key]; + } + } + } + } + else + { + throw new ConfigurationErrorsException(string.Format( + "Configuration error: default provider {0} doesn't exist in {1} providers", + providerConfiguration.DefaultProvider, + providerType)); + } + + return settings; + } + + /// + /// Populates the settings. + /// + /// The settings. + /// The current editor settings. + /// The portal settings. + /// The module configuration. + /// The attributes. + /// The width. + /// The height. + /// The control identifier. + /// The parent module identifier. + /// The tool bar name override. + public static void PopulateSettings( + NameValueCollection settings, + EditorProviderSettings currentEditorSettings, + PortalSettings portalSettings, + ModuleInfo moduleConfiguration, + NameValueCollection attributes, + Unit width, + Unit height, + string controlId, + int parentModuleId, + string toolBarNameOverride) + { + // Override local settings with attributes + foreach (string key in attributes.Keys) + { + settings[key] = attributes[key]; + } + + // Inject all Editor Config + foreach ( + PropertyInfo info in + SettingsUtil.GetEditorConfigProperties()) + { + XmlAttributeAttribute xmlAttributeAttribute = null; + var settingValue = string.Empty; + + if (!info.Name.Equals("CodeMirror") && !info.Name.Equals("WordCount")) + { + if (info.GetValue(currentEditorSettings.Config, null) == null) + { + continue; + } + + var rawValue = info.GetValue(currentEditorSettings.Config, null); + + settingValue = info.PropertyType.Name.Equals("Double") + ? Convert.ToDouble(rawValue) + .ToString(CultureInfo.InvariantCulture) + : rawValue.ToString(); + + if (string.IsNullOrEmpty(settingValue)) + { + continue; + } + + xmlAttributeAttribute = info.GetCustomAttribute(true); + } + + if (info.PropertyType.Name == "Boolean") + { + settings[xmlAttributeAttribute.AttributeName] = settingValue.ToLower(); + } + else + { + switch (info.Name) + { + case "ToolbarLocation": + settings[xmlAttributeAttribute.AttributeName] = settingValue.ToLower(); + break; + case "EnterMode": + case "ShiftEnterMode": + switch (settingValue) + { + case "P": + settings[xmlAttributeAttribute.AttributeName] = "1"; + break; + case "BR": + settings[xmlAttributeAttribute.AttributeName] = "2"; + break; + case "DIV": + settings[xmlAttributeAttribute.AttributeName] = "3"; + break; + } + + break; + case "ContentsLangDirection": + { + switch (settingValue) + { + case "LeftToRight": + settings[xmlAttributeAttribute.AttributeName] = "ltr"; + break; + case "RightToLeft": + settings[xmlAttributeAttribute.AttributeName] = "rtl"; + break; + default: + settings[xmlAttributeAttribute.AttributeName] = string.Empty; + break; + } + } + + break; + case "CodeMirror": + { + var codeMirrorArray = new StringBuilder(); + + foreach (var codeMirrorInfo in + typeof(CodeMirror).GetProperties()) + { + var xmlAttribute = + codeMirrorInfo.GetCustomAttribute(true); + var rawSettingValue = codeMirrorInfo.GetValue( + currentEditorSettings.Config.CodeMirror, null); + + var codeMirrorSettingValue = rawSettingValue.ToString(); + + if (string.IsNullOrEmpty(codeMirrorSettingValue)) + { + continue; + } + + switch (codeMirrorInfo.PropertyType.Name) + { + case "String": + codeMirrorArray.AppendFormat("{0}: '{1}',", xmlAttribute.AttributeName, HttpUtility.JavaScriptStringEncode(codeMirrorSettingValue)); + break; + case "Boolean": + codeMirrorArray.AppendFormat("{0}: {1},", xmlAttribute.AttributeName, codeMirrorSettingValue.ToLower()); + break; + } + } + + var codemirrorSettings = codeMirrorArray.ToString(); + + settings["codemirror"] = + $"{{ {codemirrorSettings.Remove(codemirrorSettings.Length - 1, 1)} }}"; + } + + break; + case "WordCount": + { + var wordcountArray = new StringBuilder(); + + foreach (var wordCountInfo in typeof(WordCountConfig).GetProperties()) + { + var xmlAttribute = + wordCountInfo.GetCustomAttribute(true); + + var rawSettingValue = + wordCountInfo.GetValue(currentEditorSettings.Config.WordCount, null); + + var wordCountSettingValue = rawSettingValue.ToString(); + + if (string.IsNullOrEmpty(wordCountSettingValue)) + { + continue; + } + + switch (wordCountInfo.PropertyType.Name) + { + case "String": + wordcountArray.AppendFormat("{0}: '{1}',", xmlAttribute.AttributeName, HttpUtility.JavaScriptStringEncode(wordCountSettingValue)); + break; + case "Boolean": + wordcountArray.AppendFormat("{0}: {1},", xmlAttribute.AttributeName, wordCountSettingValue.ToLower()); + break; + } + } + + var wordcountSettings = wordcountArray.ToString(); + + settings["wordcount"] = + $"{{ {wordcountSettings.Remove(wordcountSettings.Length - 1, 1)} }}"; + } + + break; + default: + settings[xmlAttributeAttribute.AttributeName] = settingValue; + break; + } + } + } + + try + { + var currentCulture = Thread.CurrentThread.CurrentUICulture; + + settings["language"] = currentCulture.Name.ToLowerInvariant(); + + if (string.IsNullOrEmpty(currentEditorSettings.Config.Scayt_sLang)) + { + // 'en-us' is not a language code that is supported, the correct is 'en_US' + // https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-scayt_sLang + settings["scayt_sLang"] = currentCulture.Name.Replace("-", "_"); + } + } + catch (Exception) + { + settings["language"] = "en"; + } + + if (!string.IsNullOrEmpty(currentEditorSettings.Config.CustomConfig)) + { + settings["customConfig"] = FormatUrl(portalSettings, currentEditorSettings.Config.CustomConfig); + } + else + { + settings["customConfig"] = string.Empty; + } + + if (!string.IsNullOrEmpty(currentEditorSettings.Config.Skin)) + { + if (currentEditorSettings.Config.Skin.Equals("office2003") + || currentEditorSettings.Config.Skin.Equals("BootstrapCK-Skin") + || currentEditorSettings.Config.Skin.Equals("chris") + || currentEditorSettings.Config.Skin.Equals("v2")) + { + settings["skin"] = "moono"; + } + else + { + settings["skin"] = currentEditorSettings.Config.Skin; + } + } + + settings["linkDefaultProtocol"] = currentEditorSettings.DefaultLinkProtocol.ToSettingValue(); + + var cssFiles = new List(); + var skinSrc = GetSkinSourcePath(portalSettings); + var containerSrc = GetContainerSourcePath(portalSettings); + + cssFiles.Add("~/portals/_default/default.css"); + cssFiles.Add(skinSrc.Replace(skinSrc.Substring(skinSrc.LastIndexOf('/'), skinSrc.Length - skinSrc.Substring(0, skinSrc.LastIndexOf('/')).Length), "/skin.css")); + cssFiles.Add(containerSrc.Replace(containerSrc.Substring(containerSrc.LastIndexOf('/'), containerSrc.Length - containerSrc.Substring(0, containerSrc.LastIndexOf('/')).Length), "/container.css")); + if (moduleConfiguration != null && moduleConfiguration.ModuleID > -1) + { + cssFiles.Add("~/DesktopModules/" + moduleConfiguration.DesktopModule.FolderName + "/module.css"); + } + + cssFiles.Add("~" + portalSettings.HomeDirectory + "portal.css"); + cssFiles.Add("~/Providers/HtmlEditorProviders/DNNConnect.CKE/css/CkEditorContents.css"); + + var resolvedCssFiles = cssFiles.Where(cssFile => File.Exists(Globals.ApplicationMapPath + Globals.ResolveUrl(cssFile).Replace("/", "\\"))).Select(Globals.ResolveUrl).ToList(); + + if (!string.IsNullOrEmpty(currentEditorSettings.Config.ContentsCss)) + { + var customCss = Globals.ResolveUrl(FormatUrl(portalSettings, currentEditorSettings.Config.ContentsCss)); + resolvedCssFiles.Add(customCss); + } + + var serializer = new JavaScriptSerializer(); + settings["contentsCss"] = serializer.Serialize(resolvedCssFiles); + + if (!string.IsNullOrEmpty(currentEditorSettings.Config.Templates_Files)) + { + var templateUrl = FormatUrl(portalSettings, currentEditorSettings.Config.Templates_Files); + + var templateFile = templateUrl.EndsWith(".xml") ? $"xml:{templateUrl}" : templateUrl; + settings["templates_files"] = + $"[ '{HttpUtility.JavaScriptStringEncode(templateFile)}' ]"; + } + + if (!string.IsNullOrEmpty(toolBarNameOverride)) + { + settings["toolbar"] = toolBarNameOverride; + } + else + { + var toolbarName = SetUserToolbar(settings["configFolder"], portalSettings, currentEditorSettings); + + var listToolbarSets = ToolbarUtil.GetToolbars(portalSettings.HomeDirectoryMapPath, settings["configFolder"]); + + var toolbarSet = listToolbarSets.FirstOrDefault(toolbar => toolbar.Name.Equals(toolbarName)); + + var toolbarSetString = ToolbarUtil.ConvertToolbarSetToString(toolbarSet, true); + + settings["toolbar"] = $"[{toolbarSetString}]"; + } + + // Easy Image Upload + if (currentEditorSettings.ImageButtonMode == ImageButtonType.EasyImageButton) + { + // replace 'Image' Plugin with 'EasyImage' + settings["toolbar"] = settings["toolbar"].Replace("'Image'", "'EasyImageUpload'"); + + // add the plugin in extraPlugins + if (string.IsNullOrEmpty(settings["extraPlugins"]) || !settings["extraPlugins"].Split(',').Contains("easyimage")) + { + if (!string.IsNullOrEmpty(settings["extraPlugins"])) + { + settings["extraPlugins"] += ","; + } + + settings["extraPlugins"] += "easyimage"; + } + + // change the easyimage toolbar + settings["easyimage_toolbar"] = "['EasyImageAlt']"; + + // remove the image plugin in removePlugins + if (string.IsNullOrEmpty(settings["removePlugins"]) || !settings["removePlugins"].Split(',').Contains("image")) + { + if (!string.IsNullOrEmpty(settings["removePlugins"])) + { + settings["removePlugins"] += ","; + } + + settings["removePlugins"] += "image"; + } + + settings.Add("cloudServices_uploadUrl", Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=EasyImageUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId, + parentModuleId, + controlId, + currentEditorSettings.SettingMode, + CultureInfo.CurrentCulture.Name))); + } + else + { + // remove the easyimage plugin in removePlugins + if (string.IsNullOrEmpty(settings["removePlugins"]) || !settings["removePlugins"].Split(',').Contains("easyimage")) + { + if (!string.IsNullOrEmpty(settings["removePlugins"])) + { + settings["removePlugins"] += ","; + } + + settings["removePlugins"] += "easyimage"; + } + } + + // cloudservices variables need to be set regardless + var tokenUrl = ServicesFramework.GetServiceFrameworkRoot() + "API/CKEditorProvider/CloudServices/GetToken"; + settings.Add("cloudServices_tokenUrl", tokenUrl); + + // Editor Width + if (!string.IsNullOrEmpty(currentEditorSettings.Config.Width)) + { + settings["width"] = currentEditorSettings.Config.Width; + } + else + { + if (width.Value > 0) + { + settings["width"] = width.ToString(); + } + } + + // Editor Height + if (!string.IsNullOrEmpty(currentEditorSettings.Config.Height)) + { + settings["height"] = currentEditorSettings.Config.Height; + } + else + { + if (height.Value > 0) + { + settings["height"] = height.ToString(); + } + } + + if (!string.IsNullOrEmpty(settings["extraPlugins"]) + && settings["extraPlugins"].Contains("xmlstyles")) + { + settings["extraPlugins"] = settings["extraPlugins"].Replace(",xmlstyles", string.Empty); + } + + // fix oEmbed/oembed issue and other bad settings + if (!string.IsNullOrEmpty(settings["extraPlugins"]) + && settings["extraPlugins"].Contains("oEmbed")) + { + settings["extraPlugins"] = settings["extraPlugins"].Replace("oEmbed", "oembed"); + } + + if (settings["PasteFromWordCleanupFile"] != null + && settings["PasteFromWordCleanupFile"].Equals("default")) + { + settings["PasteFromWordCleanupFile"] = string.Empty; + } + + if (settings["menu_groups"] != null + && settings["menu_groups"].Equals("clipboard,table,anchor,link,image")) + { + settings["menu_groups"] = + "clipboard,tablecell,tablecellproperties,tablerow,tablecolumn,table,anchor,link,image,flash,checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea,div"; + } + + // Inject maxFileSize + settings["maxFileSize"] = Utility.GetMaxUploadSize().ToString(); + + HttpContext.Current.Session["CKDNNtabid"] = portalSettings.ActiveTab.TabID; + HttpContext.Current.Session["CKDNNporid"] = portalSettings.PortalId; + + // Add FileBrowser + switch (currentEditorSettings.BrowserMode) + { + case BrowserType.StandardBrowser: + { + settings["filebrowserBrowseUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Type=Link&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId, + parentModuleId, + controlId, + currentEditorSettings.SettingMode, + CultureInfo.CurrentCulture.Name)); + settings["filebrowserImageBrowseUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Type=Image&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId, + parentModuleId, + controlId, + currentEditorSettings.SettingMode, + CultureInfo.CurrentCulture.Name)); + settings["filebrowserFlashBrowseUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Type=Flash&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId, + parentModuleId, + controlId, + currentEditorSettings.SettingMode, + CultureInfo.CurrentCulture.Name)); + + if (currentEditorSettings.ImageButtonMode == ImageButtonType.StandardButton && Utility.CheckIfUserHasFolderWriteAccess(currentEditorSettings.UploadDirId, portalSettings)) + { + settings["filebrowserUploadUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=FileUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId, + parentModuleId, + controlId, + currentEditorSettings.SettingMode, + CultureInfo.CurrentCulture.Name)); + settings["filebrowserFlashUploadUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=FlashUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId, + parentModuleId, + controlId, + currentEditorSettings.SettingMode, + CultureInfo.CurrentCulture.Name)); + settings["filebrowserImageUploadUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=ImageUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId, + parentModuleId, + controlId, + currentEditorSettings.SettingMode, + CultureInfo.CurrentCulture.Name)); + } + + settings["filebrowserWindowWidth"] = "870"; + settings["filebrowserWindowHeight"] = "800"; + + // Set Browser Authorize + const bool isAuthorized = true; + + HttpContext.Current.Session["CKE_DNNIsAuthorized"] = isAuthorized; + + DataCache.SetCache("CKE_DNNIsAuthorized", isAuthorized); + } + + break; + case BrowserType.CKFinder: + { + settings["filebrowserBrowseUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/ckfinder.html?tabid={0}&PortalID={1}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId)); + settings["filebrowserImageBrowseUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/ckfinder.html?type=Images&tabid={0}&PortalID={1}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId)); + settings["filebrowserFlashBrowseUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/ckfinder.html?type=Flash&tabid={0}&PortalID={1}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId)); + + if (Utility.CheckIfUserHasFolderWriteAccess(currentEditorSettings.UploadDirId, portalSettings)) + { + settings["filebrowserUploadUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Files&tabid={0}&PortalID={1}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId)); + settings["filebrowserFlashUploadUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Flash&tabid={0}&PortalID={1}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId)); + settings["filebrowserImageUploadUrl"] = + Globals.ResolveUrl( + string.Format( + "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Images&tabid={0}&PortalID={1}", + portalSettings.ActiveTab.TabID, + portalSettings.PortalId)); + } + + HttpContext.Current.Session["CKDNNSubDirs"] = currentEditorSettings.SubDirs; + + HttpContext.Current.Session["CKDNNRootDirId"] = currentEditorSettings.BrowserRootDirId; + HttpContext.Current.Session["CKDNNRootDirForImgId"] = currentEditorSettings.BrowserRootDirId; + HttpContext.Current.Session["CKDNNUpDirId"] = currentEditorSettings.UploadDirId; + HttpContext.Current.Session["CKDNNUpDirForImgId"] = currentEditorSettings.UploadDirId; + + // Set Browser Authorize + const bool isAuthorized = true; + + HttpContext.Current.Session["CKE_DNNIsAuthorized"] = isAuthorized; + + DataCache.SetCache("CKE_DNNIsAuthorized", isAuthorized); + } + + break; + } + } + + /// + /// Gets the editor configuration script. + /// + /// The settings. + /// The editor variable name. + /// The editor configuration script. + public static string GetEditorConfigScript(NameValueCollection settings, string editorVar) + { + var editorConfigScript = new StringBuilder(); + editorConfigScript.AppendFormat("var editorConfig{0} = {{", editorVar); + + var keysCount = settings.Keys.Count; + var currentCount = 0; + + foreach (string key in settings.Keys) + { + var value = settings[key]; + + currentCount++; + + if (value.Equals("true", StringComparison.InvariantCultureIgnoreCase) + || value.Equals("false", StringComparison.InvariantCultureIgnoreCase) || value.StartsWith("[") + || value.StartsWith("{") || Utility.IsNumeric(value)) + { + if (value.Equals("True")) + { + value = "true"; + } + else if (value.Equals("False")) + { + value = "false"; + } + + editorConfigScript.AppendFormat("{0}:{1}", key, value); + + editorConfigScript.Append(currentCount == keysCount ? "};" : ","); + } + else + { + if (key == "browser") + { + continue; + } + + editorConfigScript.AppendFormat("{0}:'{1}'", key, HttpUtility.JavaScriptStringEncode(value)); + + editorConfigScript.Append(currentCount == keysCount ? "};" : ","); + } + } + + return editorConfigScript.ToString(); + } + + private static string FormatUrl(PortalSettings portalSettings, string inputUrl) + { + var formattedUrl = string.Empty; + + if (string.IsNullOrEmpty(inputUrl)) + { + return formattedUrl; + } + + if (inputUrl.StartsWith("http://") || inputUrl.StartsWith("https://") || inputUrl.StartsWith("//")) + { + formattedUrl = inputUrl; + } + else if (inputUrl.StartsWith("FileID=")) + { + var fileId = int.Parse(inputUrl.Substring(7)); + + var objFileInfo = FileManager.Instance.GetFile(fileId); + + formattedUrl = portalSettings.HomeDirectory + objFileInfo.Folder + objFileInfo.FileName; + } + else + { + formattedUrl = portalSettings.HomeDirectory + inputUrl; + } + + return formattedUrl; + } + + private static string GetSkinSourcePath(PortalSettings portalSettings) + { + var skinSource = portalSettings.ActiveTab.SkinSrc ?? portalSettings.DefaultPortalSkin; + skinSource = ResolveSourcePath(skinSource); + return skinSource; + } + + private static string GetContainerSourcePath(PortalSettings portalSettings) + { + var containerSource = portalSettings.ActiveTab.ContainerSrc ?? portalSettings.DefaultPortalContainer; + containerSource = ResolveSourcePath(containerSource); + return containerSource; + } + + private static string ResolveSourcePath(string source) + { + source = "~" + source; + return source; + } + + private static string SetUserToolbar(string alternateConfigSubFolder, PortalSettings portalSettings, EditorProviderSettings currentEditorSettings) + { + string toolbarName = CanUseFullToolbarAsDefault(portalSettings) ? "Full" : "Basic"; + + var listToolbarSets = ToolbarUtil.GetToolbars( + portalSettings.HomeDirectoryMapPath, alternateConfigSubFolder); + + var listUserToolbarSets = new List(); + + if (currentEditorSettings.ToolBarRoles.Count <= 0) + { + return toolbarName; + } + + foreach (var roleToolbar in currentEditorSettings.ToolBarRoles) + { + if (roleToolbar.RoleId.Equals(-1) && !HttpContext.Current.Request.IsAuthenticated) + { + return roleToolbar.Toolbar; + } + + if (roleToolbar.RoleId.Equals(-1)) + { + continue; + } + + // Role + var role = RoleController.Instance.GetRoleById(portalSettings.PortalId, roleToolbar.RoleId); + + if (role == null) + { + continue; + } + + if (!PortalSecurity.IsInRole(role.RoleName)) + { + continue; + } + + // Handle Different Roles + if (!listToolbarSets.Any(toolbarSel => toolbarSel.Name.Equals(roleToolbar.Toolbar))) + { + continue; + } + + var toolbar = listToolbarSets.Find(toolbarSel => toolbarSel.Name.Equals(roleToolbar.Toolbar)); + + listUserToolbarSets.Add(toolbar); + } + + if (listUserToolbarSets.Count <= 0) + { + return toolbarName; + } + + // Compare The User Toolbars if the User is more then One Role, and apply the Toolbar with the Highest Priority + int iHighestPrio = listUserToolbarSets.Max(toolb => toolb.Priority); + + return ToolbarUtil.FindHighestToolbar(listUserToolbarSets, iHighestPrio).Name; + } + + private static bool CanUseFullToolbarAsDefault(PortalSettings portalSettings) + { + if (!HttpContext.Current.Request.IsAuthenticated) + { + return false; + } + + var currentUser = UserController.Instance.GetCurrentUserInfo(); + return currentUser.IsSuperUser || PortalSecurity.IsInRole(portalSettings.AdministratorRoleName); + } + } +} diff --git a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Web/CKEditorHelper.cs b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Web/CKEditorHelper.cs new file mode 100644 index 00000000000..d8e7373a3cd --- /dev/null +++ b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Web/CKEditorHelper.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 DNNConnect.CKEditorProvider.Web +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.Linq.Expressions; + using System.Text; + using System.Web; + using System.Web.Mvc; + using System.Web.Mvc.Html; + using System.Web.UI.WebControls; + + using DNNConnect.CKEditorProvider.Utilities; + using DotNetNuke.Abstractions; + using DotNetNuke.Abstractions.ClientResources; + using DotNetNuke.Common; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; + using DotNetNuke.Framework.JavaScriptLibraries; + using DotNetNuke.Security; + using DotNetNuke.Services.ClientDependency; + using DotNetNuke.Services.Localization; + using DotNetNuke.Web.MvcPipeline.Controllers; + using DotNetNuke.Web.MvcPipeline.UI.Utilities; + using Microsoft.Extensions.DependencyInjection; + + public static class CKEditorHelper + { + public static IHtmlString CKEditorEditorFor(this HtmlHelper htmlHelper, Expression> expression, int moduleId) + { + var controller = htmlHelper.ViewContext.Controller as DnnPageController; + + if (controller == null) + { + throw new InvalidOperationException("The DnnHelper class can only be used from DnnPageController"); + } + + // HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes) + var id = htmlHelper.IdFor(expression); + + var attrs = new Dictionary(); + attrs.Add("id", id.ToString()); + attrs.Add("data-ckeditor", true); + + var portalSettings = PortalController.Instance.GetCurrentPortalSettings(); + if (!htmlHelper.ViewContext.HttpContext.Request.IsAjaxRequest()) + { + var cdf = controller.DependencyProvider.GetRequiredService(); + + cdf.RegisterStylesheet("~/Providers/HtmlEditorProviders/DNNConnect.CKE/css/CKEditorToolBars.css"); + cdf.RegisterStylesheet("~/Providers/HtmlEditorProviders/DNNConnect.CKE/css/CKEditorOverride.css"); + + // controller.RegisterStylesheet("~/Providers/HtmlEditorProviders/DNNConnect.CKE/js/ckeditor/4.18.0/editor.css"); + /* + const string CsName = "CKEdScript"; + const string CsFindName = "CKFindScript"; + */ + + JavaScript.RequestRegistration(CommonJs.jQuery); + + cdf.RegisterScript("~/Providers/HtmlEditorProviders/DNNConnect.CKE/js/ckeditor/4.18.0/ckeditor.js"); + cdf.RegisterScript("~/Providers/HtmlEditorProviders/DNNConnect.CKE/js/editorOverride.js"); + + LoadEditorSettings(portalSettings, portalSettings.ActiveTab.TabID, moduleId); + } + + var textArea = htmlHelper.TextAreaFor(expression, attrs); + var htmlBuilder = new StringBuilder(); + + // Wrap textarea in a div + htmlBuilder.Append("
    "); + htmlBuilder.Append(textArea.ToString()); + + // Add options link div if user is admin + if (PortalSecurity.IsInRoles(portalSettings.AdministratorRoleName)) + { + var navigationManager = controller.DependencyProvider.GetRequiredService(); + var editorId = id.ToString().Replace("-", string.Empty).Replace(".", string.Empty); + var editorUrl = navigationManager.NavigateURL( + "CKEditorOptions", + "ModuleId=" + moduleId, + "minc=" + id, + "PortalID=" + portalSettings.PortalId, + "langCode=" + CultureInfo.CurrentCulture.Name, + "popUp=true"); + + htmlBuilder.Append("
    "); + htmlBuilder.AppendFormat( + "{2}", + HttpUtility.HtmlAttributeEncode(HttpUtility.JavaScriptStringEncode(editorUrl, true)), + string.Format("{0}_ckoptions", editorId), + Localization.GetString("Options.Text", GetResxFileName())); + htmlBuilder.Append("
    "); + } + + htmlBuilder.Append("
    "); + + return new MvcHtmlString(htmlBuilder.ToString()); + } + + private static string GetResxFileName() + { + return + Globals.ResolveUrl( + string.Format("~/Providers/HtmlEditorProviders/DNNConnect.CKE/{0}/Options.aspx.resx", Localization.LocalResourceDirectory)); + } + + private static void LoadEditorSettings(PortalSettings portalSettings, int tabId, int moduleId) + { + const string ProviderType = "htmlEditor"; + + // Load config settings + var settings = SettingsLoader.LoadConfigSettings(ProviderType); + var configFolder = settings["configFolder"]; + + // Load editor settings + var currentEditorSettings = SettingsLoader.LoadSettings( + portalSettings, + moduleId, + moduleId.ToString(), + configFolder); + + // Get module configuration + var moduleConfiguration = ModuleController.Instance.GetModule(moduleId, tabId, false); + + // Populate settings + var emptyAttributes = new NameValueCollection(); + SettingsLoader.PopulateSettings( + settings, + currentEditorSettings, + portalSettings, + moduleConfiguration, + emptyAttributes, + Unit.Empty, + Unit.Empty, + moduleId.ToString(), + moduleId, + null); + + // Generate config script + var editorVar = $"editor{moduleId}"; + var configScript = SettingsLoader.GetEditorConfigScript(settings, editorVar); + MvcClientAPI.RegisterStartupScript("CKEditorConfig", configScript); + } + } +} diff --git a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Web/EditorControl.cs b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Web/EditorControl.cs index 3b10e1983a6..8e30fe7d970 100644 --- a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Web/EditorControl.cs +++ b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/Web/EditorControl.cs @@ -16,7 +16,7 @@ namespace DNNConnect.CKEditorProvider.Web using System.Text; using System.Threading; using System.Web; - using System.Web.Script.Serialization; + using System.Web.UI; using System.Web.UI.WebControls; using System.Xml.Serialization; @@ -60,7 +60,7 @@ public class EditorControl : WebControl, IPostBackDataHandler public EditorControl() { this.navigationManager = this.Context.GetScope().ServiceProvider.GetRequiredService(); - this.LoadConfigSettings(); + this.settings = SettingsLoader.LoadConfigSettings(ProviderType); this.Init += this.CKEditorInit; } @@ -77,522 +77,24 @@ public NameValueCollection Settings return this.settings; } - // Override local settings with attributes + // Convert AttributeCollection to NameValueCollection + var attributesCollection = new NameValueCollection(); foreach (string key in this.Attributes.Keys) { - this.settings[key] = this.Attributes[key]; - } - - // Inject all Editor Config - foreach ( - PropertyInfo info in - SettingsUtil.GetEditorConfigProperties()) - { - XmlAttributeAttribute xmlAttributeAttribute = null; - var settingValue = string.Empty; - - if (!info.Name.Equals("CodeMirror") && !info.Name.Equals("WordCount")) - { - if (info.GetValue(this.currentEditorSettings.Config, null) == null) - { - continue; - } - - var rawValue = info.GetValue(this.currentEditorSettings.Config, null); - - settingValue = info.PropertyType.Name.Equals("Double") - ? Convert.ToDouble(rawValue) - .ToString(CultureInfo.InvariantCulture) - : rawValue.ToString(); - - if (string.IsNullOrEmpty(settingValue)) - { - continue; - } - - xmlAttributeAttribute = info.GetCustomAttribute(true); - } - - if (info.PropertyType.Name == "Boolean") - { - this.settings[xmlAttributeAttribute.AttributeName] = settingValue.ToLower(); - } - else - { - switch (info.Name) - { - case "ToolbarLocation": - this.settings[xmlAttributeAttribute.AttributeName] = settingValue.ToLower(); - break; - case "EnterMode": - case "ShiftEnterMode": - switch (settingValue) - { - case "P": - this.settings[xmlAttributeAttribute.AttributeName] = "1"; - break; - case "BR": - this.settings[xmlAttributeAttribute.AttributeName] = "2"; - break; - case "DIV": - this.settings[xmlAttributeAttribute.AttributeName] = "3"; - break; - } - - break; - case "ContentsLangDirection": - { - switch (settingValue) - { - case "LeftToRight": - this.settings[xmlAttributeAttribute.AttributeName] = "ltr"; - break; - case "RightToLeft": - this.settings[xmlAttributeAttribute.AttributeName] = "rtl"; - break; - default: - this.settings[xmlAttributeAttribute.AttributeName] = string.Empty; - break; - } - } - - break; - case "CodeMirror": - { - var codeMirrorArray = new StringBuilder(); - - foreach (var codeMirrorInfo in - typeof(CodeMirror).GetProperties()) - { - var xmlAttribute = - codeMirrorInfo.GetCustomAttribute(true); - var rawSettingValue = codeMirrorInfo.GetValue( - this.currentEditorSettings.Config.CodeMirror, null); - - var codeMirrorSettingValue = rawSettingValue.ToString(); - - if (string.IsNullOrEmpty(codeMirrorSettingValue)) - { - continue; - } - - switch (codeMirrorInfo.PropertyType.Name) - { - case "String": - codeMirrorArray.AppendFormat("{0}: '{1}',", xmlAttribute.AttributeName, HttpUtility.JavaScriptStringEncode(codeMirrorSettingValue)); - break; - case "Boolean": - codeMirrorArray.AppendFormat("{0}: {1},", xmlAttribute.AttributeName, codeMirrorSettingValue.ToLower()); - break; - } - } - - var codemirrorSettings = codeMirrorArray.ToString(); - - this.settings["codemirror"] = - $"{{ {codemirrorSettings.Remove(codemirrorSettings.Length - 1, 1)} }}"; - } - - break; - case "WordCount": - { - var wordcountArray = new StringBuilder(); - - foreach (var wordCountInfo in typeof(WordCountConfig).GetProperties()) - { - var xmlAttribute = - wordCountInfo.GetCustomAttribute(true); - - var rawSettingValue = - wordCountInfo.GetValue(this.currentEditorSettings.Config.WordCount, null); - - var wordCountSettingValue = rawSettingValue.ToString(); - - if (string.IsNullOrEmpty(wordCountSettingValue)) - { - continue; - } - - switch (wordCountInfo.PropertyType.Name) - { - case "String": - wordcountArray.AppendFormat("{0}: '{1}',", xmlAttribute.AttributeName, HttpUtility.JavaScriptStringEncode(wordCountSettingValue)); - break; - case "Boolean": - wordcountArray.AppendFormat("{0}: {1},", xmlAttribute.AttributeName, wordCountSettingValue.ToLower()); - break; - } - } - - var wordcountSettings = wordcountArray.ToString(); - - this.settings["wordcount"] = - $"{{ {wordcountSettings.Remove(wordcountSettings.Length - 1, 1)} }}"; - } - - break; - default: - this.settings[xmlAttributeAttribute.AttributeName] = settingValue; - break; - } - } - } - - try - { - var currentCulture = Thread.CurrentThread.CurrentUICulture; - - this.settings["language"] = currentCulture.Name.ToLowerInvariant(); - - if (string.IsNullOrEmpty(this.currentEditorSettings.Config.Scayt_sLang)) - { - // 'en-us' is not a language code that is supported, the correct is 'en_US' - // https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-scayt_sLang - this.settings["scayt_sLang"] = currentCulture.Name.Replace("-", "_"); - } - } - catch (Exception) - { - this.settings["language"] = "en"; + attributesCollection[key] = this.Attributes[key]; } - if (!string.IsNullOrEmpty(this.currentEditorSettings.Config.CustomConfig)) - { - this.settings["customConfig"] = this.FormatUrl(this.currentEditorSettings.Config.CustomConfig); - } - else - { - this.settings["customConfig"] = string.Empty; - } - - if (!string.IsNullOrEmpty(this.currentEditorSettings.Config.Skin)) - { - if (this.currentEditorSettings.Config.Skin.Equals("office2003") - || this.currentEditorSettings.Config.Skin.Equals("BootstrapCK-Skin") - || this.currentEditorSettings.Config.Skin.Equals("chris") - || this.currentEditorSettings.Config.Skin.Equals("v2")) - { - this.settings["skin"] = "moono"; - } - else - { - this.settings["skin"] = this.currentEditorSettings.Config.Skin; - } - } - - this.settings["linkDefaultProtocol"] = this.currentEditorSettings.DefaultLinkProtocol.ToSettingValue(); - - var cssFiles = new List(); - var skinSrc = this.GetSkinSourcePath(); - var containerSrc = this.GetContainerSourcePath(); - - cssFiles.Add("~/portals/_default/default.css"); - cssFiles.Add(skinSrc.Replace(skinSrc.Substring(skinSrc.LastIndexOf('/'), skinSrc.Length - skinSrc.Substring(0, skinSrc.LastIndexOf('/')).Length), "/skin.css")); - cssFiles.Add(containerSrc.Replace(containerSrc.Substring(containerSrc.LastIndexOf('/'), containerSrc.Length - containerSrc.Substring(0, containerSrc.LastIndexOf('/')).Length), "/container.css")); - if (this.portalModule != null && this.portalModule.ModuleId > -1) - { - cssFiles.Add("~/DesktopModules/" + this.portalModule.ModuleConfiguration.DesktopModule.FolderName + "/module.css"); - } - - cssFiles.Add("~" + this.portalSettings.HomeDirectory + "portal.css"); - cssFiles.Add("~/Providers/HtmlEditorProviders/DNNConnect.CKE/css/CkEditorContents.css"); - - var resolvedCssFiles = cssFiles.Where(cssFile => File.Exists(this.MapPathSecure(cssFile))).Select(Globals.ResolveUrl).ToList(); - - if (!string.IsNullOrEmpty(this.currentEditorSettings.Config.ContentsCss)) - { - var customCss = Globals.ResolveUrl(this.FormatUrl(this.currentEditorSettings.Config.ContentsCss)); - resolvedCssFiles.Add(customCss); - } - - var serializer = new JavaScriptSerializer(); - this.settings["contentsCss"] = serializer.Serialize(resolvedCssFiles); - - if (!string.IsNullOrEmpty(this.currentEditorSettings.Config.Templates_Files)) - { - var templateUrl = this.FormatUrl(this.currentEditorSettings.Config.Templates_Files); - - var templateFile = templateUrl.EndsWith(".xml") ? $"xml:{templateUrl}" : templateUrl; - this.settings["templates_files"] = - $"[ '{HttpUtility.JavaScriptStringEncode(templateFile)}' ]"; - } - - if (!string.IsNullOrEmpty(this.toolBarNameOverride)) - { - this.settings["toolbar"] = this.toolBarNameOverride; - } - else - { - var toolbarName = this.SetUserToolbar(this.settings["configFolder"]); - - var listToolbarSets = ToolbarUtil.GetToolbars(this.portalSettings.HomeDirectoryMapPath, this.settings["configFolder"]); - - var toolbarSet = listToolbarSets.FirstOrDefault(toolbar => toolbar.Name.Equals(toolbarName)); - - var toolbarSetString = ToolbarUtil.ConvertToolbarSetToString(toolbarSet, true); - - this.settings["toolbar"] = $"[{toolbarSetString}]"; - } - - // Easy Image Upload - if (this.currentEditorSettings.ImageButtonMode == ImageButtonType.EasyImageButton) - { - // replace 'Image' Plugin with 'EasyImage' - this.settings["toolbar"] = this.settings["toolbar"].Replace("'Image'", "'EasyImageUpload'"); - - // add the plugin in extraPlugins - if (string.IsNullOrEmpty(this.settings["extraPlugins"]) || !this.settings["extraPlugins"].Split(',').Contains("easyimage")) - { - if (!string.IsNullOrEmpty(this.settings["extraPlugins"])) - { - this.settings["extraPlugins"] += ","; - } - - this.settings["extraPlugins"] += "easyimage"; - } - - // change the easyimage toolbar - this.settings["easyimage_toolbar"] = "['EasyImageAlt']"; - - // remove the image plugin in removePlugins - if (string.IsNullOrEmpty(this.settings["removePlugins"]) || !this.settings["removePlugins"].Split(',').Contains("image")) - { - if (!string.IsNullOrEmpty(this.settings["removePlugins"])) - { - this.settings["removePlugins"] += ","; - } - - this.settings["removePlugins"] += "image"; - } - - this.settings.Add("cloudServices_uploadUrl", Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=EasyImageUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId, - this.parentModulId, - this.ID, - this.currentEditorSettings.SettingMode, - CultureInfo.CurrentCulture.Name))); - } - else - { - // remove the easyimage plugin in removePlugins - if (string.IsNullOrEmpty(this.settings["removePlugins"]) || !this.settings["removePlugins"].Split(',').Contains("easyimage")) - { - if (!string.IsNullOrEmpty(this.settings["removePlugins"])) - { - this.settings["removePlugins"] += ","; - } - - this.settings["removePlugins"] += "easyimage"; - } - } - - // cloudservices variables need to be set regardless - var tokenUrl = ServicesFramework.GetServiceFrameworkRoot() + "API/CKEditorProvider/CloudServices/GetToken"; - this.settings.Add("cloudServices_tokenUrl", tokenUrl); - - // Editor Width - if (!string.IsNullOrEmpty(this.currentEditorSettings.Config.Width)) - { - this.settings["width"] = this.currentEditorSettings.Config.Width; - } - else - { - if (this.Width.Value > 0) - { - this.settings["width"] = this.Width.ToString(); - } - } - - // Editor Height - if (!string.IsNullOrEmpty(this.currentEditorSettings.Config.Height)) - { - this.settings["height"] = this.currentEditorSettings.Config.Height; - } - else - { - if (this.Height.Value > 0) - { - this.settings["height"] = this.Height.ToString(); - } - } - - if (!string.IsNullOrEmpty(this.settings["extraPlugins"]) - && this.settings["extraPlugins"].Contains("xmlstyles")) - { - this.settings["extraPlugins"] = this.settings["extraPlugins"].Replace(",xmlstyles", string.Empty); - } - - // fix oEmbed/oembed issue and other bad settings - if (!string.IsNullOrEmpty(this.settings["extraPlugins"]) - && this.settings["extraPlugins"].Contains("oEmbed")) - { - this.settings["extraPlugins"] = this.settings["extraPlugins"].Replace("oEmbed", "oembed"); - } - - if (this.settings["PasteFromWordCleanupFile"] != null - && this.settings["PasteFromWordCleanupFile"].Equals("default")) - { - this.settings["PasteFromWordCleanupFile"] = string.Empty; - } - - if (this.settings["menu_groups"] != null - && this.settings["menu_groups"].Equals("clipboard,table,anchor,link,image")) - { - this.settings["menu_groups"] = - "clipboard,tablecell,tablecellproperties,tablerow,tablecolumn,table,anchor,link,image,flash,checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea,div"; - } - - // Inject maxFileSize - this.settings["maxFileSize"] = Utility.GetMaxUploadSize().ToString(); - - HttpContext.Current.Session["CKDNNtabid"] = this.portalSettings.ActiveTab.TabID; - HttpContext.Current.Session["CKDNNporid"] = this.portalSettings.PortalId; - - // Add FileBrowser - switch (this.currentEditorSettings.BrowserMode) - { - case BrowserType.StandardBrowser: - { - this.settings["filebrowserBrowseUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Type=Link&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId, - this.parentModulId, - this.ID, - this.currentEditorSettings.SettingMode, - CultureInfo.CurrentCulture.Name)); - this.settings["filebrowserImageBrowseUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Type=Image&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId, - this.parentModulId, - this.ID, - this.currentEditorSettings.SettingMode, - CultureInfo.CurrentCulture.Name)); - this.settings["filebrowserFlashBrowseUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Type=Flash&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId, - this.parentModulId, - this.ID, - this.currentEditorSettings.SettingMode, - CultureInfo.CurrentCulture.Name)); - - if (this.currentEditorSettings.ImageButtonMode == ImageButtonType.StandardButton && Utility.CheckIfUserHasFolderWriteAccess(this.currentEditorSettings.UploadDirId, this.portalSettings)) - { - this.settings["filebrowserUploadUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=FileUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId, - this.parentModulId, - this.ID, - this.currentEditorSettings.SettingMode, - CultureInfo.CurrentCulture.Name)); - this.settings["filebrowserFlashUploadUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=FlashUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId, - this.parentModulId, - this.ID, - this.currentEditorSettings.SettingMode, - CultureInfo.CurrentCulture.Name)); - this.settings["filebrowserImageUploadUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/Browser/Browser.aspx?Command=ImageUpload&tabid={0}&PortalID={1}&mid={2}&ckid={3}&mode={4}&lang={5}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId, - this.parentModulId, - this.ID, - this.currentEditorSettings.SettingMode, - CultureInfo.CurrentCulture.Name)); - } - - this.settings["filebrowserWindowWidth"] = "870"; - this.settings["filebrowserWindowHeight"] = "800"; - - // Set Browser Authorize - const bool isAuthorized = true; - - HttpContext.Current.Session["CKE_DNNIsAuthorized"] = isAuthorized; - - DataCache.SetCache("CKE_DNNIsAuthorized", isAuthorized); - } - - break; - case BrowserType.CKFinder: - { - this.settings["filebrowserBrowseUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/ckfinder.html?tabid={0}&PortalID={1}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId)); - this.settings["filebrowserImageBrowseUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/ckfinder.html?type=Images&tabid={0}&PortalID={1}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId)); - this.settings["filebrowserFlashBrowseUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/ckfinder.html?type=Flash&tabid={0}&PortalID={1}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId)); - - if (Utility.CheckIfUserHasFolderWriteAccess(this.currentEditorSettings.UploadDirId, this.portalSettings)) - { - this.settings["filebrowserUploadUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Files&tabid={0}&PortalID={1}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId)); - this.settings["filebrowserFlashUploadUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Flash&tabid={0}&PortalID={1}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId)); - this.settings["filebrowserImageUploadUrl"] = - Globals.ResolveUrl( - string.Format( - "~/Providers/HtmlEditorProviders/DNNConnect.CKE/ckfinder/core/connector/aspx/connector.aspx?command=QuickUpload&type=Images&tabid={0}&PortalID={1}", - this.portalSettings.ActiveTab.TabID, - this.portalSettings.PortalId)); - } - - HttpContext.Current.Session["CKDNNSubDirs"] = this.currentEditorSettings.SubDirs; - - HttpContext.Current.Session["CKDNNRootDirId"] = this.currentEditorSettings.BrowserRootDirId; - HttpContext.Current.Session["CKDNNRootDirForImgId"] = this.currentEditorSettings.BrowserRootDirId; - HttpContext.Current.Session["CKDNNUpDirId"] = this.currentEditorSettings.UploadDirId; - HttpContext.Current.Session["CKDNNUpDirForImgId"] = this.currentEditorSettings.UploadDirId; - - // Set Browser Authorize - const bool isAuthorized = true; - - HttpContext.Current.Session["CKE_DNNIsAuthorized"] = isAuthorized; - - DataCache.SetCache("CKE_DNNIsAuthorized", isAuthorized); - } - - break; - } + SettingsLoader.PopulateSettings( + this.settings, + this.currentEditorSettings, + this.portalSettings, + this.portalModule?.ModuleConfiguration, + attributesCollection, + this.Width, + this.Height, + this.ID, + this.parentModulId, + this.toolBarNameOverride); this.isMerged = true; @@ -881,127 +383,15 @@ private void CKEditorInit(object sender, EventArgs e) this.parentModulId = this.portalModule.ModuleId; } - this.SetFileBrowserMode(); - this.LoadAllSettings(); + this.currentEditorSettings = SettingsLoader.LoadSettings( + this.portalSettings, + this.parentModulId, + this.ID, + this.settings["configFolder"]); this.RegisterCKEditorLibrary(); this.GenerateEditorLoadScript(); } - private void SetFileBrowserMode() - { - ProviderConfiguration providerConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType); - Provider objProvider = (Provider)providerConfiguration.Providers[providerConfiguration.DefaultProvider]; - - if (objProvider == null || string.IsNullOrEmpty(objProvider.Attributes["ck_browser"])) - { - return; - } - - switch (objProvider.Attributes["ck_browser"]) - { - case "ckfinder": - this.currentEditorSettings.BrowserMode = BrowserType.CKFinder; - break; - case "standard": - this.currentEditorSettings.BrowserMode = BrowserType.StandardBrowser; - break; - case "none": - this.currentEditorSettings.BrowserMode = BrowserType.None; - break; - } - } - - /// Load Portal/Page/Module Settings. - private void LoadAllSettings() - { - var settingsDictionary = EditorController.GetEditorHostSettings(); - var portalRoles = RoleController.Instance.GetRoles(this.portalSettings.PortalId); - - // Load Default Settings - this.currentEditorSettings = SettingsUtil.GetDefaultSettings( - this.portalSettings, - this.portalSettings.HomeDirectoryMapPath, - this.settings["configFolder"], - portalRoles); - - // Set Current Mode to Default - this.currentEditorSettings.SettingMode = SettingsMode.Default; - - var hostKey = SettingConstants.HostKey; - var portalKey = SettingConstants.PortalKey(this.portalSettings.PortalId); - var pageKey = $"DNNCKT#{this.portalSettings.ActiveTab.TabID}#"; - var moduleKey = $"DNNCKMI#{this.parentModulId}#INS#{this.ID}#"; - - // Load Host Settings ?! - if (SettingsUtil.CheckSettingsExistByKey(settingsDictionary, hostKey)) - { - var hostPortalRoles = RoleController.Instance.GetRoles(Host.HostPortalID); - this.currentEditorSettings = SettingsUtil.LoadEditorSettingsByKey( - this.portalSettings, - this.currentEditorSettings, - settingsDictionary, - hostKey, - hostPortalRoles); - - // Set Current Mode to Host - this.currentEditorSettings.SettingMode = SettingsMode.Host; - - // reset the roles to the correct portal - if (this.portalSettings.PortalId != Host.HostPortalID) - { - foreach (var toolbarRole in this.currentEditorSettings.ToolBarRoles) - { - var roleName = hostPortalRoles.FirstOrDefault(role => role.RoleID == toolbarRole.RoleId)?.RoleName ?? string.Empty; - var roleId = portalRoles.FirstOrDefault(role => role.RoleName.Equals(roleName))?.RoleID ?? Null.NullInteger; - toolbarRole.RoleId = roleId; - } - - foreach (var uploadRoles in this.currentEditorSettings.UploadSizeRoles) - { - var roleName = hostPortalRoles.FirstOrDefault(role => role.RoleID == uploadRoles.RoleId)?.RoleName ?? string.Empty; - var roleId = portalRoles.FirstOrDefault(role => role.RoleName.Equals(roleName))?.RoleID ?? Null.NullInteger; - uploadRoles.RoleId = roleId; - } - } - } - - // Load Portal Settings ?! - if (SettingsUtil.CheckSettingsExistByKey(settingsDictionary, portalKey)) - { - this.currentEditorSettings = SettingsUtil.LoadEditorSettingsByKey( - this.portalSettings, - this.currentEditorSettings, - settingsDictionary, - portalKey, - portalRoles); - - // Set Current Mode to Portal - this.currentEditorSettings.SettingMode = SettingsMode.Portal; - } - - // Load Page Settings ?! - if (SettingsUtil.CheckSettingsExistByKey(settingsDictionary, pageKey)) - { - this.currentEditorSettings = SettingsUtil.LoadEditorSettingsByKey( - this.portalSettings, this.currentEditorSettings, settingsDictionary, pageKey, portalRoles); - - // Set Current Mode to Page - this.currentEditorSettings.SettingMode = SettingsMode.Page; - } - - // Load Module Settings ?! - if (!SettingsUtil.CheckExistsModuleInstanceSettings(moduleKey, this.parentModulId)) - { - return; - } - - this.currentEditorSettings = SettingsUtil.LoadModuleSettings( - this.portalSettings, this.currentEditorSettings, moduleKey, this.parentModulId, portalRoles); - - // Set Current Mode to Module Instance - this.currentEditorSettings.SettingMode = SettingsMode.ModuleInstance; - } - /// Format the URL from FileID to File Path URL. /// The Input URL. /// The formatted URL. @@ -1034,41 +424,6 @@ private string FormatUrl(string inputUrl) return formattedUrl; } - /// Load the Settings from the web.config file. - private void LoadConfigSettings() - { - this.settings = new NameValueCollection(); - - var providerConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType); - if (providerConfiguration.Providers.ContainsKey(providerConfiguration.DefaultProvider)) - { - var objProvider = (Provider)providerConfiguration.Providers[providerConfiguration.DefaultProvider]; - - foreach (string key in objProvider.Attributes) - { - if (key.IndexOf("ck_", StringComparison.OrdinalIgnoreCase) == 0) - { - string sAdjustedKey = key.Substring(3, key.Length - 3); - - // Do not ToLower settingKey, because CKConfig is case-Sensitive, exp: image2_prefillDimension - ////.ToLower(); - - if (!string.IsNullOrEmpty(sAdjustedKey)) - { - this.settings[sAdjustedKey] = objProvider.Attributes[key]; - } - } - } - } - else - { - throw new ConfigurationErrorsException(string.Format( - "Configuration error: default provider {0} doesn't exist in {1} providers", - providerConfiguration.DefaultProvider, - ProviderType)); - } - } - private void RegisterStartupScript(string key, string script, bool addScriptTags) { ScriptManager.RegisterStartupScript(this, this.GetType(), key, script, addScriptTags); @@ -1084,67 +439,6 @@ private void RegisterOnSubmitStatement(Type type, string key, string script) ScriptManager.RegisterOnSubmitStatement(this, type, key, script); } - private string SetUserToolbar(string alternateConfigSubFolder) - { - string toolbarName = this.CanUseFullToolbarAsDefault() ? "Full" : "Basic"; - - var listToolbarSets = ToolbarUtil.GetToolbars( - this.portalSettings.HomeDirectoryMapPath, alternateConfigSubFolder); - - var listUserToolbarSets = new List(); - - if (this.currentEditorSettings.ToolBarRoles.Count <= 0) - { - return toolbarName; - } - - foreach (var roleToolbar in this.currentEditorSettings.ToolBarRoles) - { - if (roleToolbar.RoleId.Equals(-1) && !HttpContext.Current.Request.IsAuthenticated) - { - return roleToolbar.Toolbar; - } - - if (roleToolbar.RoleId.Equals(-1)) - { - continue; - } - - // Role - var role = RoleController.Instance.GetRoleById(this.portalSettings.PortalId, roleToolbar.RoleId); - - if (role == null) - { - continue; - } - - if (!PortalSecurity.IsInRole(role.RoleName)) - { - continue; - } - - // Handle Different Roles - if (!listToolbarSets.Any(toolbarSel => toolbarSel.Name.Equals(roleToolbar.Toolbar))) - { - continue; - } - - var toolbar = listToolbarSets.Find(toolbarSel => toolbarSel.Name.Equals(roleToolbar.Toolbar)); - - listUserToolbarSets.Add(toolbar); - } - - if (listUserToolbarSets.Count <= 0) - { - return toolbarName; - } - - // Compare The User Toolbars if the User is more then One Role, and apply the Toolbar with the Highest Priority - int iHighestPrio = listUserToolbarSets.Max(toolb => toolb.Priority); - - return ToolbarUtil.FindHighestToolbar(listUserToolbarSets, iHighestPrio).Name; - } - private bool CanUseFullToolbarAsDefault() { if (!HttpContext.Current.Request.IsAuthenticated) @@ -1230,49 +524,7 @@ private void GenerateEditorLoadScript() HttpUtility.JavaScriptStringEncode(editorFixedId)); // Render EditorConfig - var editorConfigScript = new StringBuilder(); - editorConfigScript.AppendFormat("var editorConfig{0} = {{", editorVar); - - var keysCount = this.Settings.Keys.Count; - var currentCount = 0; - - // Write options - foreach (string key in this.Settings.Keys) - { - var value = this.Settings[key]; - - currentCount++; - - // Is boolean state or string - if (value.Equals("true", StringComparison.InvariantCultureIgnoreCase) - || value.Equals("false", StringComparison.InvariantCultureIgnoreCase) || value.StartsWith("[") - || value.StartsWith("{") || Utility.IsNumeric(value)) - { - if (value.Equals("True")) - { - value = "true"; - } - else if (value.Equals("False")) - { - value = "false"; - } - - editorConfigScript.AppendFormat("{0}:{1}", key, value); - - editorConfigScript.Append(currentCount == keysCount ? "};" : ","); - } - else - { - if (key == "browser") - { - continue; - } - - editorConfigScript.AppendFormat("{0}:'{1}'", key, HttpUtility.JavaScriptStringEncode(value)); - - editorConfigScript.Append(currentCount == keysCount ? "};" : ","); - } - } + string editorConfigScript = SettingsLoader.GetEditorConfigScript(this.Settings, editorVar); editorScript.AppendFormat( "if (CKEDITOR.instances.{0}){{return;}}", @@ -1310,7 +562,7 @@ private void GenerateEditorLoadScript() // End of LoadScript editorScript.Append("}"); - this.RegisterScript($@"{editorFixedId}_CKE_Config", editorConfigScript.ToString(), true); + this.RegisterScript($@"{editorFixedId}_CKE_Config", editorConfigScript, true); this.RegisterStartupScript($@"{editorFixedId}_CKE_Startup", editorScript.ToString(), true); } } diff --git a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/packages.config b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/packages.config index 86683e94dcf..d76195a9a9f 100644 --- a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/packages.config +++ b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/packages.config @@ -1,11 +1,15 @@  + + + + diff --git a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/web.config b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/web.config index ce1ea19b280..daa0dab8b1c 100644 --- a/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/web.config +++ b/DNN Platform/Providers/HtmlEditorProviders/DNNConnect.CKE/web.config @@ -69,6 +69,18 @@ + + + + + + + + + + + + diff --git a/DNN Platform/Skins/Aperture/Views/default.cshtml b/DNN Platform/Skins/Aperture/Views/default.cshtml new file mode 100644 index 00000000000..4804422ddf9 --- /dev/null +++ b/DNN Platform/Skins/Aperture/Views/default.cshtml @@ -0,0 +1,29 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins +@using DotNetNuke.Web.DDRMenu +@model PageModel + +@{ + +} + + +@section head { + @* CSS & JS includes *@ + @Html.SkinPartial("partials/_includes") +} + +
    + + @Html.SkinPartial("partials/_header") + + +
    + @Html.Pane(id: "BannerPane") + @Html.Pane(id: "ContentPane", cssClass: "aperture-content-pane") + @Html.Pane(id: "FluidPane") +
    + + + @Html.SkinPartial("partials/_footer") +
    \ No newline at end of file diff --git a/DNN Platform/Skins/Aperture/Views/partials/_footer.cshtml b/DNN Platform/Skins/Aperture/Views/partials/_footer.cshtml new file mode 100644 index 00000000000..9c8014403dd --- /dev/null +++ b/DNN Platform/Skins/Aperture/Views/partials/_footer.cshtml @@ -0,0 +1,26 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins +@using DotNetNuke.Web.NewDDRMenu + +@model PageModel + +
    +
    + + @Html.Pane(id: "FooterPane", cssClass: "footer-right") +
    +
    + diff --git a/DNN Platform/Skins/Aperture/Views/partials/_header.cshtml b/DNN Platform/Skins/Aperture/Views/partials/_header.cshtml new file mode 100644 index 00000000000..efa2eb74d58 --- /dev/null +++ b/DNN Platform/Skins/Aperture/Views/partials/_header.cshtml @@ -0,0 +1,28 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins +@using DotNetNuke.Web.NewDDRMenu @* this namespace will change in the future *@ + +@model PageModel + +
    +
    +
    + @Html.Login() + @Html.User() +
    +
    +
    +
    + @Html.Logo(injectSvg: true) + @Html.DDRMenu(clientID: "menu_desktop", + cssClass: "aperture-d-none aperture-d-md-block", + menuStyle: "menus/desktop", + nodeSelector: "*,0,2") + @Html.DDRMenu(clientID: "menu_mobile", + cssClass: "aperture-d-flex aperture-d-md-none", + menuStyle: "menus/mobile", + nodeSelector: "*,0,2") + +
    +
    +
    diff --git a/DNN Platform/Skins/Aperture/Views/partials/_includes-popUpSkin.cshtml b/DNN Platform/Skins/Aperture/Views/partials/_includes-popUpSkin.cshtml new file mode 100644 index 00000000000..3c6855ef9d2 --- /dev/null +++ b/DNN Platform/Skins/Aperture/Views/partials/_includes-popUpSkin.cshtml @@ -0,0 +1,5 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins + +@model PageModel +@Html.Meta(name: "viewport", content: "width=device-width, initial-scale=1.0") diff --git a/DNN Platform/Skins/Aperture/Views/partials/_includes.cshtml b/DNN Platform/Skins/Aperture/Views/partials/_includes.cshtml new file mode 100644 index 00000000000..7b23b9c1e92 --- /dev/null +++ b/DNN Platform/Skins/Aperture/Views/partials/_includes.cshtml @@ -0,0 +1,43 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins + +@model PageModel + +@{ + @Html.Meta(name: "viewport", content: "width=device-width, initial-scale=1.0") + + @* Include skin CSS and JS *@ + @Html.DnnCssInclude(filePath: "css/skin.min.css", pathNameAlias: "SkinPath", priority: 110) + @Html.DnnJsInclude(filePath: "js/skin.min.js", pathNameAlias: "SkinPath", priority: 110, defer: true) + + @* Preload fonts *@ + var fonts = new string[] + { + "fonts/Ubuntu-Bold", + "fonts/Ubuntu-BoldItalic", + "fonts/Ubuntu-Italic", + "fonts/Ubuntu-Light", + "fonts/Ubuntu-LightItalic", + "fonts/Ubuntu-Medium", + "fonts/Ubuntu-MediumItalic", + "fonts/Ubuntu-Regular" + }; + + var types = new Dictionary + { + { "woff2", "font/woff2" }, + { "woff", "font/woff" } + }; + + foreach (var type in types) + { + foreach (var font in fonts) + { + + } + } +} \ No newline at end of file diff --git a/DNN Platform/Skins/Aperture/Views/popUpSkin.cshtml b/DNN Platform/Skins/Aperture/Views/popUpSkin.cshtml new file mode 100644 index 00000000000..87a6a755792 --- /dev/null +++ b/DNN Platform/Skins/Aperture/Views/popUpSkin.cshtml @@ -0,0 +1,12 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins + +@model PageModel + +@section head { + @* CSS & JS includes *@ + @Html.SkinPartial("partials/_includes-popUpSkin") +} + + +@Html.Pane(id: "ContentPane", cssClass: "aperture-content-pane") \ No newline at end of file diff --git a/DNN Platform/Skins/Aperture/Views/web.config b/DNN Platform/Skins/Aperture/Views/web.config new file mode 100644 index 00000000000..fb2f7418658 --- /dev/null +++ b/DNN Platform/Skins/Aperture/Views/web.config @@ -0,0 +1,39 @@ + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Skins/Aperture/build.ts b/DNN Platform/Skins/Aperture/build.ts index 84c31b7c47a..c71745d7a81 100644 --- a/DNN Platform/Skins/Aperture/build.ts +++ b/DNN Platform/Skins/Aperture/build.ts @@ -54,14 +54,17 @@ const transpiledFiles: TranspiledFileConfig[] = [ ]; const copyFiles: StaticFileConfig[] = [ - { src: "containers/*", dest: containersDist }, - { src: "menus/desktop/*", dest: skinDist + "/menus/desktop" }, - { src: "menus/footer/*", dest: skinDist + "/menus/footer" }, - { src: "menus/mobile/*", dest: skinDist + "/menus/mobile" }, - { src: "partials/*", dest: skinDist + "/partials" }, - { src: "src/fonts/*", dest: skinDist + "/fonts" }, - { src: "src/images/*", dest: skinDist + "/images" }, - { src: "*.{ascx,png,dnn,xml,txt}", dest: skinDist }, + { src: "containers/*.ascx", dest: containersDist }, + { src: "containers/Views/*", dest: containersDist + "/Views" }, + { src: "menus/desktop/*", dest: skinDist + "/menus/desktop" }, + { src: "menus/footer/*", dest: skinDist + "/menus/footer" }, + { src: "menus/mobile/*", dest: skinDist + "/menus/mobile" }, + { src: "partials/*", dest: skinDist + "/partials" }, + { src: "src/fonts/*", dest: skinDist + "/fonts" }, + { src: "src/images/*", dest: skinDist + "/images" }, + { src: "Views/*.{cshtml,config}", dest: skinDist + "/Views" }, + { src: "Views/partials/*.cshtml", dest: skinDist + "/Views/partials" }, + { src: "*.{ascx,png,dnn,xml,txt}", dest: skinDist }, ]; /** Normalizes a path (windows vs linux, etc.) */ @@ -73,7 +76,7 @@ function normalizePath(filePath: string): string { function copyFilesPreservingStructure(copyConfig: StaticFileConfig[]): void { copyConfig.forEach(entry => { console.log(`Copying files from ${entry.src} to ${entry.dest}...`); - + globSync(entry.src).forEach(file => { const fileName = path.basename(file); const destFile = path.join(entry.dest, fileName); @@ -140,7 +143,7 @@ async function buildScss(input: string, output: string): Promise { console.error(`Error compiling SCSS for ${input}:`, error); } } - + /** Bundle TypeScript/JavaScript with esbuild */ async function buildJs(input: string, output: string): Promise { try { @@ -184,6 +187,7 @@ function watchFiles(): void { "./containers", "./menus", "./partials", + "./Views", ], { ignored: /(^|[\/\\])\../, // Ignore dotfiles @@ -281,7 +285,7 @@ async function packageFiles(): Promise { ensureEmptyDirectory(artifactsDir); const stagingDir = `${artifactsDir}/staging`; ensureEmptyDirectory(stagingDir); - + // Skin resources console.log("Packaging skin resources..."); var skinResources = await glob( @@ -291,6 +295,7 @@ async function packageFiles(): Promise { `${skinDist}/js/**/*`, `${skinDist}/menus/**/*`, `${skinDist}/patials/**/*`, + `${skinDist}/Views/**/*`, `${skinDist}/**/*.ascx`, `${skinDist}/**/*.xml`, `${skinDist}/**/*.png`, diff --git a/DNN Platform/Skins/Aperture/containers/Views/None.cshtml b/DNN Platform/Skins/Aperture/containers/Views/None.cshtml new file mode 100644 index 00000000000..4b7770d57e8 --- /dev/null +++ b/DNN Platform/Skins/Aperture/containers/Views/None.cshtml @@ -0,0 +1,4 @@ +@using DotNetNuke.Web.MvcPipeline.Containers +@model DotNetNuke.Web.MvcPipeline.Models.ContainerModel + +
    @Html.Content()
    \ No newline at end of file diff --git a/DNN Platform/Skins/Aperture/containers/Views/Title.cshtml b/DNN Platform/Skins/Aperture/containers/Views/Title.cshtml new file mode 100644 index 00000000000..9517b2e2ebf --- /dev/null +++ b/DNN Platform/Skins/Aperture/containers/Views/Title.cshtml @@ -0,0 +1,7 @@ +@using DotNetNuke.Web.MvcPipeline.Containers +@model DotNetNuke.Web.MvcPipeline.Models.ContainerModel + +
    +
    @Html.Title()
    +
    @Html.Content()
    +
    \ No newline at end of file diff --git a/DNN Platform/Skins/Aperture/containers/Views/web.config b/DNN Platform/Skins/Aperture/containers/Views/web.config new file mode 100644 index 00000000000..a7751b2f5f9 --- /dev/null +++ b/DNN Platform/Skins/Aperture/containers/Views/web.config @@ -0,0 +1,33 @@ + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Skins/Aperture/package.json b/DNN Platform/Skins/Aperture/package.json index e4b34e1c8e0..a171aaf5d10 100644 --- a/DNN Platform/Skins/Aperture/package.json +++ b/DNN Platform/Skins/Aperture/package.json @@ -20,7 +20,7 @@ "browser-sync": "^3.0.4", "chokidar": "^4.0.3", "cssnano": "^7.1.1", - "glob": "^11.0.3", + "glob": "^12.0.0", "postcss": "^8.5.6", "postcss-banner": "^4.0.1", "postcss-cli": "^11.0.1", diff --git a/DNN Platform/Tests/App.config b/DNN Platform/Tests/App.config index 942447331ef..d8ad92333d1 100644 --- a/DNN Platform/Tests/App.config +++ b/DNN Platform/Tests/App.config @@ -114,7 +114,7 @@ - + + \ No newline at end of file diff --git a/DNN Platform/Website/Portals/Web.config b/DNN Platform/Website/Portals/Web.config deleted file mode 100644 index 03c0dd1abe1..00000000000 --- a/DNN Platform/Website/Portals/Web.config +++ /dev/null @@ -1,20 +0,0 @@ - - - - -
    -
    - - - - - - - - - - - - - - diff --git a/DNN Platform/Website/Portals/_default/Containers/_default/Views/No Container.cshtml b/DNN Platform/Website/Portals/_default/Containers/_default/Views/No Container.cshtml new file mode 100644 index 00000000000..7545cf17462 --- /dev/null +++ b/DNN Platform/Website/Portals/_default/Containers/_default/Views/No Container.cshtml @@ -0,0 +1,4 @@ +@using DotNetNuke.Web.MvcPipeline.Containers +@model DotNetNuke.Web.MvcPipeline.Models.ContainerModel + +
    @Html.Content()
    diff --git a/DNN Platform/Website/Portals/_default/Containers/_default/Views/popUpContainer.cshtml b/DNN Platform/Website/Portals/_default/Containers/_default/Views/popUpContainer.cshtml new file mode 100644 index 00000000000..b6a234ec5fa --- /dev/null +++ b/DNN Platform/Website/Portals/_default/Containers/_default/Views/popUpContainer.cshtml @@ -0,0 +1,4 @@ +@using DotNetNuke.Web.MvcPipeline.Containers +@model DotNetNuke.Web.MvcPipeline.Models.ContainerModel + +
    @Html.Content()
    \ No newline at end of file diff --git a/DNN Platform/Website/Portals/_default/Containers/_default/Views/web.config b/DNN Platform/Website/Portals/_default/Containers/_default/Views/web.config new file mode 100644 index 00000000000..a7751b2f5f9 --- /dev/null +++ b/DNN Platform/Website/Portals/_default/Containers/_default/Views/web.config @@ -0,0 +1,33 @@ + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Website/Portals/_default/Skins/_default/Views/No Skin.cshtml b/DNN Platform/Website/Portals/_default/Skins/_default/Views/No Skin.cshtml new file mode 100644 index 00000000000..ca61a7af18e --- /dev/null +++ b/DNN Platform/Website/Portals/_default/Skins/_default/Views/No Skin.cshtml @@ -0,0 +1,8 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins + +@model PageModel + +@Html.DnnCssIncludeDefaultStylesheet() + +@Html.Pane(id: "ContentPane", cssClass: "aperture-content-pane") diff --git a/DNN Platform/Website/Portals/_default/Skins/_default/Views/popUpSkin.cshtml b/DNN Platform/Website/Portals/_default/Skins/_default/Views/popUpSkin.cshtml new file mode 100644 index 00000000000..6e263e3c691 --- /dev/null +++ b/DNN Platform/Website/Portals/_default/Skins/_default/Views/popUpSkin.cshtml @@ -0,0 +1,11 @@ +@using DotNetNuke.Web.MvcPipeline.Models +@using DotNetNuke.Web.MvcPipeline.Skins + +@model PageModel + +@Html.DnnCssIncludeDefaultStylesheet() + + +@Html.Pane(id: "ContentPane", cssClass: "aperture-content-pane") diff --git a/DNN Platform/Website/Portals/_default/Skins/_default/Views/web.config b/DNN Platform/Website/Portals/_default/Skins/_default/Views/web.config new file mode 100644 index 00000000000..a7751b2f5f9 --- /dev/null +++ b/DNN Platform/Website/Portals/_default/Skins/_default/Views/web.config @@ -0,0 +1,33 @@ + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.01.00.SqlDataProvider b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.01.00.SqlDataProvider index 1ef8874ae61..9fa75337906 100644 --- a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.01.00.SqlDataProvider +++ b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.01.00.SqlDataProvider @@ -280,4 +280,4 @@ AS THROW END CATCH END -GO \ No newline at end of file +GO diff --git a/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.99.00.SqlDataProvider b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.99.00.SqlDataProvider new file mode 100644 index 00000000000..79b75b8449c --- /dev/null +++ b/DNN Platform/Website/Providers/DataProviders/SqlDataProvider/10.99.00.SqlDataProvider @@ -0,0 +1,129 @@ +/************************************************************/ +/* MVC Pipeline */ +/************************************************************/ + +/* Add MvcControlClass Column to ModuleControls Table */ +/*****************************************************/ + +IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.Columns WHERE TABLE_NAME='{objectQualifier}ModuleControls' AND COLUMN_NAME='MvcControlClass') + BEGIN + -- Add new Column + ALTER TABLE {databaseOwner}{objectQualifier}ModuleControls + ADD MvcControlClass [nvarchar] (256) NULL + END +GO + +/* Update AddModuleControl */ +/***************************/ + +IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}AddModuleControl]') AND type in (N'P', N'PC')) + DROP PROCEDURE {databaseOwner}[{objectQualifier}AddModuleControl] +GO + +CREATE PROCEDURE {databaseOwner}[{objectQualifier}AddModuleControl] + + @ModuleDefID int, + @ControlKey nvarchar(50), + @ControlTitle nvarchar(50), + @ControlSrc nvarchar(256), + @MvcControlClass nvarchar(256) = null, + @IconFile nvarchar(100), + @ControlType int, + @ViewOrder int, + @HelpUrl nvarchar(200), + @SupportsPartialRendering bit, + @SupportsPopUps bit, + @CreatedByUserID int + +AS + INSERT INTO {databaseOwner}{objectQualifier}ModuleControls ( + ModuleDefID, + ControlKey, + ControlTitle, + ControlSrc, + MvcControlClass, + IconFile, + ControlType, + ViewOrder, + HelpUrl, + SupportsPartialRendering, + SupportsPopUps, + CreatedByUserID, + CreatedOnDate, + LastModifiedByUserID, + LastModifiedOnDate + ) + VALUES ( + @ModuleDefID, + @ControlKey, + @ControlTitle, + @ControlSrc, + @MvcControlClass, + @IconFile, + @ControlType, + @ViewOrder, + @HelpUrl, + @SupportsPartialRendering, + @SupportsPopUps, + @CreatedByUserID, + getdate(), + @CreatedByUserID, + getdate() + ) + + SELECT SCOPE_IDENTITY() +GO + +/* Update UpdateModuleControl */ +/******************************/ + +IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}UpdateModuleControl]') AND type in (N'P', N'PC')) + DROP PROCEDURE {databaseOwner}[{objectQualifier}UpdateModuleControl] +GO + +CREATE PROCEDURE {databaseOwner}[{objectQualifier}UpdateModuleControl] + @ModuleControlId int, + @ModuleDefID int, + @ControlKey nvarchar(50), + @ControlTitle nvarchar(50), + @ControlSrc nvarchar(256), + @MvcControlClass nvarchar(256) = null, + @IconFile nvarchar(100), + @ControlType int, + @ViewOrder int, + @HelpUrl nvarchar(200), + @SupportsPartialRendering bit, + @SupportsPopUps bit, + @LastModifiedByUserID int + +AS + UPDATE {databaseOwner}{objectQualifier}ModuleControls + SET + ModuleDefId = @ModuleDefId, + ControlKey = @ControlKey, + ControlTitle = @ControlTitle, + ControlSrc = @ControlSrc, + MvcControlClass = @MvcControlClass, + IconFile = @IconFile, + ControlType = @ControlType, + ViewOrder = ViewOrder, + HelpUrl = @HelpUrl, + SupportsPartialRendering = @SupportsPartialRendering, + SupportsPopUps = @SupportsPopUps, + LastModifiedByUserID = @LastModifiedByUserID, + LastModifiedOnDate = getdate() + WHERE ModuleControlId = @ModuleControlId +GO + +/* Update Terms and privacy */ +/************************************************/ + +UPDATE {databaseOwner}{objectQualifier}ModuleControls + SET + MvcControlClass = 'DotNetNuke.Web.MvcWebsite.Controls.PrivacyControl, DotNetNuke.Web.MvcWebsite' + WHERE ControlSrc = 'Admin/Portal/Privacy.ascx' + +UPDATE {databaseOwner}{objectQualifier}ModuleControls + SET + MvcControlClass = 'DotNetNuke.Web.MvcWebsite.Controls.TermsControl, DotNetNuke.Web.MvcWebsite' + WHERE ControlSrc = 'Admin/Portal/Terms.ascx' diff --git a/DNN Platform/Website/Resources/Shared/scripts/jquery/jquery.form.js b/DNN Platform/Website/Resources/Shared/scripts/jquery/jquery.form.js new file mode 100644 index 00000000000..168d4b8b69c --- /dev/null +++ b/DNN Platform/Website/Resources/Shared/scripts/jquery/jquery.form.js @@ -0,0 +1,1540 @@ +/*! + * jQuery Form Plugin + * version: 4.3.0 + * Requires jQuery v1.7.2 or later + * Project repository: https://github.com/jquery-form/form + + * Copyright 2017 Kevin Morris + * Copyright 2006 M. Alsup + + * Dual licensed under the LGPL-2.1+ or MIT licenses + * https://github.com/jquery-form/form#license + + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +/* global ActiveXObject */ + +/* eslint-disable */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = function( root, jQuery ) { + if (typeof jQuery === 'undefined') { + // require('jQuery') returns a factory that requires window to build a jQuery instance, we normalize how we use modules + // that require this pattern but the window provided is a noop if it's defined (how jquery works) + if (typeof window !== 'undefined') { + jQuery = require('jquery'); + } + else { + jQuery = require('jquery')(root); + } + } + factory(jQuery); + return jQuery; + }; + } else { + // Browser globals + factory(jQuery); + } + +}(function ($) { +/* eslint-enable */ + 'use strict'; + + /* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are mutually exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').on('submit', function(e) { + e.preventDefault(); // <-- important + $(this).ajaxSubmit({ + target: '#output' + }); + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + You can also use ajaxForm with delegation (requires jQuery v1.7+), so the + form does not have to exist when you invoke ajaxForm: + + $('#myForm').ajaxForm({ + delegation: true, + target: '#output' + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. + */ + + var rCRLF = /\r?\n/g; + + /** + * Feature detection + */ + var feature = {}; + + feature.fileapi = $('').get(0).files !== undefined; + feature.formdata = (typeof window.FormData !== 'undefined'); + + var hasProp = !!$.fn.prop; + + // attr2 uses prop when it can but checks the return type for + // an expected string. This accounts for the case where a form + // contains inputs with names like "action" or "method"; in those + // cases "prop" returns the element + $.fn.attr2 = function() { + if (!hasProp) { + return this.attr.apply(this, arguments); + } + + var val = this.prop.apply(this, arguments); + + if ((val && val.jquery) || typeof val === 'string') { + return val; + } + + return this.attr.apply(this, arguments); + }; + + /** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + * + * @param {object|string} options jquery.form.js parameters or custom url for submission + * @param {object} data extraData + * @param {string} dataType ajax dataType + * @param {function} onSuccess ajax success callback function + */ + $.fn.ajaxSubmit = function(options, data, dataType, onSuccess) { + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + + return this; + } + + /* eslint consistent-this: ["error", "$form"] */ + var method, action, url, isMsie, iframeSrc, $form = this; + + if (typeof options === 'function') { + options = {success: options}; + + } else if (typeof options === 'string' || (options === false && arguments.length > 0)) { + options = { + 'url' : options, + 'data' : data, + 'dataType' : dataType + }; + + if (typeof onSuccess === 'function') { + options.success = onSuccess; + } + + } else if (typeof options === 'undefined') { + options = {}; + } + + method = options.method || options.type || this.attr2('method'); + action = options.url || this.attr2('action'); + + url = (typeof action === 'string') ? $.trim(action) : ''; + url = url || window.location.href || ''; + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/) || [])[1]; + } + // IE requires javascript:false in https, but this breaks chrome >83 and goes against spec. + // Instead of using javascript:false always, let's only apply it for IE. + isMsie = /(MSIE|Trident)/.test(navigator.userAgent || ''); + iframeSrc = (isMsie && /^https/i.test(window.location.href || '')) ? 'javascript:false' : 'about:blank'; // eslint-disable-line no-script-url + + options = $.extend(true, { + url : url, + success : $.ajaxSettings.success, + type : method || $.ajaxSettings.type, + iframeSrc : iframeSrc + }, options); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + + this.trigger('form-pre-serialize', [this, options, veto]); + + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + + return this; + } + + var traditional = options.traditional; + + if (typeof traditional === 'undefined') { + traditional = $.ajaxSettings.traditional; + } + + var elements = []; + var qx, a = this.formToArray(options.semantic, elements, options.filtering); + + if (options.data) { + var optionsData = $.isFunction(options.data) ? options.data(a) : options.data; + + options.extraData = optionsData; + qx = $.param(optionsData, traditional); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + + return this; + } + + var q = $.param(a, traditional); + + if (qx) { + q = (q ? (q + '&' + qx) : qx); + } + + if (options.type.toUpperCase() === 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } else { + options.data = q; // data is the query string for 'post' + } + + var callbacks = []; + + if (options.resetForm) { + callbacks.push(function() { + $form.resetForm(); + }); + } + + if (options.clearForm) { + callbacks.push(function() { + $form.clearForm(options.includeHidden); + }); + } + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + + callbacks.push(function(data, textStatus, jqXHR) { + var successArguments = arguments, + fn = options.replaceTarget ? 'replaceWith' : 'html'; + + $(options.target)[fn](data).each(function(){ + oldSuccess.apply(this, successArguments); + }); + }); + + } else if (options.success) { + if ($.isArray(options.success)) { + $.merge(callbacks, options.success); + } else { + callbacks.push(options.success); + } + } + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + var context = options.context || this; // jQuery 1.4+ supports scope context + + for (var i = 0, max = callbacks.length; i < max; i++) { + callbacks[i].apply(context, [data, status, xhr || $form, $form]); + } + }; + + if (options.error) { + var oldError = options.error; + + options.error = function(xhr, status, error) { + var context = options.context || this; + + oldError.apply(context, [xhr, status, error, $form]); + }; + } + + if (options.complete) { + var oldComplete = options.complete; + + options.complete = function(xhr, status) { + var context = options.context || this; + + oldComplete.apply(context, [xhr, status, $form]); + }; + } + + // are there files to upload? + + // [value] (issue #113), also see comment: + // https://github.com/malsup/form/commit/588306aedba1de01388032d5f42a60159eea9228#commitcomment-2180219 + var fileInputs = $('input[type=file]:enabled', this).filter(function() { + return $(this).val() !== ''; + }); + var hasFileInputs = fileInputs.length > 0; + var mp = 'multipart/form-data'; + var multipart = ($form.attr('enctype') === mp || $form.attr('encoding') === mp); + var fileAPI = feature.fileapi && feature.formdata; + + log('fileAPI :' + fileAPI); + + var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; + var jqxhr; + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if (options.iframe !== false && (options.iframe || shouldUseFrame)) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) { + $.get(options.closeKeepAlive, function() { + jqxhr = fileUploadIframe(a); + }); + + } else { + jqxhr = fileUploadIframe(a); + } + + } else if ((hasFileInputs || multipart) && fileAPI) { + jqxhr = fileUploadXhr(a); + + } else { + jqxhr = $.ajax(options); + } + + $form.removeData('jqxhr').data('jqxhr', jqxhr); + + // clear element array + for (var k = 0; k < elements.length; k++) { + elements[k] = null; + } + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + + return this; + + // utility fn for deep serialization + function deepSerialize(extraData) { + var serialized = $.param(extraData, options.traditional).split('&'); + var len = serialized.length; + var result = []; + var i, part; + + for (i = 0; i < len; i++) { + // #252; undo param space replacement + serialized[i] = serialized[i].replace(/\+/g, ' '); + part = serialized[i].split('='); + // #278; use array instead of object storage, favoring array serializations + result.push([decodeURIComponent(part[0]), decodeURIComponent(part[1])]); + } + + return result; + } + + // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) + function fileUploadXhr(a) { + var formdata = new FormData(); + + for (var i = 0; i < a.length; i++) { + formdata.append(a[i].name, a[i].value); + } + + if (options.extraData) { + var serializedData = deepSerialize(options.extraData); + + for (i = 0; i < serializedData.length; i++) { + if (serializedData[i]) { + formdata.append(serializedData[i][0], serializedData[i][1]); + } + } + } + + options.data = null; + + var s = $.extend(true, {}, $.ajaxSettings, options, { + contentType : false, + processData : false, + cache : false, + type : method || 'POST' + }); + + if (options.uploadProgress) { + // workaround because jqXHR does not expose upload property + s.xhr = function() { + var xhr = $.ajaxSettings.xhr(); + + if (xhr.upload) { + xhr.upload.addEventListener('progress', function(event) { + var percent = 0; + var position = event.loaded || event.position; /* event.position is deprecated */ + var total = event.total; + + if (event.lengthComputable) { + percent = Math.ceil(position / total * 100); + } + + options.uploadProgress(event, position, total, percent); + }, false); + } + + return xhr; + }; + } + + s.data = null; + + var beforeSend = s.beforeSend; + + s.beforeSend = function(xhr, o) { + // Send FormData() provided by user + if (options.formData) { + o.data = options.formData; + } else { + o.data = formdata; + } + + if (beforeSend) { + beforeSend.call(this, xhr, o); + } + }; + + return $.ajax(s); + } + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUploadIframe(a) { + var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; + var deferred = $.Deferred(); + + // #341 + deferred.abort = function(status) { + xhr.abort(status); + }; + + if (a) { + // ensure that every serialized input is still enabled + for (i = 0; i < elements.length; i++) { + el = $(elements[i]); + if (hasProp) { + el.prop('disabled', false); + } else { + el.removeAttr('disabled'); + } + } + } + + s = $.extend(true, {}, $.ajaxSettings, options); + s.context = s.context || s; + id = 'jqFormIO' + new Date().getTime(); + var ownerDocument = form.ownerDocument; + var $body = $form.closest('body'); + + if (s.iframeTarget) { + $io = $(s.iframeTarget, ownerDocument); + n = $io.attr2('name'); + if (!n) { + $io.attr2('name', id); + } else { + id = n; + } + + } else { + $io = $(' + + 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/Menus/ModuleActions/ModuleActions.js b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.js index 3d7996f1688..7a9961eb9c3 100644 --- a/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.js +++ b/DNN Platform/Website/admin/Menus/ModuleActions/ModuleActions.js @@ -242,7 +242,6 @@ var modulePane = $(".DnnModule-" + moduleId).parent(); var paneName = modulePane.attr("id").replace("dnn_", ""); - var htmlString; var moduleIndex = -1; var id = paneName + moduleId; var modules = modulePane.children(); @@ -261,40 +260,20 @@ //Add Top/Up actions if (moduleIndex > 0) { - htmlString = "
  • " + opts.topText; - parent.append(htmlString); + var $topItem = $("
  • ", { id: id + "-top", addClass: "common", text: opts.topText, click: () => moveTop(paneName), }); + parent.append($topItem); - //Add click event handler to just added element - parent.find("li#" + id + "-top").click(function () { - moveTop(paneName); - }); - - htmlString = "
  • " + opts.upText; - parent.append(htmlString); - - //Add click event handler to just added element - parent.find("li#" + id + "-up").click(function () { - moveUp(paneName, moduleIndex); - }); + var $upItem = $("
  • ", { id: id + "-up", addClass: "common", text: opts.upText, click: () => moveUp(paneName, moduleIndex), }); + parent.append($upItem); } //Add Bottom/Down actions if (moduleIndex < moduleCount - 1) { - htmlString = "
  • " + opts.downText; - parent.append(htmlString); + var $downItem = $("
  • ", { id: id + "-down", addClass: "common", text: opts.downText, click: () => moveDown(paneName, moduleIndex), }); + parent.append($downItem); - //Add click event handler to just added element - parent.find("li#" + id + "-down").click(function () { - moveDown(paneName, moduleIndex); - }); - - htmlString = "
  • " + opts.bottomText; - parent.append(htmlString); - - //Add click event handler to just added element - parent.find("li#" + id + "-bottom").click(function () { - moveBottom(paneName); - }); + var $bottomItem = $("
  • ", { id: id + "-bottom", addClass: "common", text: opts.bottomText, click: () => moveBottom(paneName), }); + parent.append($bottomItem); } var htmlStringContainer = ""; @@ -321,7 +300,7 @@ if (!rootText || rootText.length == 0) { return; } - root.append("
  • " + moduleId + ":" + rootText + "
    "); + root.append($('
  • ', { addClass: rootClass, }).append($('
    ', { text: moduleId + ":" + rootText, }))); } function buildQuickSettings(root, rootText, rootClass, rootIcon) { diff --git a/DNN Platform/Website/admin/Modules/App_LocalResources/ModuleSettings.ascx.resx b/DNN Platform/Website/admin/Modules/App_LocalResources/ModuleSettings.ascx.resx index ff3606ec7a3..4cc489e4095 100644 --- a/DNN Platform/Website/admin/Modules/App_LocalResources/ModuleSettings.ascx.resx +++ b/DNN Platform/Website/admin/Modules/App_LocalResources/ModuleSettings.ascx.resx @@ -370,6 +370,12 @@ The new value of the moniker already exists; it must be unique in the portal. + + JavaScript is not allowed in the header of modules. + + + JavaScript is not allowed in the footer of modules. + Displays the id of the module. diff --git a/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs b/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs index 7236030dbd8..da89ced0c74 100644 --- a/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs +++ b/DNN Platform/Website/admin/Modules/Modulesettings.ascx.cs @@ -465,6 +465,23 @@ protected void OnUpdateClick(object sender, EventArgs e) } this.Module.IsDeleted = false; + + // If JavaScript is not allowed in module header but changes contain JavaScript, avoid saving the changes + if (!this.PortalSettings.AllowJsInModuleHeaders && HtmlUtils.ContainsJavaScript(this.txtHeader.Text)) + { + string message = Localization.GetString("JavaScriptInHeader", this.LocalResourceFile); + Skin.AddModuleMessage(this, message, ModuleMessage.ModuleMessageType.RedError); + return; + } + + // If JavaScript is not allowed in module footer but changes contain JavaScript, avoid saving the changes + if (!this.PortalSettings.AllowJsInModuleFooters && HtmlUtils.ContainsJavaScript(this.txtFooter.Text)) + { + string message = Localization.GetString("JavaScriptInFooter", this.LocalResourceFile); + Skin.AddModuleMessage(this, message, ModuleMessage.ModuleMessageType.RedError); + return; + } + this.Module.Header = this.txtHeader.Text; this.Module.Footer = this.txtFooter.Text; @@ -649,7 +666,6 @@ private void BindData() this.cboAlign.Items.FindByValue(this.Module.Alignment).Selected = true; this.txtColor.Text = this.Module.Color; this.txtBorder.Text = this.Module.Border; - this.txtHeader.Text = this.Module.Header; this.txtFooter.Text = this.Module.Footer; 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 257095c61ef..26bec7844a1 100644 --- a/DNN Platform/Website/development.config +++ b/DNN Platform/Website/development.config @@ -141,7 +141,8 @@ - + + @@ -159,7 +160,7 @@ --> - + - +
    ").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("