diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index 7727b6b760..4b3268d4c5 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -73,14 +73,14 @@ namespace Umbraco.Web.Install.InstallSteps { var databases = new List() { - new { name = "Microsoft SQL Server", id = 1 }, - new { name = "Microsoft SQL Azure", id = 3 }, - new { name = "Custom connection string", id = -1 }, + new { name = "Microsoft SQL Server", id = DatabaseType.SqlServer.ToString() }, + new { name = "Microsoft SQL Azure", id = DatabaseType.SqlAzure.ToString() }, + new { name = "Custom connection string", id = DatabaseType.Custom.ToString() }, }; if (IsSqlCeAvailable()) { - databases.Insert(0, new { name = "Microsoft SQL Server Compact (SQL CE)", id = 0 }); + databases.Insert(0, new { name = "Microsoft SQL Server Compact (SQL CE)", id = DatabaseType.SqlCe.ToString() }); } return new diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/HtmlHelperExtensionMethodsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/HtmlHelperExtensionMethodsTests.cs new file mode 100644 index 0000000000..abb44a5dfb --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Extensions/HtmlHelperExtensionMethodsTests.cs @@ -0,0 +1,118 @@ +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; +using Microsoft.Extensions.WebEncoders.Testing; +using Moq; +using NUnit.Framework; +using Umbraco.Extensions; + +namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Filters +{ + + [TestFixture] + public class HtmlHelperExtensionMethodsTests + { + private const string SampleWithAnchorElement = "Hello world, this is some text with a link"; + private const string SampleWithBoldAndAnchorElements = "Hello world, this is some text with a link"; + + [SetUp] + public virtual void Initialize() + { + //create an empty htmlHelper + _htmlHelper = new HtmlHelper(Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + new HtmlTestEncoder(), + UrlEncoder.Default); + } + + private HtmlHelper _htmlHelper; + + [Test] + public void Truncate_Simple() + { + var result = _htmlHelper.Truncate(SampleWithAnchorElement, 25).ToString(); + + Assert.AreEqual("Hello world, this is some…", result); + } + + [Test] + public void When_Truncating_A_String_Ends_With_A_Space_We_Should_Trim_The_Space_Before_Appending_The_Ellipsis() + { + var result = _htmlHelper.Truncate(SampleWithAnchorElement, 26).ToString(); + + Assert.AreEqual("Hello world, this is some…", result); + } + + [Test] + public void Truncate_Inside_Word() + { + var result = _htmlHelper.Truncate(SampleWithAnchorElement, 24).ToString(); + + Assert.AreEqual("Hello world, this is som…", result); + } + + [Test] + public void Truncate_With_Tag() + { + var result = _htmlHelper.Truncate(SampleWithAnchorElement, 35).ToString(); + + Assert.AreEqual("Hello world, this is some text with…", result); + } + + [Test] + public void Truncate_By_Words() + { + var result = _htmlHelper.TruncateByWords(SampleWithAnchorElement, 4).ToString(); + + Assert.AreEqual("Hello world, this is…", result); + } + + [Test] + public void Truncate_By_Words_With_Tag() + { + var result = _htmlHelper.TruncateByWords(SampleWithBoldAndAnchorElements, 4).ToString(); + + Assert.AreEqual("Hello world, this is…", result); + } + + [Test] + public void Truncate_By_Words_Mid_Tag() + { + var result = _htmlHelper.TruncateByWords(SampleWithAnchorElement, 7).ToString(); + + Assert.AreEqual("Hello world, this is some text with…", result); + } + + [Test] + public void Strip_All_Html() + { + var result = _htmlHelper.StripHtml(SampleWithBoldAndAnchorElements, null).ToString(); + + Assert.AreEqual("Hello world, this is some text with a link", result); + } + + [Test] + public void Strip_Specific_Html() + { + string[] tags = { "b" }; + + var result = _htmlHelper.StripHtml(SampleWithBoldAndAnchorElements, tags).ToString(); + + Assert.AreEqual(SampleWithAnchorElement, result); + } + + [Test] + public void Strip_Invalid_Html() + { + const string text = "Hello world, is some text with a link"; + + var result = _htmlHelper.StripHtml(text).ToString(); + + Assert.AreEqual("Hello world, is some text with a link", result); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringFilterTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringFilterTests.cs new file mode 100644 index 0000000000..3ed4a28b06 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringFilterTests.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.DataProtection; +using NUnit.Framework; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Common.Filters; +using Umbraco.Web.Common.Security; + +namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Filters +{ + [TestFixture] + public class ValidateUmbracoFormRouteStringFilterTests + { + private IDataProtectionProvider DataProtectionProvider { get; } = new EphemeralDataProtectionProvider(); + + [Test] + public void Validate_Route_String() + { + var filter = new ValidateUmbracoFormRouteStringAttribute.ValidateUmbracoFormRouteStringFilter(DataProtectionProvider); + + Assert.Throws(() => filter.ValidateRouteString(null, null, null, null)); + + const string ControllerName = "Test"; + const string ControllerAction = "Index"; + const string Area = "MyArea"; + var validUfprt = EncryptionHelper.CreateEncryptedRouteString(DataProtectionProvider, ControllerName, ControllerAction, Area); + + var invalidUfprt = validUfprt + "z"; + Assert.Throws(() => filter.ValidateRouteString(invalidUfprt, null, null, null)); + + Assert.Throws(() => filter.ValidateRouteString(validUfprt, ControllerName, ControllerAction, "doesntMatch")); + Assert.Throws(() => filter.ValidateRouteString(validUfprt, ControllerName, ControllerAction, null)); + Assert.Throws(() => filter.ValidateRouteString(validUfprt, ControllerName, "doesntMatch", Area)); + Assert.Throws(() => filter.ValidateRouteString(validUfprt, ControllerName, null, Area)); + Assert.Throws(() => filter.ValidateRouteString(validUfprt, "doesntMatch", ControllerAction, Area)); + Assert.Throws(() => filter.ValidateRouteString(validUfprt, null, ControllerAction, Area)); + + Assert.DoesNotThrow(() => filter.ValidateRouteString(validUfprt, ControllerName, ControllerAction, Area)); + Assert.DoesNotThrow(() => filter.ValidateRouteString(validUfprt, ControllerName.ToLowerInvariant(), ControllerAction.ToLowerInvariant(), Area.ToLowerInvariant())); + } + + } +} diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Mvc/HtmlStringUtilitiesTests.cs similarity index 94% rename from src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Mvc/HtmlStringUtilitiesTests.cs index 36ddecc676..4b1675b7f0 100644 --- a/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Mvc/HtmlStringUtilitiesTests.cs @@ -1,7 +1,8 @@ using NUnit.Framework; using Umbraco.Web; +using Umbraco.Web.Common.Mvc; -namespace Umbraco.Tests.Web.Mvc +namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Mvc { [TestFixture] public class HtmlStringUtilitiesTests diff --git a/src/Umbraco.Tests/Web/UrlHelperExtensionTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/EncryptionHelperTests.cs similarity index 59% rename from src/Umbraco.Tests/Web/UrlHelperExtensionTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/EncryptionHelperTests.cs index a4b96ab4ff..db80e2cd74 100644 --- a/src/Umbraco.Tests/Web/UrlHelperExtensionTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/EncryptionHelperTests.cs @@ -1,15 +1,20 @@ using System.Collections.Generic; +using Microsoft.AspNetCore.DataProtection; using NUnit.Framework; using Umbraco.Core; using Umbraco.Web; +using Umbraco.Web.Common.Security; -namespace Umbraco.Tests.Web +namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Security { [TestFixture] - public class UrlHelperExtensionTests + public class EncryptionHelperTests { + + private IDataProtectionProvider DataProtectionProvider { get; } = new EphemeralDataProtectionProvider(); + [Test] - public static void Create_Encrypted_RouteString_From_Anonymous_Object() + public void Create_Encrypted_RouteString_From_Anonymous_Object() { var additionalRouteValues = new { @@ -19,14 +24,15 @@ namespace Umbraco.Tests.Web keY4 = "valuE4" }; - var encryptedRouteString = UrlHelperRenderExtensions.CreateEncryptedRouteString( + var encryptedRouteString = EncryptionHelper.CreateEncryptedRouteString( + DataProtectionProvider, "FormController", "FormAction", "", additionalRouteValues ); - var result = encryptedRouteString.DecryptWithMachineKey(); + var result = EncryptionHelper.Decrypt(encryptedRouteString, DataProtectionProvider); const string expectedResult = "c=FormController&a=FormAction&ar=&key1=value1&key2=value2&Key3=Value3&keY4=valuE4"; @@ -34,7 +40,7 @@ namespace Umbraco.Tests.Web } [Test] - public static void Create_Encrypted_RouteString_From_Dictionary() + public void Create_Encrypted_RouteString_From_Dictionary() { var additionalRouteValues = new Dictionary() { @@ -44,14 +50,15 @@ namespace Umbraco.Tests.Web {"keY4", "valuE4"} }; - var encryptedRouteString = UrlHelperRenderExtensions.CreateEncryptedRouteString( + var encryptedRouteString = EncryptionHelper.CreateEncryptedRouteString( + DataProtectionProvider, "FormController", "FormAction", "", additionalRouteValues ); - var result = encryptedRouteString.DecryptWithMachineKey(); + var result = EncryptionHelper.Decrypt(encryptedRouteString, DataProtectionProvider); const string expectedResult = "c=FormController&a=FormAction&ar=&key1=value1&key2=value2&Key3=Value3&keY4=valuE4"; diff --git a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs b/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs deleted file mode 100644 index 342aa99b47..0000000000 --- a/src/Umbraco.Tests/Publishing/PublishingStrategyTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Events; -using Umbraco.Core.Models; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Services; -using Umbraco.Tests.Testing; - -namespace Umbraco.Tests.Publishing -{ - [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] - public class PublishingStrategyTests : TestWithDatabaseBase - { - private IContent _homePage; - - [NUnit.Framework.Ignore("fixme - ignored test")] - [Test] - public void Can_Publish_And_Update_Xml_Cache() - { - // TODO: Create new test - } - - public IEnumerable CreateTestData() - { - //NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. - - //Create and Save ContentType "umbTextpage" -> 1045 - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage", "Textpage"); - ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! - ServiceContext.ContentTypeService.Save(contentType); - var mandatoryType = MockedContentTypes.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", true); - //ServiceContext.FileService.SaveTemplate(mandatoryType.DefaultTemplate); // else, FK violation on contentType! - ServiceContext.ContentTypeService.Save(mandatoryType); - - //Create and Save Content "Homepage" based on "umbTextpage" -> 1046 - _homePage = MockedContent.CreateSimpleContent(contentType); - ServiceContext.ContentService.Save(_homePage, 0); - - //Create and Save Content "Text Page 1" based on "umbTextpage" -> 1047 - Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", _homePage.Id); - ServiceContext.ContentService.Save(subpage, 0); - - //Create and Save Content "Text Page 2" based on "umbTextpage" -> 1048 - Content subpage2 = MockedContent.CreateSimpleContent(contentType, "Text Page 2", _homePage.Id); - ServiceContext.ContentService.Save(subpage2, 0); - - //Create and Save Content "Text Page 3" based on "umbTextpage" -> 1048 - Content subpage3 = MockedContent.CreateSimpleContent(contentType, "Text Page 3", subpage2.Id); - ServiceContext.ContentService.Save(subpage3, 0); - - return new[] {_homePage, subpage, subpage2, subpage3}; - } - } -} diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index de20211774..b3663738dd 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -48,7 +48,7 @@ namespace Umbraco.Tests.Routing new TestUmbracoContextAccessor(), TestObjects.GetGlobalSettings(), ShortStringHelper, - new SurfaceControllerTypeCollection(Enumerable.Empty()), + // new SurfaceControllerTypeCollection(Enumerable.Empty()), new UmbracoApiControllerTypeCollection(Enumerable.Empty()), HostingEnvironment); } @@ -69,8 +69,8 @@ namespace Umbraco.Tests.Routing // set the default RenderMvcController Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // FIXME: Wrong! - var surfaceControllerTypes = new SurfaceControllerTypeCollection(Composition.TypeLoader.GetSurfaceControllers()); - Composition.Services.AddUnique(surfaceControllerTypes); + // var surfaceControllerTypes = new SurfaceControllerTypeCollection(Composition.TypeLoader.GetSurfaceControllers()); + // Composition.Services.AddUnique(surfaceControllerTypes); var umbracoApiControllerTypes = new UmbracoApiControllerTypeCollection(Composition.TypeLoader.GetUmbracoApiControllers()); Composition.Services.AddUnique(umbracoApiControllerTypes); diff --git a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs b/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs deleted file mode 100644 index 70beb3643a..0000000000 --- a/src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using Examine; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Runtime; -using Umbraco.Net; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; -using Umbraco.Web; -using Umbraco.Web.Hosting; -using Umbraco.Web.Runtime; -using ConnectionStrings = Umbraco.Core.Configuration.Models.ConnectionStrings; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Tests.Runtimes -{ - [TestFixture] - public class CoreRuntimeTests - { - [SetUp] - public void SetUp() - { - TestComponent.Reset(); - } - - public void TearDown() - { - TestComponent.Reset(); - } - - - // test application - public class TestUmbracoApplication : UmbracoApplicationBase - { - public TestUmbracoApplication() : base(new NullLogger(), - NullLoggerFactory.Instance, - new SecuritySettings(), - new GlobalSettings(), - new ConnectionStrings(), - _ioHelper, _profiler, new AspNetHostingEnvironment(Options.Create(new HostingSettings())), new AspNetBackOfficeInfo(_globalSettings, _ioHelper, new NullLogger(), Options.Create(new WebRoutingSettings()))) - { - } - - private static readonly IIOHelper _ioHelper = TestHelper.IOHelper; - private static readonly IProfiler _profiler = new TestProfiler(); - private static readonly GlobalSettings _globalSettings = new GlobalSettings(); - - - - protected override CoreRuntimeBootstrapper GetRuntime(GlobalSettings globalSettings, ConnectionStrings connectionStrings, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, ILoggerFactory loggerFactory, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - { - return new TestRuntimeBootstrapper(globalSettings, connectionStrings, umbracoVersion, ioHelper, logger, loggerFactory, profiler, hostingEnvironment, backOfficeInfo); - } - } - - // test runtime - public class TestRuntimeBootstrapper : CoreRuntimeBootstrapper - { - public TestRuntimeBootstrapper(GlobalSettings globalSettings, ConnectionStrings connectionStrings, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, ILogger logger, ILoggerFactory loggerFactory, IProfiler profiler, IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo) - :base(globalSettings, connectionStrings,umbracoVersion, ioHelper, loggerFactory, profiler, new AspNetUmbracoBootPermissionChecker(), hostingEnvironment, backOfficeInfo, TestHelper.DbProviderFactoryCreator, TestHelper.MainDom, TestHelper.GetTypeFinder(), AppCaches.NoCache) - { - - } - - // must override the database factory - // else BootFailedException because U cannot connect to the configured db - protected internal override IUmbracoDatabaseFactory CreateBootstrapDatabaseFactory() - { - var mock = new Mock(); - mock.Setup(x => x.Configured).Returns(true); - mock.Setup(x => x.CanConnect).Returns(true); - return mock.Object; - } - - public override void Configure(IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); - - - base.Configure(services); - } - - // runs with only one single component - // UmbracoCoreComponent will be force-added too - // and that's it - protected override IEnumerable GetComposerTypes(TypeLoader typeLoader) - { - return new[] { typeof(TestComposer) }; - } - } - - - public class TestComposer : IComposer - { - // test flags - public static bool Ctored; - public static bool Composed; - - public static void Reset() - { - Ctored = Composed = false; - } - - public TestComposer() - { - Ctored = true; - } - - public void Compose(Composition composition) - { - composition.Services.AddUnique(); - composition.Components().Append(); - - Composed = true; - } - } - - public class TestComponent : IComponent, IDisposable - { - // test flags - public static bool Ctored; - public static bool Initialized; - public static bool Terminated; - public static IProfilingLogger ProfilingLogger; - - public bool Disposed; - - public static void Reset() - { - Ctored = Initialized = Terminated = false; - ProfilingLogger = null; - } - - public TestComponent(IProfilingLogger proflog) - { - Ctored = true; - ProfilingLogger = proflog; - } - - public void Initialize() - { - Initialized = true; - } - - public void Terminate() - { - Terminated = true; - } - - public void Dispose() - { - Disposed = true; - } - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 5d6d4222e4..ec62d7f1f5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -141,7 +141,6 @@ - @@ -202,20 +201,16 @@ - - - - @@ -242,7 +237,6 @@ - @@ -283,7 +277,6 @@ - diff --git a/src/Umbraco.Tests/Web/Controllers/BackOfficeControllerUnitTests.cs b/src/Umbraco.Tests/Web/Controllers/BackOfficeControllerUnitTests.cs deleted file mode 100644 index fa9335bc3f..0000000000 --- a/src/Umbraco.Tests/Web/Controllers/BackOfficeControllerUnitTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Linq; -using NUnit.Framework; -using Umbraco.Web.Editors; - -namespace Umbraco.Tests.Web.Controllers -{ - [TestFixture] - public class BackOfficeControllerUnitTests - { - public static object[] TestLegacyJsActionPaths = new object[] { - new string[] - { - "alert('hello');", - "function test() { window.location = 'http://www.google.com'; }", - "function openCourierSecurity(userid){ UmbClientMgr.contentFrame('page?userid=123); }", - @"function openRepository(repo, folder){ UmbClientMgr.contentFrame('page?repo=repo&folder=folder); } - function openTransfer(revision, repo, folder){ UmbClientMgr.contentFrame('page?revision=revision&repo=repo&folder=folder); }", - "umbraco/js/test.js", - "/umbraco/js/test.js", - "~/umbraco/js/test.js" - } - }; - - - } -} diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs b/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs deleted file mode 100644 index 1a4220c83b..0000000000 --- a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Web.Mvc; -using NUnit.Framework; -using Umbraco.Web; - -namespace Umbraco.Tests.Web.Mvc -{ - - [TestFixture] - public class HtmlHelperExtensionMethodsTests - { - private const string SampleWithAnchorElement = "Hello world, this is some text with a link"; - private const string SampleWithBoldAndAnchorElements = "Hello world, this is some text with a link"; - - [SetUp] - public virtual void Initialize() - { - //create an empty htmlHelper - _htmlHelper = new HtmlHelper(new ViewContext(), new ViewPage()); - } - - private HtmlHelper _htmlHelper; - - [Test] - public void Wrap_Simple() - { - var output = _htmlHelper.Wrap("div", "hello world"); - Assert.AreEqual("
hello world
", output.ToHtmlString()); - } - - [Test] - public void Wrap_Object_Attributes() - { - var output = _htmlHelper.Wrap("div", "hello world", new {style = "color:red;", onclick = "void();"}); - Assert.AreEqual("
hello world
", output.ToHtmlString()); - } - - [Test] - public static void Truncate_Simple() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.Truncate(SampleWithAnchorElement, 25).ToString(); - - Assert.AreEqual("Hello world, this is some…", result); - } - - [Test] - public static void When_Truncating_A_String_Ends_With_A_Space_We_Should_Trim_The_Space_Before_Appending_The_Ellipsis() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.Truncate(SampleWithAnchorElement, 26).ToString(); - - Assert.AreEqual("Hello world, this is some…", result); - } - - [Test] - public static void Truncate_Inside_Word() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.Truncate(SampleWithAnchorElement, 24).ToString(); - - Assert.AreEqual("Hello world, this is som…", result); - } - - [Test] - public static void Truncate_With_Tag() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.Truncate(SampleWithAnchorElement, 35).ToString(); - - Assert.AreEqual("Hello world, this is some text with…", result); - } - - [Test] - public static void Truncate_By_Words() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.TruncateByWords(SampleWithAnchorElement, 4).ToString(); - - Assert.AreEqual("Hello world, this is…", result); - } - - [Test] - public static void Truncate_By_Words_With_Tag() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.TruncateByWords(SampleWithBoldAndAnchorElements, 4).ToString(); - - Assert.AreEqual("Hello world, this is…", result); - } - - [Test] - public static void Truncate_By_Words_Mid_Tag() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.TruncateByWords(SampleWithAnchorElement, 7).ToString(); - - Assert.AreEqual("Hello world, this is some text with…", result); - } - - [Test] - public static void Strip_All_Html() - { - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.StripHtml(SampleWithBoldAndAnchorElements, null).ToString(); - - Assert.AreEqual("Hello world, this is some text with a link", result); - } - - [Test] - public static void Strip_Specific_Html() - { - string[] tags = { "b" }; - - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.StripHtml(SampleWithBoldAndAnchorElements, tags).ToString(); - - Assert.AreEqual(SampleWithAnchorElement, result); - } - - [Test] - public static void Strip_Invalid_Html() - { - const string text = "Hello world, is some text with a link"; - - var helper = new HtmlHelper(new ViewContext(), new ViewPage()); - var result = helper.StripHtml(text).ToString(); - - Assert.AreEqual("Hello world, is some text with a link", result); - } - } -} diff --git a/src/Umbraco.Tests/Web/Mvc/ValidateUmbracoFormRouteStringAttributeTests.cs b/src/Umbraco.Tests/Web/Mvc/ValidateUmbracoFormRouteStringAttributeTests.cs deleted file mode 100644 index d4c3b7c887..0000000000 --- a/src/Umbraco.Tests/Web/Mvc/ValidateUmbracoFormRouteStringAttributeTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using NUnit.Framework; -using Umbraco.Web; -using Umbraco.Web.Mvc; - -namespace Umbraco.Tests.Web.Mvc -{ - [TestFixture] - public class ValidateUmbracoFormRouteStringAttributeTests - { - [Test] - public void Validate_Route_String() - { - var attribute = new ValidateUmbracoFormRouteStringAttribute(); - - Assert.Throws(() => attribute.ValidateRouteString(null, null, null, null)); - - const string ControllerName = "Test"; - const string ControllerAction = "Index"; - const string Area = "MyArea"; - var validUfprt = UrlHelperRenderExtensions.CreateEncryptedRouteString(ControllerName, ControllerAction, Area); - - var invalidUfprt = validUfprt + "z"; - Assert.Throws(() => attribute.ValidateRouteString(invalidUfprt, null, null, null)); - - Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, ControllerAction, "doesntMatch")); - Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, ControllerAction, null)); - Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, "doesntMatch", Area)); - Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, null, Area)); - Assert.Throws(() => attribute.ValidateRouteString(validUfprt, "doesntMatch", ControllerAction, Area)); - Assert.Throws(() => attribute.ValidateRouteString(validUfprt, null, ControllerAction, Area)); - - Assert.DoesNotThrow(() => attribute.ValidateRouteString(validUfprt, ControllerName, ControllerAction, Area)); - Assert.DoesNotThrow(() => attribute.ValidateRouteString(validUfprt, ControllerName.ToLowerInvariant(), ControllerAction.ToLowerInvariant(), Area.ToLowerInvariant())); - } - - } -} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 511254e96b..928c0dcd3d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -95,7 +95,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog /// /// - [UmbracoAuthorize] + [UmbracoBackOfficeAuthorize] public IDictionary GetPasswordConfig(int userId) { return _passwordConfiguration.GetConfiguration(userId != _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id); @@ -183,7 +183,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session /// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies. /// - [UmbracoAuthorize] + [UmbracoBackOfficeAuthorize] [SetAngularAntiForgeryTokens] //[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level public UserDetail GetCurrentUser() @@ -205,7 +205,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// We cannot user GetCurrentUser since that requires they are approved, this is the same as GetCurrentUser but doesn't require them to be approved /// - [UmbracoAuthorize(redirectToUmbracoLogin: false, requireApproval: false)] + [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: false, requireApproval: false)] [SetAngularAntiForgeryTokens] public ActionResult GetCurrentInvitedUser() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 99eae2c459..06ac53441b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; @@ -220,7 +221,7 @@ namespace Umbraco.Web.BackOffice.Controllers return nestedDictionary; } - [UmbracoAuthorize(Order = 0)] + [UmbracoBackOfficeAuthorize(Order = 0)] [HttpGet] public IEnumerable GetGridConfig() { @@ -231,7 +232,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Returns the JavaScript object representing the static server variables javascript object /// /// - [UmbracoAuthorize(Order = 0)] + [UmbracoBackOfficeAuthorize(Order = 0)] [MinifyJavaScriptResult(Order = 1)] public async Task ServerVariables() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 6dd7164520..709be76acc 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -131,7 +131,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [HttpGet] - [UmbracoAuthorize, OverrideAuthorization] + [UmbracoBackOfficeAuthorize, OverrideAuthorization] public bool AllowsCultureVariation() { var contentTypes = _contentTypeService.GetAll(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 0254021b40..3605e65563 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -170,7 +170,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// This only works when the user is logged in (partially) /// - [UmbracoAuthorize(redirectToUmbracoLogin: false, requireApproval : true)] + [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: false, requireApproval : true)] public async Task PostSetInvitedUserPassword([FromBody]string newPassword) { var user = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackofficeSecurity.GetUserId().ResultOr(0).ToString()); @@ -235,7 +235,7 @@ namespace Umbraco.Web.BackOffice.Controllers throw HttpResponseException.CreateValidationErrorResponse(ModelState); } - [UmbracoAuthorize] + [UmbracoBackOfficeAuthorize] [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 0d341e9a04..4d12f8db0c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.BackOffice.Controllers [ValidationFilter] [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions [IsBackOffice] - [UmbracoAuthorize] + [UmbracoBackOfficeAuthorize] public class DashboardController : UmbracoApiController { private readonly IUmbracoContextAccessor _umbracoContextAccessor; diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 07e3cb13da..cd320d49ff 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -64,7 +64,7 @@ namespace Umbraco.Web.BackOffice.Controllers _viewEngines = viewEngines; } - [UmbracoAuthorize(redirectToUmbracoLogin: true, requireApproval : false)] + [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: true, requireApproval : false)] [DisableBrowserCache] public ActionResult Index() { @@ -107,7 +107,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The endpoint that is loaded within the preview iframe /// /// - [UmbracoAuthorize] + [UmbracoBackOfficeAuthorize] public ActionResult Frame(int id, string culture) { var user = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser; diff --git a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs index c232401b78..e3d779d61d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// [IsBackOffice] [UmbracoUserTimeoutFilter] - [UmbracoAuthorize] + [UmbracoBackOfficeAuthorize] [DisableBrowserCache] [UmbracoWebApiRequireHttps] [CheckIfUserTicketDataIsStale] diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs index cf41670d8e..4b8f730e45 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs @@ -1,6 +1,7 @@ using System; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; @@ -8,12 +9,12 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Strings; +using Umbraco.Extensions; using Umbraco.Web.Common.ModelBinders; namespace Umbraco.Web.Common.AspNetCore @@ -110,6 +111,8 @@ namespace Umbraco.Web.Common.AspNetCore ViewData = (ViewDataDictionary) viewData; } + + // viewData is the ViewDataDictionary (maybe ) that we have // modelType is the type of the model that we need to bind to // @@ -143,5 +146,16 @@ namespace Umbraco.Web.Common.AspNetCore var nViewData = (ViewDataDictionary)Activator.CreateInstance(nViewDataType, tViewData); return nViewData; } + + public HtmlString RenderSection(string name, HtmlString defaultContents) + { + return RazorPageExtensions.RenderSection(this, name, defaultContents); + } + + public HtmlString RenderSection(string name, string defaultContents) + { + return RazorPageExtensions.RenderSection(this, name, defaultContents); + } + } } diff --git a/src/Umbraco.Web.Common/Constants/ViewConstants.cs b/src/Umbraco.Web.Common/Constants/ViewConstants.cs index 5da1a74f55..4c87509069 100644 --- a/src/Umbraco.Web.Common/Constants/ViewConstants.cs +++ b/src/Umbraco.Web.Common/Constants/ViewConstants.cs @@ -8,5 +8,12 @@ internal const string ViewLocation = "~/Views"; internal const string DataTokenCurrentViewContext = "umbraco-current-view-context"; + + internal static class ReservedAdditionalKeys + { + internal const string Controller = "c"; + internal const string Action = "a"; + internal const string Area = "ar"; + } } } diff --git a/src/Umbraco.Web.Common/Exceptions/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web.Common/Exceptions/HttpUmbracoFormRouteStringException.cs new file mode 100644 index 0000000000..8ba326a926 --- /dev/null +++ b/src/Umbraco.Web.Common/Exceptions/HttpUmbracoFormRouteStringException.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Common.Exceptions +{ + /// + /// Exception that occurs when an Umbraco form route string is invalid + /// + [Serializable] + public sealed class HttpUmbracoFormRouteStringException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public HttpUmbracoFormRouteStringException() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that holds the contextual information about the source or destination. + private HttpUmbracoFormRouteStringException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + public HttpUmbracoFormRouteStringException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + /// The , if any, that threw the current exception. + public HttpUmbracoFormRouteStringException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Umbraco.Web/BlockListTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs similarity index 51% rename from src/Umbraco.Web/BlockListTemplateExtensions.cs rename to src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs index 6e105a24d6..06c3efaf02 100644 --- a/src/Umbraco.Web/BlockListTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs @@ -1,30 +1,30 @@ using System; -using System.Web.Mvc; -using System.Web.Mvc.Html; using Umbraco.Core.Models.Blocks; using Umbraco.Core.Models.PublishedContent; -using System.Web; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; -namespace Umbraco.Web +namespace Umbraco.Extensions { public static class BlockListTemplateExtensions { public const string DefaultFolder = "BlockList/"; public const string DefaultTemplate = "Default"; - public static IHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) + public static IHtmlContent GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) { - if (model?.Count == 0) return new MvcHtmlString(string.Empty); + if (model?.Count == 0) return new HtmlString(string.Empty); var view = DefaultFolder + template; return html.Partial(view, model); } - public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); + public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); - public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); + public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); - public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) { if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); diff --git a/src/Umbraco.Web/CacheHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs similarity index 83% rename from src/Umbraco.Web/CacheHelperExtensions.cs rename to src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs index e27b0db1fc..e8309262e6 100644 --- a/src/Umbraco.Web/CacheHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs @@ -1,11 +1,11 @@ using System; -using System.Web; -using System.Web.Mvc; -using System.Web.Mvc.Html; +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Umbraco.Core.Cache; using Umbraco.Core.Hosting; -namespace Umbraco.Web +namespace Umbraco.Extensions { /// @@ -25,10 +25,10 @@ namespace Umbraco.Web /// used to cache the partial view, this key could change if it is cached by page or by member /// /// - public static IHtmlString CachedPartialView( + public static IHtmlContent CachedPartialView( this AppCaches appCaches, IHostingEnvironment hostingEnvironment, - HtmlHelper htmlHelper, + IHtmlHelper htmlHelper, string partialViewName, object model, int cachedSeconds, @@ -42,7 +42,7 @@ namespace Umbraco.Web return htmlHelper.Partial(partialViewName, model, viewData); } - return appCaches.RuntimeCache.GetCacheItem( + return appCaches.RuntimeCache.GetCacheItem( Core.CacheHelperExtensions.PartialViewCacheKey + cacheKey, () => htmlHelper.Partial(partialViewName, model, viewData), timeout: new TimeSpan(0, 0, 0, cachedSeconds)); diff --git a/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs b/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs new file mode 100644 index 0000000000..884e2bbdbc --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Mvc.Razor; + +namespace Umbraco.Extensions +{ + public static class RazorPageExtensions + { + public static HtmlString RenderSection(this RazorPage webPage, string name, HtmlString defaultContents) + { + return webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : defaultContents; + } + + public static HtmlString RenderSection(this RazorPage webPage, string name, string defaultContents) + { + return webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : new HtmlString(defaultContents); + } + + } +} diff --git a/src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs new file mode 100644 index 0000000000..17a9d048fc --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Umbraco.Extensions +{ + public static class ViewContextExtensions + { + /// + /// Creates a new ViewContext from an existing one but specifies a new Model for the ViewData + /// + /// + /// + /// + public static ViewContext CopyWithModel(this ViewContext vc, object model) + { + return new ViewContext + { + View = vc.View, + Writer = vc.Writer, + ActionDescriptor = vc.ActionDescriptor, + FormContext = vc.FormContext, + HttpContext = vc.HttpContext, + RouteData = vc.RouteData, + TempData = vc.TempData, + ViewData = new ViewDataDictionary(vc.ViewData) + { + Model = model + }, + ClientValidationEnabled = vc.ClientValidationEnabled, + ExecutingFilePath = vc.ExecutingFilePath, + ValidationMessageElement = vc.ValidationMessageElement, + Html5DateRenderingMode = vc.Html5DateRenderingMode, + ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement + }; + } + + public static ViewContext Clone(this ViewContext vc) + { + return new ViewContext + { + View = vc.View, + Writer = vc.Writer, + ActionDescriptor = vc.ActionDescriptor, + FormContext = vc.FormContext, + HttpContext = vc.HttpContext, + RouteData = vc.RouteData, + TempData = vc.TempData, + ViewData = new ViewDataDictionary(vc.ViewData), + ClientValidationEnabled = vc.ClientValidationEnabled, + ExecutingFilePath = vc.ExecutingFilePath, + ValidationMessageElement = vc.ValidationMessageElement, + Html5DateRenderingMode = vc.Html5DateRenderingMode, + ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement + }; + } + + //public static ViewContext CloneWithWriter(this ViewContext vc, TextWriter writer) + //{ + // return new ViewContext + // { + // Controller = vc.Controller, + // HttpContext = vc.HttpContext, + // RequestContext = vc.RequestContext, + // RouteData = vc.RouteData, + // TempData = vc.TempData, + // View = vc.View, + // ViewData = vc.ViewData, + // FormContext = vc.FormContext, + // ClientValidationEnabled = vc.ClientValidationEnabled, + // UnobtrusiveJavaScriptEnabled = vc.UnobtrusiveJavaScriptEnabled, + // Writer = writer + // }; + //} + } +} diff --git a/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs similarity index 66% rename from src/Umbraco.Web.Common/Filters/UmbracoAuthorizeAttribute.cs rename to src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs index 8a7c7b04d5..1f4abbaa25 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs @@ -5,12 +5,12 @@ namespace Umbraco.Web.Common.Filters /// /// Ensures authorization is successful for a back office user. /// - public class UmbracoAuthorizeAttribute : TypeFilterAttribute + public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute { /// /// Default constructor /// - public UmbracoAuthorizeAttribute() : this(false, false) + public UmbracoBackOfficeAuthorizeAttribute() : this(false, false) { } @@ -20,7 +20,7 @@ namespace Umbraco.Web.Common.Filters /// /// - public UmbracoAuthorizeAttribute(bool redirectToUmbracoLogin, bool requireApproval) : base(typeof(UmbracoAuthorizeFilter)) + public UmbracoBackOfficeAuthorizeAttribute(bool redirectToUmbracoLogin, bool requireApproval) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) { Arguments = new object[] { redirectToUmbracoLogin, requireApproval }; } @@ -29,7 +29,7 @@ namespace Umbraco.Web.Common.Filters /// Constructor with redirect url behavior /// /// - public UmbracoAuthorizeAttribute(string redirectUrl) : base(typeof(UmbracoAuthorizeFilter)) + public UmbracoBackOfficeAuthorizeAttribute(string redirectUrl) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) { Arguments = new object[] { redirectUrl }; } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs similarity index 94% rename from src/Umbraco.Web.Common/Filters/UmbracoAuthorizeFilter.cs rename to src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs index 66b1462ae9..e7889cd1a8 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.Common.Filters /// /// Ensures authorization is successful for a back office user. /// - public class UmbracoAuthorizeFilter : IAuthorizationFilter + public class UmbracoBackOfficeAuthorizeFilter : IAuthorizationFilter { private readonly bool _requireApproval; @@ -28,7 +28,7 @@ namespace Umbraco.Web.Common.Filters private readonly bool _redirectToUmbracoLogin; private string _redirectUrl; - private UmbracoAuthorizeFilter( + private UmbracoBackOfficeAuthorizeFilter( IHostingEnvironment hostingEnvironment, IUmbracoContextAccessor umbracoContext, IRuntimeState runtimeState, LinkGenerator linkGenerator, @@ -51,7 +51,7 @@ namespace Umbraco.Web.Common.Filters /// /// /// - public UmbracoAuthorizeFilter( + public UmbracoBackOfficeAuthorizeFilter( IHostingEnvironment hostingEnvironment, IUmbracoContextAccessor umbracoContext, IRuntimeState runtimeState, LinkGenerator linkGenerator, @@ -59,7 +59,7 @@ namespace Umbraco.Web.Common.Filters { } - public UmbracoAuthorizeFilter( + public UmbracoBackOfficeAuthorizeFilter( IHostingEnvironment hostingEnvironment, IUmbracoContextAccessor umbracoContext, IRuntimeState runtimeState, LinkGenerator linkGenerator, diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeAttribute.cs new file mode 100644 index 0000000000..cc6058121b --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeAttribute.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Filters +{ + /// + /// Ensures authorization is successful for a website user (member). + /// + public class UmbracoMemberAuthorizeAttribute : TypeFilterAttribute + { + public UmbracoMemberAuthorizeAttribute() : this(string.Empty, string.Empty, string.Empty) + { + } + + public UmbracoMemberAuthorizeAttribute(string allowType, string allowGroup, string allowMembers) : base(typeof(UmbracoMemberAuthorizeFilter)) + { + Arguments = new object[] { allowType, allowGroup, allowMembers}; + } + + } +} diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs new file mode 100644 index 0000000000..27c2922637 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Extensions; + +namespace Umbraco.Web.Common.Filters +{ + + /// + /// Ensures authorization is successful for a back office user. + /// + public class UmbracoMemberAuthorizeFilter : IAuthorizationFilter + { + /// + /// Comma delimited list of allowed member types + /// + public string AllowType { get; private set;} + + /// + /// Comma delimited list of allowed member groups + /// + public string AllowGroup { get; private set;} + + /// + /// Comma delimited list of allowed members + /// + public string AllowMembers { get; private set; } + + + private UmbracoMemberAuthorizeFilter( + string allowType, string allowGroup, string allowMembers) + { + AllowType = allowType; + AllowGroup = allowGroup; + AllowMembers = allowMembers; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!IsAuthorized()) + { + context.HttpContext.SetReasonPhrase("Resource restricted: either member is not logged on or is not of a permitted type or group."); + context.Result = new ForbidResult(); + } + } + + private bool IsAuthorized() + { + if (AllowMembers.IsNullOrWhiteSpace()) + AllowMembers = ""; + if (AllowGroup.IsNullOrWhiteSpace()) + AllowGroup = ""; + if (AllowType.IsNullOrWhiteSpace()) + AllowType = ""; + + var members = new List(); + foreach (var s in AllowMembers.Split(',')) + { + if (int.TryParse(s, out var id)) + { + members.Add(id); + } + } + + return false;// TODO reintroduce when members are implemented: _memberHelper.IsMemberAuthorized(AllowType.Split(','), AllowGroup.Split(','), members); + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs new file mode 100644 index 0000000000..45806b9d18 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core; +using Umbraco.Web.Common.Constants; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Common.Security; + +namespace Umbraco.Web.Common.Filters +{ + + /// + /// Attribute used to check that the request contains a valid Umbraco form request string. + /// + /// + /// Applying this attribute/filter to a or SurfaceController Action will ensure that the Action can only be executed + /// when it is routed to from within Umbraco, typically when rendering a form with BeginUmbracoForm. It will mean that the natural MVC route for this Action + /// will fail with a . + /// + public class ValidateUmbracoFormRouteStringAttribute : TypeFilterAttribute + { + + public ValidateUmbracoFormRouteStringAttribute() : base(typeof(ValidateUmbracoFormRouteStringFilter)) + { + Arguments = new object[] { }; + } + + internal class ValidateUmbracoFormRouteStringFilter: IAuthorizationFilter + { + private readonly IDataProtectionProvider _dataProtectionProvider; + + public ValidateUmbracoFormRouteStringFilter(IDataProtectionProvider dataProtectionProvider) + { + _dataProtectionProvider = dataProtectionProvider; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + + var ufprt = context.HttpContext.Request.Form["ufprt"]; + + if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + { + ValidateRouteString(ufprt, controllerActionDescriptor.ControllerName, controllerActionDescriptor.ActionName, context.RouteData?.DataTokens["area"]?.ToString()); + } + + } + + public void ValidateRouteString(string ufprt, string currentController, string currentAction, string currentArea) + { + if (ufprt.IsNullOrWhiteSpace()) + { + throw new HttpUmbracoFormRouteStringException("The required request field \"ufprt\" is not present."); + } + + if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString(_dataProtectionProvider, ufprt, out var additionalDataParts)) + { + throw new HttpUmbracoFormRouteStringException("The Umbraco form request route string could not be decrypted."); + } + + if (!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Controller].InvariantEquals(currentController) || + !additionalDataParts[ViewConstants.ReservedAdditionalKeys.Action].InvariantEquals(currentAction) || + (!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].InvariantEquals(currentArea))) + { + throw new HttpUmbracoFormRouteStringException("The provided Umbraco form request route string was meant for a different controller and action."); + } + + } + } + } +} diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs similarity index 97% rename from src/Umbraco.Web/HtmlStringUtilities.cs rename to src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs index cbeff72acf..204bb61425 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs @@ -5,8 +5,9 @@ using System.Linq; using System.Text; using System.Web; using HtmlAgilityPack; +using Microsoft.AspNetCore.Html; -namespace Umbraco.Web +namespace Umbraco.Web.Common.Mvc { /// /// Provides utility methods for UmbracoHelper for working with strings and HTML in views. @@ -20,7 +21,7 @@ namespace Umbraco.Web /// /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). /// - public IHtmlString ReplaceLineBreaks(string text) + public IHtmlContent ReplaceLineBreaks(string text) { var value = HttpUtility.HtmlEncode(text)? .Replace("\r\n", "
") @@ -63,7 +64,7 @@ namespace Umbraco.Web return new HtmlString(doc.DocumentNode.FirstChild.InnerHtml.Replace(" ", " ")); } - internal string Join(string separator, params object[] args) + public string Join(string separator, params object[] args) { var results = args .Where(x => x != null) @@ -72,7 +73,7 @@ namespace Umbraco.Web return string.Join(separator, results); } - internal string Concatenate(params object[] args) + public string Concatenate(params object[] args) { var sb = new StringBuilder(); foreach (var arg in args @@ -85,7 +86,7 @@ namespace Umbraco.Web return sb.ToString(); } - internal string Coalesce(params object[] args) + public string Coalesce(params object[] args) { var arg = args .Where(x => x != null) @@ -95,7 +96,7 @@ namespace Umbraco.Web return arg ?? string.Empty; } - public IHtmlString Truncate(string html, int length, bool addElipsis, bool treatTagsAsContent) + public IHtmlContent Truncate(string html, int length, bool addElipsis, bool treatTagsAsContent) { const string hellip = "…"; diff --git a/src/Umbraco.Web.Common/Security/EncryptionHelper.cs b/src/Umbraco.Web.Common/Security/EncryptionHelper.cs new file mode 100644 index 0000000000..300afd530d --- /dev/null +++ b/src/Umbraco.Web.Common/Security/EncryptionHelper.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Web; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Web.Common.Constants; + +namespace Umbraco.Web.Common.Security +{ + public class EncryptionHelper + { + private static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider) + { + return dataProtectionProvider.CreateProtector(nameof(EncryptionHelper)); + } + + public static string Decrypt(string encryptedString, IDataProtectionProvider dataProtectionProvider) + { + return CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString); + } + + public static string Encrypt(string plainString, IDataProtectionProvider dataProtectionProvider) + { + return CreateDataProtector(dataProtectionProvider).Protect(plainString); + } + + /// + /// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which + /// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller. + /// + /// + /// + /// + /// + /// + /// + public static string CreateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string controllerName, string controllerAction, string area, object additionalRouteVals = null) + { + if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider)); + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); + if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); + if (controllerAction == null) throw new ArgumentNullException(nameof(controllerAction)); + if (string.IsNullOrEmpty(controllerAction)) throw new ArgumentException("Value can't be empty.", nameof(controllerAction)); + if (area == null) throw new ArgumentNullException(nameof(area)); + + //need to create a params string as Base64 to put into our hidden field to use during the routes + var surfaceRouteParams = $"{ViewConstants.ReservedAdditionalKeys.Controller}={WebUtility.UrlEncode(controllerName)}&{ViewConstants.ReservedAdditionalKeys.Action}={WebUtility.UrlEncode(controllerAction)}&{ViewConstants.ReservedAdditionalKeys.Area}={area}"; + + //checking if the additional route values is already a dictionary and convert to querystring + string additionalRouteValsAsQuery; + if (additionalRouteVals != null) + { + if (additionalRouteVals is Dictionary additionalRouteValsAsDictionary) + additionalRouteValsAsQuery = additionalRouteValsAsDictionary.ToQueryString(); + else + additionalRouteValsAsQuery = additionalRouteVals.ToDictionary().ToQueryString(); + } + else + additionalRouteValsAsQuery = null; + + if (additionalRouteValsAsQuery.IsNullOrWhiteSpace() == false) + surfaceRouteParams += "&" + additionalRouteValsAsQuery; + + return Encrypt(surfaceRouteParams, dataProtectionProvider); + } + + public static bool DecryptAndValidateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string encryptedString, out IDictionary parts) + { + if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider)); + string decryptedString; + try + { + decryptedString = Decrypt(encryptedString, dataProtectionProvider); + } + catch (Exception) + { + StaticApplicationLogging.Logger.LogWarning("A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); + parts = null; + return false; + } + var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); + parts = new Dictionary(); + foreach (var key in parsedQueryString.AllKeys) + { + parts[key] = parsedQueryString[key]; + } + //validate all required keys exist + //the controller + if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Controller)) + return false; + //the action + if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Action)) + return false; + //the area + if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Area)) + return false; + + return true; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index 2c81f3ddf3..eee933b561 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -17,11 +17,11 @@ -
+

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

-
+
What is the exact connection string we should use?
@@ -32,7 +32,7 @@
-
+
Where do we find your database?
@@ -40,7 +40,7 @@
Enter server domain or IP
@@ -86,7 +86,7 @@
-
+