diff --git a/build/build.ps1 b/build/build.ps1 index 3ba347a6dc..185d1b8ff6 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -438,14 +438,11 @@ Write-Host "Prepare C# Documentation" $src = "$($this.SolutionRoot)\src" - $tmp = $this.BuildTemp - $out = $this.BuildOutput + $tmp = $this.BuildTemp + $out = $this.BuildOutput $DocFxJson = Join-Path -Path $src "\ApiDocs\docfx.json" $DocFxSiteOutput = Join-Path -Path $tmp "\_site\*.*" - - #restore nuget packages - $this.RestoreNuGet() # run DocFx $DocFx = $this.BuildEnv.DocFx @@ -463,13 +460,18 @@ $src = "$($this.SolutionRoot)\src" $out = $this.BuildOutput + # Check if the solution has been built + if (!(Test-Path "$src\Umbraco.Web.UI.Client\node_modules")) {throw "Umbraco needs to be built before generating the Angular Docs"} + "Moving to Umbraco.Web.UI.Docs folder" - cd ..\src\Umbraco.Web.UI.Docs + cd $src\Umbraco.Web.UI.Docs "Generating the docs and waiting before executing the next commands" & npm install & npx gulp docs + Pop-Location + # change baseUrl $BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/" $IndexPath = "./api/index.html" diff --git a/src/Umbraco.Configuration/Models/GlobalSettings.cs b/src/Umbraco.Configuration/Models/GlobalSettings.cs index e4995cfb0b..908ba71590 100644 --- a/src/Umbraco.Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Configuration/Models/GlobalSettings.cs @@ -39,7 +39,7 @@ namespace Umbraco.Configuration.Models } public int TimeOutInMinutes => _configuration.GetValue(Prefix + "TimeOutInMinutes", 20); - public string DefaultUILanguage => _configuration.GetValue(Prefix + "TimeOutInMinutes", "en-US"); + public string DefaultUILanguage => _configuration.GetValue(Prefix + "DefaultUILanguage", "en-US"); public bool HideTopLevelNodeFromPath => _configuration.GetValue(Prefix + "HideTopLevelNodeFromPath", false); diff --git a/src/Umbraco.Configuration/Models/SecuritySettings.cs b/src/Umbraco.Configuration/Models/SecuritySettings.cs index 9244eace96..297c95b1af 100644 --- a/src/Umbraco.Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Configuration/Models/SecuritySettings.cs @@ -14,7 +14,7 @@ namespace Umbraco.Configuration.Models _configuration = configuration; } - public bool KeepUserLoggedIn => _configuration.GetValue(Prefix + "KeepUserLoggedIn", true); + public bool KeepUserLoggedIn => _configuration.GetValue(Prefix + "KeepUserLoggedIn", false); public bool HideDisabledUsersInBackoffice => _configuration.GetValue(Prefix + "HideDisabledUsersInBackoffice", false); diff --git a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs index 15db0e0dc7..93acb279dc 100644 --- a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs @@ -36,7 +36,6 @@ namespace Umbraco.Core.BackOffice private UmbracoBackOfficeIdentity(ClaimsIdentity identity) : base(identity.Claims, Constants.Security.BackOfficeAuthenticationType) { - Actor = identity; } /// @@ -89,7 +88,7 @@ namespace Umbraco.Core.BackOffice if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); - Actor = childIdentity; + AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles); } @@ -204,5 +203,18 @@ namespace Umbraco.Core.BackOffice public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray(); + /// + /// Overridden to remove any temporary claims that shouldn't be copied + /// + /// + public override ClaimsIdentity Clone() + { + var clone = base.Clone(); + + foreach (var claim in clone.FindAll(x => x.Type == Constants.Security.TicketExpiresClaimType).ToList()) + clone.RemoveClaim(claim); + + return clone; + } } } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 3551aa1c31..94370930de 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -9,6 +9,8 @@ namespace Umbraco.Core /// public static class AppSettings { + // TODO: Are these all obsolete in netcore now? + public const string MainDomLock = "Umbraco.Core.MainDom.Lock"; // TODO: Kill me - still used in Umbraco.Core.IO.SystemFiles:27 diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 7e96c6a912..d18e23a6c3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -48,6 +48,7 @@ public static class Mvc { public const string InstallArea = "UmbracoInstall"; + public const string BackOfficePathSegment = "BackOffice"; // The path segment prefix for all back office controllers public const string BackOfficeArea = "UmbracoBackOffice"; // Used for area routes of non-api controllers public const string BackOfficeApiArea = "UmbracoApi"; // Same name as v8 so all routing remains the same } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 89ae269469..aa9d6174dd 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -13,7 +13,6 @@ namespace Umbraco.Core.IO public class PhysicalFileSystem : IFileSystem { private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; private readonly ILogger _logger; // the rooted, filesystem path, using directory separator chars, NOT ending with a separator @@ -34,19 +33,19 @@ namespace Umbraco.Core.IO public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILogger logger, string virtualRoot) { _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + if (hostingEnvironment == null) throw new ArgumentNullException(nameof(hostingEnvironment)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - if (virtualRoot == null) throw new ArgumentNullException("virtualRoot"); + if (virtualRoot == null) throw new ArgumentNullException(nameof(virtualRoot)); if (virtualRoot.StartsWith("~/") == false) throw new ArgumentException("The virtualRoot argument must be a virtual path and start with '~/'"); - _rootPath = EnsureDirectorySeparatorChar(_hostingEnvironment.MapPathContentRoot(virtualRoot)).TrimEnd(Path.DirectorySeparatorChar); + _rootPath = EnsureDirectorySeparatorChar(hostingEnvironment.MapPathContentRoot(virtualRoot)).TrimEnd(Path.DirectorySeparatorChar); _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); - _rootUrl = EnsureUrlSeparatorChar(_ioHelper.ResolveUrl(virtualRoot)).TrimEnd('/'); + _rootUrl = EnsureUrlSeparatorChar(hostingEnvironment.ToAbsolute(virtualRoot)).TrimEnd('/'); } - public PhysicalFileSystem(IIOHelper ioHelper, ILogger logger, string rootPath, string rootUrl) + public PhysicalFileSystem(IIOHelper ioHelper,IHostingEnvironment hostingEnvironment, ILogger logger, string rootPath, string rootUrl) { _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -62,8 +61,7 @@ namespace Umbraco.Core.IO if (Path.IsPathRooted(rootPath) == false) { // but the test suite App.config cannot really "root" anything so we have to do it here - // TODO: This will map to the web content root (www) not the web app root, is that what we want? Else we need to use IHostingEnvironment.ApplicationPhysicalPath - var localRoot = _ioHelper.MapPath("~"); + var localRoot = hostingEnvironment.MapPathContentRoot("~"); rootPath = Path.Combine(localRoot, rootPath); } diff --git a/src/Umbraco.Core/Net/IUmbracoComponentRenderer.cs b/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs similarity index 98% rename from src/Umbraco.Core/Net/IUmbracoComponentRenderer.cs rename to src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs index 0391a01183..cc75ff706c 100644 --- a/src/Umbraco.Core/Net/IUmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Templates/IUmbracoComponentRenderer.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Umbraco.Core.Strings; -namespace Umbraco.Core.Net +namespace Umbraco.Core.Templates { /// /// Methods used to render umbraco components as HTML in templates diff --git a/src/Umbraco.Core/Net/UmbracoComponentRenderer.cs b/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs similarity index 98% rename from src/Umbraco.Core/Net/UmbracoComponentRenderer.cs rename to src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs index 22703a111e..27c298d54f 100644 --- a/src/Umbraco.Core/Net/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Core/Templates/UmbracoComponentRenderer.cs @@ -2,7 +2,6 @@ using System.Linq; using System.IO; using System.Web; -using Umbraco.Core; using Umbraco.Web.Templates; using System.Collections.Generic; using Umbraco.Core.Models.PublishedContent; @@ -10,7 +9,7 @@ using Umbraco.Core.Strings; using Umbraco.Web; using Umbraco.Web.Macros; -namespace Umbraco.Core.Net +namespace Umbraco.Core.Templates { /// @@ -19,7 +18,6 @@ namespace Umbraco.Core.Net /// /// Used by UmbracoHelper /// - // Migrated to .NET Core public class UmbracoComponentRenderer : IUmbracoComponentRenderer { private readonly IUmbracoContextAccessor _umbracoContextAccessor; diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 8fd0e365e7..8f0c7beff8 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -76,13 +76,13 @@ namespace Umbraco.Core } //check for special back office paths - if (urlPath.InvariantStartsWith("/" + mvcArea + "/BackOffice/") - || urlPath.InvariantStartsWith("/" + mvcArea + "/Preview/")) + if (urlPath.InvariantStartsWith("/" + mvcArea + "/" + Constants.Web.Mvc.BackOfficePathSegment + "/")) { return true; } //check for special front-end paths + // TODO: These should be constants - will need to update when we do front-end routing if (urlPath.InvariantStartsWith("/" + mvcArea + "/Surface/") || urlPath.InvariantStartsWith("/" + mvcArea + "/Api/")) { diff --git a/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs b/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs index a64bf15b7f..c21b09523d 100644 --- a/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs @@ -29,5 +29,7 @@ namespace Umbraco.Web.Models public string EventElement { get; set; } [DataMember(Name = "customProperties")] public JObject CustomProperties { get; set; } + [DataMember(Name = "skipStepIfVisible")] + public string SkipStepIfVisible { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 1c37968a6a..2843236252 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -18,7 +18,7 @@ using Umbraco.Core.Migrations; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Migrations.PostMigrations; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Net; +using Umbraco.Core.Templates; using Umbraco.Core.Persistence; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.Validators; @@ -40,7 +40,6 @@ using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.HealthCheck.NotificationMethods; using Umbraco.Web.Install; -using Umbraco.Web.Macros; using Umbraco.Web.Media.EmbedProviders; using Umbraco.Web.Migrations.PostMigrations; using Umbraco.Web.Models.PublishedContent; diff --git a/src/Umbraco.Tests.Common/TestClone.cs b/src/Umbraco.Tests.Common/TestClone.cs new file mode 100644 index 0000000000..5fd0aa30e2 --- /dev/null +++ b/src/Umbraco.Tests.Common/TestClone.cs @@ -0,0 +1,77 @@ +using System; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Common +{ + public class TestClone : IDeepCloneable, IEquatable + { + public TestClone(Guid id) + { + Id = id; + IsClone = true; + } + + public TestClone() + { + Id = Guid.NewGuid(); + } + + public Guid Id { get; } + public bool IsClone { get; } + + public object DeepClone() + { + return new TestClone(Id); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(TestClone other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((TestClone)obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public static bool operator ==(TestClone left, TestClone right) + { + return Equals(left, right); + } + + public static bool operator !=(TestClone left, TestClone right) + { + return Equals(left, right) == false; + } + } +} diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice.Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice.Security/BackOfficeCookieManagerTests.cs new file mode 100644 index 0000000000..3464259052 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice.Security/BackOfficeCookieManagerTests.cs @@ -0,0 +1,172 @@ + + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Moq; +using NUnit.Framework; +using System; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Hosting; +using Umbraco.Extensions; +using Umbraco.Tests.Integration.Implementations; +using Umbraco.Web; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.BackOffice.Security; + +namespace Umbraco.Tests.Security +{ + [TestFixture] + public class BackOfficeCookieManagerTests + { + [Test] + public void ShouldAuthenticateRequest_When_Not_Configured() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(), + globalSettings, + Mock.Of(), + Mock.Of()); + + var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco")); + + Assert.IsFalse(result); + } + + [Test] + public void ShouldAuthenticateRequest_When_Configured() + { + var testHelper = new TestHelper(); + + + //hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"), + globalSettings, + Mock.Of(), + Mock.Of()); + + var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco")); + + Assert.IsTrue(result); + } + + [Test] + public void ShouldAuthenticateRequest_Is_Back_Office() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + globalSettings, + Mock.Of(), + GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + + var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{remainingTimeoutSecondsPath}")); + Assert.IsTrue(result); + + result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{isAuthPath}")); + Assert.IsTrue(result); + } + + [Test] + public void ShouldAuthenticateRequest_Force_Auth() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + globalSettings, + Mock.Of(x => x.IsAvailable == true && x.Get(Constants.Security.ForceReAuthFlag) == "not null"), + GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + + var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice")); + Assert.IsTrue(result); + } + + [Test] + public void ShouldAuthenticateRequest_Not_Back_Office() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + globalSettings, + Mock.Of(), + GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + + var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice")); + Assert.IsFalse(result); + result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/umbraco/api/notbackoffice")); + Assert.IsFalse(result); + result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/umbraco/surface/notbackoffice")); + Assert.IsFalse(result); + } + + private LinkGenerator GetMockLinkGenerator(out string remainingTimeoutSecondsPath, out string isAuthPath) + { + var controllerName = ControllerExtensions.GetControllerName(); + + // this path is not a back office request even though it's in the same controller - it's a 'special' endpoint + var rPath = remainingTimeoutSecondsPath = $"/umbraco/{Constants.Web.Mvc.BackOfficePathSegment}/{Constants.Web.Mvc.BackOfficeApiArea}/{controllerName}/{nameof(AuthenticationController.GetRemainingTimeoutSeconds)}".ToLower(); + + // this is on the same controller but is considered a back office request + var aPath = isAuthPath = $"/umbraco/{Constants.Web.Mvc.BackOfficePathSegment}/{Constants.Web.Mvc.BackOfficeApiArea}/{controllerName}/{nameof(AuthenticationController.IsAuthenticated)}".ToLower(); + + var linkGenerator = new Mock(); + linkGenerator.Setup(x => x.GetPathByAddress( + //It.IsAny(), + It.IsAny(), + //It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns((RouteValuesAddress address, RouteValueDictionary routeVals1, PathString path, FragmentString fragment, LinkOptions options) => + { + if (routeVals1["action"].ToString() == nameof(AuthenticationController.GetRemainingTimeoutSeconds)) + return rPath; + if (routeVals1["action"].ToString() == nameof(AuthenticationController.IsAuthenticated).ToLower()) + return aPath; + return null; + }); + + return linkGenerator.Object; + } + } +} diff --git a/src/Umbraco.Tests/CoreThings/AttemptTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/AttemptTests.cs similarity index 92% rename from src/Umbraco.Tests/CoreThings/AttemptTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/AttemptTests.cs index a004095243..9a16c5b10f 100644 --- a/src/Umbraco.Tests/CoreThings/AttemptTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/AttemptTests.cs @@ -1,12 +1,11 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class AttemptTests { - [Test] public void AttemptIf() { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index 1aa1010458..87cff07475 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -71,7 +71,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice } [TestCase(ClaimTypes.NameIdentifier, _testUserId)] - [TestCase(ClaimTypes.Name, _testUserName)] + [TestCase(ClaimTypes.Name, _testUserName)] public async Task CreateAsync_Should_Include_Claim(string expectedClaimType, object expectedClaimValue) { var sut = CreateSut(); @@ -79,7 +79,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var claimsPrincipal = await sut.CreateAsync(_testUser); Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue.ToString())); - Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue.ToString())); + Assert.True(claimsPrincipal.GetUmbracoIdentity().HasClaim(expectedClaimType, expectedClaimValue.ToString())); } [Test] @@ -94,9 +94,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var sut = CreateSut(); var claimsPrincipal = await sut.CreateAsync(_testUser); - + Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue)); - Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue)); + Assert.True(claimsPrincipal.GetUmbracoIdentity().HasClaim(expectedClaimType, expectedClaimValue)); } [Test] @@ -112,7 +112,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var sut = CreateSut(); var claimsPrincipal = await sut.CreateAsync(_testUser); - + Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue)); } @@ -131,7 +131,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var claimsPrincipal = await sut.CreateAsync(_testUser); - Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue)); + Assert.True(claimsPrincipal.GetUmbracoIdentity().HasClaim(expectedClaimType, expectedClaimValue)); } [SetUp] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs index 47e1261c09..9e9d29a123 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs @@ -37,6 +37,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice if (!UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity)) Assert.Fail(); + Assert.IsNull(backofficeIdentity.Actor); Assert.AreEqual(1234, backofficeIdentity.Id); //Assert.AreEqual(sessionId, backofficeIdentity.SessionId); Assert.AreEqual(securityStamp, backofficeIdentity.SecurityStamp); @@ -60,7 +61,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer), }); - if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity)) + if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out _)) Assert.Fail(); Assert.Pass(); @@ -83,7 +84,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin", ClaimValueTypes.String, TestIssuer, TestIssuer), }); - if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity)) + if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out _)) Assert.Fail(); Assert.Pass(); @@ -105,6 +106,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); Assert.AreEqual(12, identity.Claims.Count()); + Assert.IsNull(identity.Actor); } @@ -116,7 +118,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var identity = new UmbracoBackOfficeIdentity( 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); + // this will be filtered out during cloning + identity.AddClaim(new Claim(Constants.Security.TicketExpiresClaimType, "test")); + var cloned = identity.Clone(); + Assert.IsNull(cloned.Actor); Assert.AreEqual(10, cloned.Claims.Count()); } diff --git a/src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ClaimsIdentityExtensionsTests.cs similarity index 96% rename from src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/ClaimsIdentityExtensionsTests.cs index 4f728c3861..76f928ca46 100644 --- a/src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ClaimsIdentityExtensionsTests.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using System.Security.Claims; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Web; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { public class ClaimsIdentityExtensionsTests { diff --git a/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/DeepCloneableListTests.cs similarity index 53% rename from src/Umbraco.Tests/Collections/DeepCloneableListTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/DeepCloneableListTests.cs index 37e5b2081d..bcfe142d8d 100644 --- a/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/DeepCloneableListTests.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Linq; using NUnit.Framework; using Umbraco.Core.Collections; -using Umbraco.Core.Models; +using Umbraco.Tests.Common; -namespace Umbraco.Tests.Collections +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Collections { [TestFixture] public class DeepCloneableListTests @@ -85,7 +81,7 @@ namespace Umbraco.Tests.Collections list.Add(new TestClone()); list.Add(new TestClone()); - var cloned = (DeepCloneableList)list.DeepClone(); + var cloned = (DeepCloneableList) list.DeepClone(); //Test that each item in the sequence is equal - based on the equality comparer of TestClone (i.e. it's ID) Assert.IsTrue(list.SequenceEqual(cloned)); @@ -97,77 +93,5 @@ namespace Umbraco.Tests.Collections Assert.AreNotSame(item, clone); } } - - public class TestClone : IDeepCloneable, IEquatable - { - public TestClone(Guid id) - { - Id = id; - IsClone = true; - } - - public TestClone() - { - Id = Guid.NewGuid(); - } - - public Guid Id { get; private set; } - public bool IsClone { get; private set; } - - public object DeepClone() - { - return new TestClone(Id); - } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// - /// true if the current object is equal to the parameter; otherwise, false. - /// - /// An object to compare with this object. - public bool Equals(TestClone other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id.Equals(other.Id); - } - - /// - /// Determines whether the specified object is equal to the current object. - /// - /// - /// true if the specified object is equal to the current object; otherwise, false. - /// - /// The object to compare with the current object. - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((TestClone)obj); - } - - /// - /// Serves as the default hash function. - /// - /// - /// A hash code for the current object. - /// - public override int GetHashCode() - { - return Id.GetHashCode(); - } - - public static bool operator ==(TestClone left, TestClone right) - { - return Equals(left, right); - } - - public static bool operator !=(TestClone left, TestClone right) - { - return Equals(left, right) == false; - } - } } } diff --git a/src/Umbraco.Tests/Collections/OrderedHashSetTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/OrderedHashSetTests.cs similarity index 87% rename from src/Umbraco.Tests/Collections/OrderedHashSetTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/OrderedHashSetTests.cs index a98348751b..df7f004884 100644 --- a/src/Umbraco.Tests/Collections/OrderedHashSetTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/OrderedHashSetTests.cs @@ -1,9 +1,8 @@ using System; using NUnit.Framework; -using Umbraco.Core; using Umbraco.Core.Collections; -namespace Umbraco.Tests.Collections +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Collections { [TestFixture] public class OrderedHashSetTests @@ -12,7 +11,7 @@ namespace Umbraco.Tests.Collections public void Keeps_Last() { var list = new OrderedHashSet(keepOldest: false); - var items = new[] { new MyClass("test"), new MyClass("test"), new MyClass("test") }; + var items = new[] {new MyClass("test"), new MyClass("test"), new MyClass("test")}; foreach (var item in items) { list.Add(item); @@ -27,7 +26,7 @@ namespace Umbraco.Tests.Collections public void Keeps_First() { var list = new OrderedHashSet(keepOldest: true); - var items = new[] { new MyClass("test"), new MyClass("test"), new MyClass("test") }; + var items = new[] {new MyClass("test"), new MyClass("test"), new MyClass("test")}; foreach (var item in items) { list.Add(item); @@ -60,7 +59,7 @@ namespace Umbraco.Tests.Collections if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((MyClass)obj); + return Equals((MyClass) obj); } public override int GetHashCode() diff --git a/src/Umbraco.Tests/CoreThings/DelegateExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/DelegateExtensionsTests.cs similarity index 95% rename from src/Umbraco.Tests/CoreThings/DelegateExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/DelegateExtensionsTests.cs index 87b2f4c03b..a3e36c8ae6 100644 --- a/src/Umbraco.Tests/CoreThings/DelegateExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/DelegateExtensionsTests.cs @@ -3,7 +3,7 @@ using Lucene.Net.Index; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class DelegateExtensionsTests diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumExtensionsTests.cs similarity index 97% rename from src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumExtensionsTests.cs index faa15b0077..d5ea4d2677 100644 --- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumExtensionsTests.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Web.Trees; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class EnumExtensionsTests diff --git a/src/Umbraco.Tests/CoreThings/EnumerableExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumerableExtensionsTests.cs similarity index 98% rename from src/Umbraco.Tests/CoreThings/EnumerableExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumerableExtensionsTests.cs index e734713c76..32e039f26a 100644 --- a/src/Umbraco.Tests/CoreThings/EnumerableExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumerableExtensionsTests.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class EnumerableExtensionsTests diff --git a/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs similarity index 95% rename from src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs index 639d85b4ff..62e0955d78 100644 --- a/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { public class GuidUtilsTests { diff --git a/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/HexEncoderTests.cs similarity index 97% rename from src/Umbraco.Tests/CoreThings/HexEncoderTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/HexEncoderTests.cs index 588fff83e8..f22c3f2ac1 100644 --- a/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/HexEncoderTests.cs @@ -3,7 +3,7 @@ using System.Text; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { public class HexEncoderTests { diff --git a/src/Umbraco.Tests/Clr/ReflectionTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionTests.cs similarity index 54% rename from src/Umbraco.Tests/Clr/ReflectionTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionTests.cs index f3704c8552..063a8c2621 100644 --- a/src/Umbraco.Tests/Clr/ReflectionTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.Clr +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class ReflectionTests @@ -25,37 +25,16 @@ namespace Umbraco.Tests.Clr Assert.Contains(typeof(object), types); } - [Test] - public void GetInterfacesIsOk() - { - // tests that GetInterfaces gets _all_ interfaces - // so the AllInterfaces extension method is useless - - var type = typeof(Class2); - var interfaces = type.GetInterfaces(); - Assert.AreEqual(2, interfaces.Length); - Assert.Contains(typeof(IInterface1), interfaces); - Assert.Contains(typeof(IInterface2), interfaces); - } - #region Test Objects - interface IInterface1 - { } - - interface IInterface2 : IInterface1 + private class Class1 { - void Method(); } - class Class1 : IInterface2 + private class Class2 : Class1 { - public void Method() { } } - class Class2 : Class1 - { } - #endregion } } diff --git a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionUtilitiesTests.cs similarity index 98% rename from src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionUtilitiesTests.cs index c885f364dc..0f48f2cea2 100644 --- a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionUtilitiesTests.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core; -using System.Linq; -using Newtonsoft.Json; -namespace Umbraco.Tests.Clr +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class ReflectionUtilitiesTests @@ -104,8 +104,6 @@ namespace Umbraco.Tests.Clr [Test] public void EmitMethodEmitsStaticStatic() { - // static types cannot be used as type arguments - //var method = ReflectionUtilities.EmitMethod("Method"); var method = ReflectionUtilities.EmitMethod(typeof (StaticClass1), "Method"); method(); } @@ -205,10 +203,6 @@ namespace Umbraco.Tests.Clr (var getter3, var setter3) = ReflectionUtilities.EmitPropertyGetterAndSetter("Value3"); Assert.AreEqual(42, getter3(class1)); setter3(class1, 42); - - // this is not supported yet - //var getter4 = ReflectionUtilities.EmitPropertyGetter("Value1", returned: typeof(int)); - //Assert.AreEqual(42, getter1(class1)); } [Test] @@ -448,7 +442,7 @@ namespace Umbraco.Tests.Clr var propInt4 = type4.GetProperty("IntValue"); Assert.IsNotNull(propInt4); - // ... if explicitely getting a value type + // ... if explicitly getting a value type var getterInt4T = ReflectionUtilities.EmitPropertyGetter(propInt4); Assert.IsNotNull(getterInt4T); var valueInt4T = getterInt4T(object4); diff --git a/src/Umbraco.Tests/CoreThings/VersionExtensionTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/VersionExtensionTests.cs similarity index 86% rename from src/Umbraco.Tests/CoreThings/VersionExtensionTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/VersionExtensionTests.cs index 5799e28b87..a4ab15afe7 100644 --- a/src/Umbraco.Tests/CoreThings/VersionExtensionTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/VersionExtensionTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class VersionExtensionTests @@ -18,7 +18,7 @@ namespace Umbraco.Tests.CoreThings [TestCase(0, 0, 1, 1, "0.0.1.0")] [TestCase(0, 0, 0, 1, "0.0.0.0")] [TestCase(7, 3, 0, 0, "7.2.2147483647.2147483647")] - public void Subract_Revision(int major, int minor, int build, int rev, string outcome) + public void Subtract_Revision(int major, int minor, int build, int rev, string outcome) { var version = new Version(major, minor, build, rev); diff --git a/src/Umbraco.Tests/CoreThings/XmlExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/XmlExtensionsTests.cs similarity index 96% rename from src/Umbraco.Tests/CoreThings/XmlExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/XmlExtensionsTests.cs index 62e385beff..ae3762ef57 100644 --- a/src/Umbraco.Tests/CoreThings/XmlExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/XmlExtensionsTests.cs @@ -3,7 +3,7 @@ using System.Xml.Linq; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class XmlExtensionsTests diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 20c05ca201..c358eabf27 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs b/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs index 63e481e9a5..f7b39590f2 100644 --- a/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs +++ b/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs @@ -12,7 +12,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Tests.Collections; +using Umbraco.Tests.Common; using Umbraco.Tests.TestHelpers; using Umbraco.Web.Cache; @@ -41,12 +41,12 @@ namespace Umbraco.Tests.Cache [Test] public void Clones_List() { - var original = new DeepCloneableList(ListCloneBehavior.Always); - original.Add(new DeepCloneableListTests.TestClone()); - original.Add(new DeepCloneableListTests.TestClone()); - original.Add(new DeepCloneableListTests.TestClone()); + var original = new DeepCloneableList(ListCloneBehavior.Always); + original.Add(new TestClone()); + original.Add(new TestClone()); + original.Add(new TestClone()); - var val = _provider.GetCacheItem>("test", () => original); + var val = _provider.GetCacheItem>("test", () => original); Assert.AreEqual(original.Count, val.Count); foreach (var item in val) diff --git a/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs b/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs index c25085f6b0..f1e705c9cb 100644 --- a/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/PhysicalFileSystemTests.cs @@ -16,7 +16,7 @@ namespace Umbraco.Tests.IO public class PhysicalFileSystemTests : AbstractFileSystemTests { public PhysicalFileSystemTests() - : base(new PhysicalFileSystem(TestHelper.IOHelper, Mock.Of(), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"), "/Media/")) + : base(new PhysicalFileSystem(TestHelper.IOHelper, TestHelper.GetHostingEnvironment(), Mock.Of(), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"), "/Media/")) { } [SetUp] diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index b8517d8263..07a04479a4 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -54,14 +54,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); Directory.CreateDirectory(path + "/ShadowTests/d1"); @@ -91,14 +92,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); Directory.CreateDirectory(path + "/ShadowTests/sub"); @@ -143,14 +145,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); File.WriteAllText(path + "/ShadowTests/f1.txt", "foo"); @@ -185,6 +188,7 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); @@ -192,8 +196,8 @@ namespace Umbraco.Tests.IO Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); Directory.CreateDirectory(path + "/ShadowTests/sub"); @@ -244,14 +248,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); Assert.Throws(() => @@ -266,14 +271,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper,hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); File.WriteAllText(path + "/ShadowTests/f2.txt", "foo"); @@ -308,14 +314,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) @@ -351,14 +358,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) @@ -376,14 +384,15 @@ namespace Umbraco.Tests.IO { var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); Directory.CreateDirectory(path + "/ShadowTests/sub/sub"); @@ -415,6 +424,7 @@ namespace Umbraco.Tests.IO { var logger = Mock.Of(); var ioHelper = TestHelper.IOHelper; + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); var shadowfs = ioHelper.MapPath(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"); @@ -423,7 +433,7 @@ namespace Umbraco.Tests.IO var scopedFileSystems = false; - var phy = new PhysicalFileSystem(ioHelper, logger, path, "ignore"); + var phy = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path, "ignore"); var container = Mock.Of(); var fileSystems = new FileSystems(container, logger, ioHelper, SettingsForTests.GenerateMockGlobalSettings(), TestHelper.GetHostingEnvironment()) { IsScoped = () => scopedFileSystems }; @@ -511,6 +521,7 @@ namespace Umbraco.Tests.IO { var logger = Mock.Of(); var ioHelper = TestHelper.IOHelper; + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); var shadowfs = ioHelper.MapPath(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"); @@ -518,7 +529,7 @@ namespace Umbraco.Tests.IO var scopedFileSystems = false; - var phy = new PhysicalFileSystem(ioHelper, logger, path, "ignore"); + var phy = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path, "ignore"); var container = Mock.Of(); var fileSystems = new FileSystems(container, logger, ioHelper, SettingsForTests.GenerateMockGlobalSettings(), TestHelper.GetHostingEnvironment()) { IsScoped = () => scopedFileSystems }; @@ -565,6 +576,7 @@ namespace Umbraco.Tests.IO { var logger = Mock.Of(); var ioHelper = TestHelper.IOHelper; + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); var shadowfs = ioHelper.MapPath(Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"); @@ -572,7 +584,7 @@ namespace Umbraco.Tests.IO var scopedFileSystems = false; - var phy = new PhysicalFileSystem(ioHelper, logger, path, "ignore"); + var phy = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path, "ignore"); var container = Mock.Of(); var fileSystems = new FileSystems(container, logger, ioHelper, SettingsForTests.GenerateMockGlobalSettings(), TestHelper.GetHostingEnvironment()) { IsScoped = () => scopedFileSystems }; @@ -683,14 +695,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); // Act @@ -718,14 +731,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); // Act @@ -756,14 +770,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); // Act @@ -791,14 +806,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); // Act @@ -829,14 +845,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); // Act @@ -879,14 +896,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); // Act @@ -916,14 +934,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "ignore"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "ignore"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "ignore"); var ss = new ShadowFileSystem(fs, sfs); // Act @@ -958,14 +977,15 @@ namespace Umbraco.Tests.IO // Arrange var ioHelper = TestHelper.IOHelper; var logger = Mock.Of(); + var hostingEnvironment = TestHelper.GetHostingEnvironment(); var path = ioHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); - var fs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowTests/", "rootUrl"); - var sfs = new PhysicalFileSystem(ioHelper, logger, path + "/ShadowSystem/", "rootUrl"); + var fs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowTests/", "rootUrl"); + var sfs = new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, path + "/ShadowSystem/", "rootUrl"); var ss = new ShadowFileSystem(fs, sfs); // Act diff --git a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs new file mode 100644 index 0000000000..1b83c048d2 --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -0,0 +1,405 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Web.Compose; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class NestedContentPropertyComponentTests + { + [Test] + public void Invalid_Json() + { + var component = new NestedContentPropertyComponent(); + + Assert.DoesNotThrow(() => component.CreateNestedContentKeys("this is not json", true)); + } + + [Test] + public void No_Nesting() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[ + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} +]"; + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void One_Level_Nesting_Unescaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"": [{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ] + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void One_Level_Nesting_Escaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + // Complex editor such as the grid + var complexEditorJsonEscaped = @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subJsonEscaped + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + complexEditorJsonEscaped + @" + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + + [Test] + public void No_Nesting_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[ + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1 my key wont change"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""name"":""Item 2 was copied and has no key prop"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + + // Ensure that the original key is NOT changed/modified & still exists + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08")); + } + + [Test] + public void One_Level_Nesting_Escaped_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Nested Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + var json = @"[{ + ""name"": ""Item 1 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON for each item + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString())); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Nested Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + // Complex editor such as the grid + var complexEditorJsonEscaped = @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subJsonEscaped + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + complexEditorJsonEscaped + @" + } +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON for each item + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + } + } +} diff --git a/src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs b/src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs index 6b16f49d7d..2e2ebf392c 100644 --- a/src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs @@ -55,7 +55,7 @@ namespace Umbraco.Tests.Scoping [TestCase(false)] public void CreateMediaTest(bool complete) { - var physMediaFileSystem = new PhysicalFileSystem(IOHelper, Mock.Of(), IOHelper.MapPath("media"), "ignore"); + var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, Mock.Of(), IOHelper.MapPath("media"), "ignore"); var mediaFileSystem = Current.MediaFileSystem; Assert.IsFalse(physMediaFileSystem.FileExists("f1.txt")); @@ -88,7 +88,7 @@ namespace Umbraco.Tests.Scoping [Test] public void MultiThread() { - var physMediaFileSystem = new PhysicalFileSystem(IOHelper, Mock.Of(),IOHelper.MapPath("media"), "ignore"); + var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, Mock.Of(),IOHelper.MapPath("media"), "ignore"); var mediaFileSystem = Current.MediaFileSystem; var scopeProvider = ScopeProvider; diff --git a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs deleted file mode 100644 index 1e451cf57d..0000000000 --- a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Linq; -using System.Web; -using Microsoft.Owin; -using Moq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Composing; -using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.Accessors; -using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using Umbraco.Tests.Common; - -namespace Umbraco.Tests.Security -{ - [TestFixture] - [UmbracoTest(WithApplication = true)] - public class BackOfficeCookieManagerTests : UmbracoTestBase - { - [Test] - public void ShouldAuthenticateRequest_When_Not_Configured() - { - //should force app ctx to show not-configured - ConfigurationManager.AppSettings.Set(Constants.AppSettings.ConfigurationStatus, ""); - - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - var globalSettings = TestObjects.GetGlobalSettings(); - var umbracoContext = new UmbracoContext( - httpContextAccessor, - Mock.Of(), - Mock.Of(), - globalSettings, - HostingEnvironment, - new TestVariationContextAccessor(), - UriUtility, - new AspNetCookieManager(httpContextAccessor)); - - var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); - var mgr = new BackOfficeCookieManager( - Mock.Of(accessor => accessor.UmbracoContext == umbracoContext), runtime, HostingEnvironment, globalSettings, AppCaches.RequestCache); - - var result = mgr.ShouldAuthenticateRequest(Mock.Of(), new Uri("http://localhost/umbraco")); - - Assert.IsFalse(result); - } - - [Test] - public void ShouldAuthenticateRequest_When_Configured() - { - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - var globalSettings = TestObjects.GetGlobalSettings(); - var umbCtx = new UmbracoContext( - httpContextAccessor, - Mock.Of(), - Mock.Of(), - globalSettings, - HostingEnvironment, - new TestVariationContextAccessor(), - UriUtility, - new AspNetCookieManager(httpContextAccessor)); - - var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); - var mgr = new BackOfficeCookieManager(Mock.Of(accessor => accessor.UmbracoContext == umbCtx), runtime, HostingEnvironment, globalSettings, AppCaches.RequestCache); - - var request = new Mock(); - request.Setup(owinRequest => owinRequest.Uri).Returns(new Uri("http://localhost/umbraco")); - - var result = mgr.ShouldAuthenticateRequest( - Mock.Of(context => context.Request == request.Object), - new Uri("http://localhost/umbraco")); - - Assert.IsTrue(result); - } - - // TODO: Write remaining tests for `ShouldAuthenticateRequest` - } -} diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index 3f848325cc..76b23cf5e9 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -14,7 +14,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Net; +using Umbraco.Core.Templates; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Tests.Common; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f2a6dee0bc..46fb33335f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -118,17 +118,11 @@ - - - - - - @@ -162,6 +156,7 @@ + @@ -249,17 +244,14 @@ - - - @@ -276,7 +268,6 @@ - @@ -308,7 +299,6 @@ - @@ -457,7 +447,6 @@ - @@ -465,10 +454,8 @@ - - diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index e96e9cba48..47772449ec 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -1,13 +1,17 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; using System; using System.Net; using System.Threading.Tasks; +using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Extensions; +using Umbraco.Net; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; @@ -33,6 +37,8 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IUserService _userService; private readonly UmbracoMapper _umbracoMapper; private readonly IGlobalSettings _globalSettings; + private readonly ILogger _logger; + private readonly IIpResolver _ipResolver; // TODO: We need to import the logic from Umbraco.Web.Editors.AuthenticationController // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here @@ -43,7 +49,8 @@ namespace Umbraco.Web.BackOffice.Controllers BackOfficeSignInManager signInManager, IUserService userService, UmbracoMapper umbracoMapper, - IGlobalSettings globalSettings) + IGlobalSettings globalSettings, + ILogger logger, IIpResolver ipResolver) { _webSecurity = webSecurity; _userManager = backOfficeUserManager; @@ -51,6 +58,27 @@ namespace Umbraco.Web.BackOffice.Controllers _userService = userService; _umbracoMapper = umbracoMapper; _globalSettings = globalSettings; + _logger = logger; + _ipResolver = ipResolver; + } + + [HttpGet] + public double GetRemainingTimeoutSeconds() + { + var backOfficeIdentity = HttpContext.User.GetUmbracoIdentity(); + var remainingSeconds = HttpContext.User.GetRemainingAuthSeconds(); + if (remainingSeconds <= 30 && backOfficeIdentity != null) + { + //NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in + // the timeout process. + + _logger.Info( + "User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}", + backOfficeIdentity.Name, + _ipResolver.GetCurrentRequestIpAddress()); + } + + return remainingSeconds; } /// @@ -78,7 +106,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// 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] - [TypeFilter(typeof(SetAngularAntiForgeryTokens))] + [SetAngularAntiForgeryTokens] //[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level public UserDetail GetCurrentUser() { @@ -95,7 +123,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Logs a user in /// /// - [TypeFilter(typeof(SetAngularAntiForgeryTokens))] + [SetAngularAntiForgeryTokens] public async Task PostLogin(LoginModel loginModel) { // Sign the user in with username/password, this also gives a chance for developers to @@ -156,6 +184,22 @@ namespace Umbraco.Web.BackOffice.Controllers throw new HttpResponseException(HttpStatusCode.BadRequest); } + /// + /// Logs the current user out + /// + /// + [ValidateAngularAntiForgeryToken] + public IActionResult PostLogout() + { + HttpContext.SignOutAsync(Core.Constants.Security.BackOfficeAuthenticationType); + + _logger.Info("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress); + + _userManager.RaiseLogoutSuccessEvent(User, int.Parse(User.Identity.GetUserId())); + + return Ok(); + } + /// /// Return the for the given /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs index d5490edb9a..7cbeb8e86e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.IO; @@ -11,7 +12,7 @@ using Umbraco.Web.Common.Attributes; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class BackOfficeAssetsController : UmbracoAuthorizedJsonController { private readonly IFileSystem _jsLibFileSystem; diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs index a4c115f66f..6580ed5335 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Web.WebApi.Filters; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.BackOffice.Controllers { @@ -8,9 +7,8 @@ namespace Umbraco.Web.BackOffice.Controllers /// resulting message is INotificationModel in which case it will append any Event Messages /// currently in the request. /// - [TypeFilter(typeof(AppendCurrentEventMessagesAttribute))] + [AppendCurrentEventMessagesAttribute] public abstract class BackOfficeNotificationsController : UmbracoAuthorizedJsonController { - } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index a02220774d..439ae2e79c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.BackOffice.Controllers { // TODO: Put some exception filters in our webapi to return 404 instead of 500 when we throw ArgumentNullException // ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/ - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] //[PrefixlessBodyModelValidator] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Settings)] public class CodeFileController : BackOfficeNotificationsController diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index e204a3431e..03bbe132f3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -25,7 +25,7 @@ using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.BackOffice.Controllers { //we need to fire up the controller like this to enable loading of remote css directly from this controller - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [ValidationFilter] [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions [IsBackOffice] @@ -67,7 +67,7 @@ namespace Umbraco.Web.BackOffice.Controllers private static readonly HttpClient HttpClient = new HttpClient(); //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side - [TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))] + [ValidateAngularAntiForgeryToken] public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") { var user = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser; @@ -211,7 +211,7 @@ namespace Umbraco.Web.BackOffice.Controllers } // return IDashboardSlim - we don't need sections nor access rules - [TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))] + [ValidateAngularAntiForgeryToken] [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public IEnumerable> GetDashboard(string section) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index 0d66a5e329..157a09dbf8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The security for this controller is defined to allow full CRUD access to data types if the user has access to either: /// Content Types, Member Types or Media Types ... and of course to Data Types /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorizeAttribute(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] public class DataTypeController : BackOfficeNotificationsController { @@ -264,7 +264,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [TypeFilter(typeof(DataTypeValidateAttribute))] + [DataTypeValidate] public ActionResult PostSave(DataTypeSave dataType) { //If we've made it here, then everything has been wired up and validated by the attribute diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index a997c3dacf..7d362e52b6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The security for this controller is defined to allow full CRUD access to dictionary if the user has access to either: /// Dictionary /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] public class DictionaryController : BackOfficeNotificationsController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs index 5f5439f046..14fc25cfeb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs @@ -18,7 +18,7 @@ using SearchResult = Umbraco.Web.Models.ContentEditing.SearchResult; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class ExamineManagementController : UmbracoAuthorizedJsonController { private readonly IExamineManager _examineManager; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs index 47a5efdcbe..ee8d113abd 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Umbraco.Core; using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Web.Common.Attributes; @@ -22,7 +23,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// building to generate correct URLs /// /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class ImageUrlGeneratorController : UmbracoAuthorizedJsonController { private readonly IImageUrlGenerator _imageUrlGenerator; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index e79a481701..6ce6d3a1c6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Media; @@ -13,7 +14,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// A controller used to return images for media /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class ImagesController : UmbracoAuthorizedApiController { private readonly IMediaFileSystem _mediaFileSystem; diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 03e4ad163d..f2cbe571db 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Backoffice controller supporting the dashboard for language administration. /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] //[PrefixlessBodyModelValidator] public class LanguageController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs index 97dc74ac31..297c39a450 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The API controller used for getting log history /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class LogController : UmbracoAuthorizedJsonController { private readonly IMediaFileSystem _mediaFileSystem; diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 444ebbe988..258c3817aa 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Backoffice controller supporting the dashboard for viewing logs with some simple graphs & filtering /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class LogViewerController : UmbracoAuthorizedJsonController { private readonly ILogViewer _logViewer; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs index 58c30aa0ee..c3c4c9d9b7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacroRenderingController.cs @@ -12,7 +12,7 @@ using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Net; +using Umbraco.Core.Templates; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Common.Attributes; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs index 5b61b91c66..720b01acd9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs @@ -237,7 +237,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The . /// - public List GetPartialViews() + public IEnumerable GetPartialViews() { var views = new List(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 8994046cb2..e5e39f944f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// A controller used for managing packages in the back office /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] public class PackageController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 0216e6f09d..b61e86746a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// A controller used for installing packages and managing all of the data in the packages section in the back office /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] public class PackageInstallController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs index bee20f58e7..5086919b83 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs @@ -15,7 +15,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RedirectUrlManagementController : UmbracoAuthorizedApiController { private readonly ILogger _logger; diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs index 686afe284b..bf40e5722f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs @@ -17,7 +17,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] public class RelationController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index f679dd6b8e..31c77ce0a4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The API controller for editing relation types. /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorizeAttribute(Constants.Trees.RelationTypes)] public class RelationTypeController : BackOfficeNotificationsController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs index eb1694c34f..5239994e04 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.Controllers; +using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -17,7 +18,7 @@ namespace Umbraco.Web.Editors /// /// The API controller used for using the list of sections /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class SectionController : UmbracoAuthorizedJsonController { private readonly IControllerFactory _controllerFactory; diff --git a/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs index 852bff28c1..4dbfda1148 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The API controller used for retrieving available stylesheets /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class StylesheetController : UmbracoAuthorizedJsonController { private readonly IFileService _fileService; diff --git a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs index 7a2d4de4b0..0c1798fa8d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs @@ -16,7 +16,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorizeAttribute(Constants.Trees.Templates)] public class TemplateController : BackOfficeNotificationsController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index dd8620d8ee..dd7c539922 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs @@ -19,7 +19,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorize( Constants.Applications.Content, Constants.Applications.Media, diff --git a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs index bf8e89ae0d..f85bdb1bd5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; using Umbraco.Core.Services; @@ -13,7 +14,7 @@ using Umbraco.Web.Tour; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class TourController : UmbracoAuthorizedJsonController { private readonly TourFilterCollection _filters; diff --git a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs index 2c1b106aa3..917aeaef23 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs @@ -1,6 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Web.BackOffice.Controllers; -using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Filters; namespace Umbraco.Web.BackOffice.Controllers @@ -12,10 +10,9 @@ namespace Umbraco.Web.BackOffice.Controllers /// Inheriting from this controller means that ALL of your methods are JSON methods that are called by Angular, /// methods that are not called by Angular or don't contain a valid csrf header will NOT work. /// - [TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))] + [ValidateAngularAntiForgeryToken] [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions public abstract class UmbracoAuthorizedJsonController : UmbracoAuthorizedApiController { - } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs b/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs index 6ae0d1f612..4212bace72 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs @@ -15,7 +15,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class UpdateCheckController : UmbracoAuthorizedJsonController { private readonly IUpgradeService _upgradeService; diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index 832d92f4f0..acd468191a 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -23,9 +23,8 @@ namespace Umbraco.Extensions { services.AddAntiforgery(); - //We need to have runtime compilation of views when using backoffice. We could consider having only this when a specific config is set. - //But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. - services.AddControllersWithViews().AddRazorRuntimeCompilation(); + // TODO: We had this check in v8 where we don't enable these unless we can run... + //if (runtimeState.Level != RuntimeLevel.Upgrade && runtimeState.Level != RuntimeLevel.Run) return app; services .AddAuthentication(Constants.Security.BackOfficeAuthenticationType) diff --git a/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs index 60a4ae375f..23377c8651 100644 --- a/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs @@ -12,62 +12,73 @@ namespace Umbraco.Web.WebApi.Filters /// resulting message is INotificationModel in which case it will append any Event Messages /// currently in the request. /// - internal sealed class AppendCurrentEventMessagesAttribute : ActionFilterAttribute + internal sealed class AppendCurrentEventMessagesAttribute : TypeFilterAttribute { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IEventMessagesFactory _eventMessagesFactory; - - public AppendCurrentEventMessagesAttribute(IUmbracoContextAccessor umbracoContextAccessor, IEventMessagesFactory eventMessagesFactory) + public AppendCurrentEventMessagesAttribute() : base(typeof(AppendCurrentEventMessagesFilter)) { - _umbracoContextAccessor = umbracoContextAccessor; - _eventMessagesFactory = eventMessagesFactory; } - public override void OnActionExecuted(ActionExecutedContext context) + private class AppendCurrentEventMessagesFilter : IActionFilter { - if (context.HttpContext.Response == null) return; - if (context.HttpContext.Request.Method.Equals(HttpMethod.Get.ToString(), StringComparison.InvariantCultureIgnoreCase)) return; - var umbracoContext = _umbracoContextAccessor.UmbracoContext; - if (umbracoContext == null) return; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IEventMessagesFactory _eventMessagesFactory; - if(!(context.Result is ObjectResult obj)) return; - - var notifications = obj.Value as INotificationModel; - if (notifications == null) return; - - var msgs = _eventMessagesFactory.GetOrDefault(); - if (msgs == null) return; - - foreach (var eventMessage in msgs.GetAll()) + public AppendCurrentEventMessagesFilter(IUmbracoContextAccessor umbracoContextAccessor, IEventMessagesFactory eventMessagesFactory) { - NotificationStyle msgType; - switch (eventMessage.MessageType) - { - case EventMessageType.Default: - msgType = NotificationStyle.Save; - break; - case EventMessageType.Info: - msgType = NotificationStyle.Info; - break; - case EventMessageType.Error: - msgType = NotificationStyle.Error; - break; - case EventMessageType.Success: - msgType = NotificationStyle.Success; - break; - case EventMessageType.Warning: - msgType = NotificationStyle.Warning; - break; - default: - throw new ArgumentOutOfRangeException(); - } + _umbracoContextAccessor = umbracoContextAccessor; + _eventMessagesFactory = eventMessagesFactory; + } - notifications.Notifications.Add(new BackOfficeNotification + public void OnActionExecuted(ActionExecutedContext context) + { + if (context.HttpContext.Response == null) return; + if (context.HttpContext.Request.Method.Equals(HttpMethod.Get.ToString(), StringComparison.InvariantCultureIgnoreCase)) return; + var umbracoContext = _umbracoContextAccessor.UmbracoContext; + if (umbracoContext == null) return; + + if (!(context.Result is ObjectResult obj)) return; + + var notifications = obj.Value as INotificationModel; + if (notifications == null) return; + + var msgs = _eventMessagesFactory.GetOrDefault(); + if (msgs == null) return; + + foreach (var eventMessage in msgs.GetAll()) { - Message = eventMessage.Message, - Header = eventMessage.Category, - NotificationType = msgType - }); + NotificationStyle msgType; + switch (eventMessage.MessageType) + { + case EventMessageType.Default: + msgType = NotificationStyle.Save; + break; + case EventMessageType.Info: + msgType = NotificationStyle.Info; + break; + case EventMessageType.Error: + msgType = NotificationStyle.Error; + break; + case EventMessageType.Success: + msgType = NotificationStyle.Success; + break; + case EventMessageType.Warning: + msgType = NotificationStyle.Warning; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + notifications.Notifications.Add(new BackOfficeNotification + { + Message = eventMessage.Message, + Header = eventMessage.Category, + NotificationType = msgType + }); + } + } + + public void OnActionExecuting(ActionExecutingContext context) + { } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/DataTypeValidateAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/DataTypeValidateAttribute.cs index 41e928053d..83b2bde0e5 100644 --- a/src/Umbraco.Web.BackOffice/Filters/DataTypeValidateAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/DataTypeValidateAttribute.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core; using Umbraco.Core.Mapping; @@ -15,94 +16,98 @@ using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors { /// - /// An action filter that wires up the persisted entity of the DataTypeSave model and validates the whole request + /// An attribute/filter that wires up the persisted entity of the DataTypeSave model and validates the whole request /// - internal sealed class DataTypeValidateAttribute : ActionFilterAttribute + internal sealed class DataTypeValidateAttribute : TypeFilterAttribute { - private readonly IDataTypeService _dataTypeService; - private readonly PropertyEditorCollection _propertyEditorCollection; - private readonly UmbracoMapper _umbracoMapper; - - - /// - /// For use in unit tests. Not possible to use as attribute ctor. - /// - /// - /// - /// - public DataTypeValidateAttribute(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditorCollection, UmbracoMapper umbracoMapper) + public DataTypeValidateAttribute() : base(typeof(DataTypeValidateFilter)) { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - _propertyEditorCollection = propertyEditorCollection ?? throw new ArgumentNullException(nameof(propertyEditorCollection)); - _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); } - public override void OnActionExecuting(ActionExecutingContext context) + private class DataTypeValidateFilter : IActionFilter { - var dataType = (DataTypeSave) context.ActionArguments["dataType"]; + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditorCollection; + private readonly UmbracoMapper _umbracoMapper; - dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); - dataType.Alias = dataType.Alias == null ? dataType.Name : dataType.Alias.CleanForXss('[', ']', '(', ')', ':'); - - // get the property editor, ensuring that it exits - if (!_propertyEditorCollection.TryGet(dataType.EditorAlias, out var propertyEditor)) + public DataTypeValidateFilter(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditorCollection, UmbracoMapper umbracoMapper) { - var message = $"Property editor \"{dataType.EditorAlias}\" was not found."; - context.Result = new UmbracoProblemResult(message, HttpStatusCode.NotFound); - return; + _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + _propertyEditorCollection = propertyEditorCollection ?? throw new ArgumentNullException(nameof(propertyEditorCollection)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); } - // assign - dataType.PropertyEditor = propertyEditor; - - // validate that the data type exists, or create one if required - IDataType persisted; - switch (dataType.Action) + public void OnActionExecuted(ActionExecutedContext context) { - case ContentSaveAction.Save: - persisted = _dataTypeService.GetDataType(Convert.ToInt32(dataType.Id)); - if (persisted == null) - { - var message = $"Data type with id {dataType.Id} was not found."; - context.Result = new UmbracoProblemResult(message, HttpStatusCode.NotFound); - return; - } - // map the model to the persisted instance - _umbracoMapper.Map(dataType, persisted); - break; + } - case ContentSaveAction.SaveNew: - // create the persisted model from mapping the saved model - persisted = _umbracoMapper.Map(dataType); - ((DataType) persisted).ResetIdentity(); - break; + public void OnActionExecuting(ActionExecutingContext context) + { + var dataType = (DataTypeSave)context.ActionArguments["dataType"]; - default: - context.Result = new UmbracoProblemResult($"Data type action {dataType.Action} was not found.", HttpStatusCode.NotFound); + dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); + dataType.Alias = dataType.Alias == null ? dataType.Name : dataType.Alias.CleanForXss('[', ']', '(', ')', ':'); + + // get the property editor, ensuring that it exits + if (!_propertyEditorCollection.TryGet(dataType.EditorAlias, out var propertyEditor)) + { + var message = $"Property editor \"{dataType.EditorAlias}\" was not found."; + context.Result = new UmbracoProblemResult(message, HttpStatusCode.NotFound); return; - } + } - // assign (so it's available in the action) - dataType.PersistedDataType = persisted; + // assign + dataType.PropertyEditor = propertyEditor; - // validate the configuration - // which is posted as a set of fields with key (string) and value (object) - var configurationEditor = propertyEditor.GetConfigurationEditor(); - foreach (var field in dataType.ConfigurationFields) - { - var editorField = configurationEditor.Fields.SingleOrDefault(x => x.Key == field.Key); - if (editorField == null) continue; + // validate that the data type exists, or create one if required + IDataType persisted; + switch (dataType.Action) + { + case ContentSaveAction.Save: + persisted = _dataTypeService.GetDataType(Convert.ToInt32(dataType.Id)); + if (persisted == null) + { + var message = $"Data type with id {dataType.Id} was not found."; + context.Result = new UmbracoProblemResult(message, HttpStatusCode.NotFound); + return; + } + // map the model to the persisted instance + _umbracoMapper.Map(dataType, persisted); + break; - // run each IValueValidator (with null valueType and dataTypeConfiguration: not relevant here) - foreach (var validator in editorField.Validators) - foreach (var result in validator.Validate(field.Value, null, null)) - context.ModelState.AddValidationError(result, "Properties", field.Key); - } + case ContentSaveAction.SaveNew: + // create the persisted model from mapping the saved model + persisted = _umbracoMapper.Map(dataType); + ((DataType)persisted).ResetIdentity(); + break; - if (context.ModelState.IsValid == false) - { - // if it is not valid, do not continue and return the model state - throw HttpResponseException.CreateValidationErrorResponse(context.ModelState); + default: + context.Result = new UmbracoProblemResult($"Data type action {dataType.Action} was not found.", HttpStatusCode.NotFound); + return; + } + + // assign (so it's available in the action) + dataType.PersistedDataType = persisted; + + // validate the configuration + // which is posted as a set of fields with key (string) and value (object) + var configurationEditor = propertyEditor.GetConfigurationEditor(); + foreach (var field in dataType.ConfigurationFields) + { + var editorField = configurationEditor.Fields.SingleOrDefault(x => x.Key == field.Key); + if (editorField == null) continue; + + // run each IValueValidator (with null valueType and dataTypeConfiguration: not relevant here) + foreach (var validator in editorField.Validators) + foreach (var result in validator.Validate(field.Value, null, null)) + context.ModelState.AddValidationError(result, "Properties", field.Key); + } + + if (context.ModelState.IsValid == false) + { + // if it is not valid, do not continue and return the model state + throw HttpResponseException.CreateValidationErrorResponse(context.ModelState); + } } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokens.cs b/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokens.cs deleted file mode 100644 index 77804e3801..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokens.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.AspNetCore.Antiforgery; -using Microsoft.AspNetCore.Mvc.Filters; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Web.BackOffice.Security; - -namespace Umbraco.Extensions -{ - - /// - /// A filter to set the csrf cookie token based on angular conventions - /// - public sealed class SetAngularAntiForgeryTokens : IAsyncActionFilter - { - private readonly IBackOfficeAntiforgery _antiforgery; - private readonly IGlobalSettings _globalSettings; - - public SetAngularAntiForgeryTokens(IBackOfficeAntiforgery antiforgery, IGlobalSettings globalSettings) - { - _antiforgery = antiforgery; - _globalSettings = globalSettings; - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - if (context.HttpContext.Response != null) - { - //DO not set the token cookies if the request has failed!! - if (context.HttpContext.Response.StatusCode == (int)HttpStatusCode.OK) - { - //don't need to set the cookie if they already exist and they are valid - if (context.HttpContext.Request.Cookies.TryGetValue(Constants.Web.AngularCookieName, out var angularCookieVal) - && context.HttpContext.Request.Cookies.TryGetValue(Constants.Web.CsrfValidationCookieName, out var csrfCookieVal)) - { - //if they are not valid for some strange reason - we need to continue setting valid ones - var valResult = await _antiforgery.ValidateRequestAsync(context.HttpContext); - if (valResult.Success) - { - await next(); - return; - } - } - - string cookieToken, headerToken; - _antiforgery.GetTokens(context.HttpContext, out cookieToken, out headerToken); - - //We need to set 2 cookies: one is the cookie value that angular will use to set a header value on each request, - // the 2nd is the validation value generated by the anti-forgery helper that we use to validate the header token against. - - context.HttpContext.Response.Cookies.Append( - Constants.Web.AngularCookieName, headerToken, - new Microsoft.AspNetCore.Http.CookieOptions - { - Path = "/", - //must be js readable - HttpOnly = false, - Secure = _globalSettings.UseHttps - }); - - context.HttpContext.Response.Cookies.Append( - Constants.Web.CsrfValidationCookieName, cookieToken, - new Microsoft.AspNetCore.Http.CookieOptions - { - Path = "/", - HttpOnly = true, - Secure = _globalSettings.UseHttps - }); - } - } - - await next(); - } - - } -} diff --git a/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokensAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokensAttribute.cs new file mode 100644 index 0000000000..e52287b57e --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/SetAngularAntiForgeryTokensAttribute.cs @@ -0,0 +1,83 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Web.BackOffice.Security; + +namespace Umbraco.Extensions +{ + /// + /// An attribute/filter to set the csrf cookie token based on angular conventions + /// + public class SetAngularAntiForgeryTokensAttribute : TypeFilterAttribute + { + public SetAngularAntiForgeryTokensAttribute() : base(typeof(SetAngularAntiForgeryTokensFilter)) + { + } + + private class SetAngularAntiForgeryTokensFilter : IAsyncActionFilter + { + private readonly IBackOfficeAntiforgery _antiforgery; + private readonly IGlobalSettings _globalSettings; + + public SetAngularAntiForgeryTokensFilter(IBackOfficeAntiforgery antiforgery, IGlobalSettings globalSettings) + { + _antiforgery = antiforgery; + _globalSettings = globalSettings; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (context.HttpContext.Response != null) + { + //DO not set the token cookies if the request has failed!! + if (context.HttpContext.Response.StatusCode == (int)HttpStatusCode.OK) + { + //don't need to set the cookie if they already exist and they are valid + if (context.HttpContext.Request.Cookies.TryGetValue(Constants.Web.AngularCookieName, out var angularCookieVal) + && context.HttpContext.Request.Cookies.TryGetValue(Constants.Web.CsrfValidationCookieName, out var csrfCookieVal)) + { + //if they are not valid for some strange reason - we need to continue setting valid ones + var valResult = await _antiforgery.ValidateRequestAsync(context.HttpContext); + if (valResult.Success) + { + await next(); + return; + } + } + + string cookieToken, headerToken; + _antiforgery.GetTokens(context.HttpContext, out cookieToken, out headerToken); + + //We need to set 2 cookies: one is the cookie value that angular will use to set a header value on each request, + // the 2nd is the validation value generated by the anti-forgery helper that we use to validate the header token against. + + context.HttpContext.Response.Cookies.Append( + Constants.Web.AngularCookieName, headerToken, + new Microsoft.AspNetCore.Http.CookieOptions + { + Path = "/", + //must be js readable + HttpOnly = false, + Secure = _globalSettings.UseHttps + }); + + context.HttpContext.Response.Cookies.Append( + Constants.Web.CsrfValidationCookieName, cookieToken, + new Microsoft.AspNetCore.Http.CookieOptions + { + Path = "/", + HttpOnly = true, + Secure = _globalSettings.UseHttps + }); + } + } + + await next(); + } + + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index 2fd37e2875..57df8f69bf 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Net; using System.Security.Claims; using System.Threading.Tasks; @@ -15,100 +14,105 @@ using Umbraco.Web.BackOffice.Security; namespace Umbraco.Web.BackOffice.Filters { /// - /// A filter to check for the csrf token based on Angular's standard approach + /// An attribute/filter to check for the csrf token based on Angular's standard approach /// /// /// Code derived from http://ericpanorel.net/2013/07/28/spa-authentication-and-csrf-mvc4-antiforgery-implementation/ /// /// If the authentication type is cookie based, then this filter will execute, otherwise it will be disabled /// - public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute + public sealed class ValidateAngularAntiForgeryTokenAttribute : TypeFilterAttribute { - private readonly ILogger _logger; - private readonly IBackOfficeAntiforgery _antiforgery; - private readonly ICookieManager _cookieManager; - - public ValidateAngularAntiForgeryTokenAttribute(ILogger logger, IBackOfficeAntiforgery antiforgery, ICookieManager cookieManager) + public ValidateAngularAntiForgeryTokenAttribute() : base(typeof(ValidateAngularAntiForgeryTokenFilter)) { - _logger = logger; - _antiforgery = antiforgery; - _cookieManager = cookieManager; } - public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + private class ValidateAngularAntiForgeryTokenFilter : IAsyncActionFilter { - if (context.Controller is ControllerBase controller && controller.User.Identity is ClaimsIdentity userIdentity) - { - //if there is not CookiePath claim, then exit - if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) - { - await base.OnActionExecutionAsync(context, next); - return; - } - } - var cookieToken = _cookieManager.GetCookieValue(Constants.Web.CsrfValidationCookieName); - var httpContext = context.HttpContext; + private readonly ILogger _logger; + private readonly IBackOfficeAntiforgery _antiforgery; + private readonly ICookieManager _cookieManager; - var validateResult = await ValidateHeaders(httpContext, cookieToken); - if (validateResult.Item1 == false) + public ValidateAngularAntiForgeryTokenFilter(ILogger logger, IBackOfficeAntiforgery antiforgery, ICookieManager cookieManager) { - //TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body - // https://github.com/aspnet/HttpAbstractions/issues/395 - var httpResponseFeature = httpContext.Features.Get(); - if (!(httpResponseFeature is null)) + _logger = logger; + _antiforgery = antiforgery; + _cookieManager = cookieManager; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (context.Controller is ControllerBase controller && controller.User.Identity is ClaimsIdentity userIdentity) { - httpResponseFeature.ReasonPhrase = validateResult.Item2; + //if there is not CookiePath claim, then exit + if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) + { + await next(); + } + } + var cookieToken = _cookieManager.GetCookieValue(Constants.Web.CsrfValidationCookieName); + var httpContext = context.HttpContext; + + var validateResult = await ValidateHeaders(httpContext, cookieToken); + if (validateResult.Item1 == false) + { + //TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body + // https://github.com/aspnet/HttpAbstractions/issues/395 + var httpResponseFeature = httpContext.Features.Get(); + if (!(httpResponseFeature is null)) + { + httpResponseFeature.ReasonPhrase = validateResult.Item2; + } + + context.Result = new StatusCodeResult((int)HttpStatusCode.ExpectationFailed); } - context.Result = new StatusCodeResult((int)HttpStatusCode.ExpectationFailed); - return; + await next(); } - await next(); - } - - private async Task<(bool,string)> ValidateHeaders( - HttpContext httpContext, - string cookieToken) - { - var requestHeaders = httpContext.Request.Headers; - if (requestHeaders.Any(z => z.Key.InvariantEquals(Constants.Web.AngularHeadername)) == false) + private async Task<(bool, string)> ValidateHeaders( + HttpContext httpContext, + string cookieToken) { - return (false, "Missing token"); + var requestHeaders = httpContext.Request.Headers; + if (requestHeaders.Any(z => z.Key.InvariantEquals(Constants.Web.AngularHeadername)) == false) + { + return (false, "Missing token"); + } + + var headerToken = requestHeaders + .Where(z => z.Key.InvariantEquals(Constants.Web.AngularHeadername)) + .Select(z => z.Value) + .SelectMany(z => z) + .FirstOrDefault(); + + // both header and cookie must be there + if (cookieToken == null || headerToken == null) + { + return (false, "Missing token null"); + } + + if (await ValidateTokens(httpContext) == false) + { + return (false, "Invalid token"); + } + + return (true, "Success"); } - var headerToken = requestHeaders - .Where(z => z.Key.InvariantEquals(Constants.Web.AngularHeadername)) - .Select(z => z.Value) - .SelectMany(z => z) - .FirstOrDefault(); - - // both header and cookie must be there - if (cookieToken == null || headerToken == null) + private async Task ValidateTokens(HttpContext httpContext) { - return (false, "Missing token null"); - } - - if (await ValidateTokens(httpContext) == false) - { - return (false, "Invalid token"); - } - - return (true, "Success"); - } - - private async Task ValidateTokens(HttpContext httpContext) - { - // ensure that the cookie matches the header and then ensure it matches the correct value! - try - { - await _antiforgery.ValidateRequestAsync(httpContext); - return true; - } - catch (AntiforgeryValidationException ex) - { - _logger.Error(ex, "Could not validate XSRF token"); - return false; + // ensure that the cookie matches the header and then ensure it matches the correct value! + try + { + await _antiforgery.ValidateRequestAsync(httpContext); + return true; + } + catch (AntiforgeryValidationException ex) + { + _logger.Error(ex, "Could not validate XSRF token"); + return false; + } } } } diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs index fc922ca835..942b9dd6ea 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs @@ -4,10 +4,11 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Core.Services; using Umbraco.Web.Common.Attributes; using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Core; namespace Umbraco.Web.BackOffice.PropertyEditors { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class NestedContentController : UmbracoAuthorizedJsonController { private readonly IContentTypeService _contentTypeService; diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs index 2250f85f9b..1f302294de 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.BackOffice.PropertyEditors /// /// ApiController to provide RTE configuration with available plugins and commands from the RTE config /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RichTextPreValueController : UmbracoAuthorizedJsonController { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs index 8d9bc06acd..5ce8e09280 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs @@ -5,13 +5,14 @@ using Umbraco.Web.BackOffice.Controllers; using Umbraco.Core.Media; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Media.EmbedProviders; +using Umbraco.Core; namespace Umbraco.Web.BackOffice.PropertyEditors { /// /// A controller used for the embed dialog /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RteEmbedController : UmbracoAuthorizedJsonController { private readonly EmbedProvidersCollection _embedCollection; diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs index 37dbd84bfc..aa2b413abd 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.BackOffice.PropertyEditors /// DO NOT inherit from UmbracoAuthorizedJsonController since we don't want to use the angularized /// json formatter as it causes problems. /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class TagsDataController : UmbracoAuthorizedApiController { private readonly ITagQuery _tagQuery; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs index ca71f8f8af..f63b2380af 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs @@ -1,13 +1,16 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Routing; using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Extensions; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; +using Umbraco.Web.BackOffice.Controllers; namespace Umbraco.Web.BackOffice.Security { @@ -20,7 +23,7 @@ namespace Umbraco.Web.BackOffice.Security /// Umbraco's back office cookie needs to be read on two paths: /umbraco and /install and /base therefore we cannot just set the cookie path to be /umbraco, /// instead we'll specify our own cookie manager and return null if the request isn't for an acceptable path. /// - internal class BackOfficeCookieManager : ChunkingCookieManager, ICookieManager + public class BackOfficeCookieManager : ChunkingCookieManager, ICookieManager { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IRuntimeState _runtime; @@ -28,13 +31,25 @@ namespace Umbraco.Web.BackOffice.Security private readonly IGlobalSettings _globalSettings; private readonly IRequestCache _requestCache; private readonly string[] _explicitPaths; - private readonly string _getRemainingSecondsPath; - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache) - : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, null) + public BackOfficeCookieManager( + IUmbracoContextAccessor umbracoContextAccessor, + IRuntimeState runtime, + IHostingEnvironment hostingEnvironment, + IGlobalSettings globalSettings, + IRequestCache requestCache, + LinkGenerator linkGenerator) + : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, linkGenerator, null) { } - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache, IEnumerable explicitPaths) + public BackOfficeCookieManager( + IUmbracoContextAccessor umbracoContextAccessor, + IRuntimeState runtime, + IHostingEnvironment hostingEnvironment, + IGlobalSettings globalSettings, + IRequestCache requestCache, + LinkGenerator linkGenerator, + IEnumerable explicitPaths) { _umbracoContextAccessor = umbracoContextAccessor; _runtime = runtime; @@ -42,9 +57,6 @@ namespace Umbraco.Web.BackOffice.Security _globalSettings = globalSettings; _requestCache = requestCache; _explicitPaths = explicitPaths?.ToArray(); - var backOfficePath = _globalSettings.GetBackOfficePath(_hostingEnvironment); - // TODO: We shouldn't hard code this path - _getRemainingSecondsPath = $"{backOfficePath}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"; } /// @@ -60,7 +72,7 @@ namespace Umbraco.Web.BackOffice.Security /// * it is a /base request /// * it is a preview request /// - internal bool ShouldAuthenticateRequest(Uri requestUri, bool checkForceAuthTokens = true) + public bool ShouldAuthenticateRequest(Uri requestUri, bool checkForceAuthTokens = true) { // Do not authenticate the request if we are not running (don't have a db, are not configured) - since we will never need // to know a current user in this scenario - we treat it as a new install. Without this we can have some issues @@ -75,9 +87,6 @@ namespace Umbraco.Web.BackOffice.Security if (_explicitPaths != null) return _explicitPaths.Any(x => x.InvariantEquals(requestUri.AbsolutePath)); - //check user seconds path - if (requestUri.AbsolutePath.InvariantEquals(_getRemainingSecondsPath)) return false; - if (//check the explicit flag checkForceAuthTokens && _requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null //check back office diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs index 3a23ae6c88..91b982b5f6 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs @@ -9,7 +9,6 @@ namespace Umbraco.Web.BackOffice.Security /// /// Custom secure format that ensures the Identity in the ticket is and not just a ClaimsIdentity /// - // TODO: Unsure if we really need this, there's no real reason why we have a custom Identity instead of just a ClaimsIdentity internal class BackOfficeSecureDataFormat : ISecureDataFormat { private readonly int _loginTimeoutMinutes; @@ -23,7 +22,7 @@ namespace Umbraco.Web.BackOffice.Security public string Protect(AuthenticationTicket data, string purpose) { - //create a new ticket based on the passed in tickets details, however, we'll adjust the expires utc based on the specified timeout mins + // create a new ticket based on the passed in tickets details, however, we'll adjust the expires utc based on the specified timeout mins var ticket = new AuthenticationTicket(data.Principal, new AuthenticationProperties(data.Properties.Items) { diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index fd009eab23..d51db33a55 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Security; using Umbraco.Extensions; using Microsoft.Extensions.DependencyInjection; using Umbraco.Web.Common.Security; +using Microsoft.AspNetCore.Routing; namespace Umbraco.Web.BackOffice.Security { @@ -35,7 +36,9 @@ namespace Umbraco.Web.BackOffice.Security private readonly IRequestCache _requestCache; private readonly IUserService _userService; private readonly IIpResolver _ipResolver; + private readonly ISystemClock _systemClock; private readonly BackOfficeSessionIdValidator _sessionIdValidator; + private readonly LinkGenerator _linkGenerator; public ConfigureBackOfficeCookieOptions( IUmbracoContextAccessor umbracoContextAccessor, @@ -47,7 +50,9 @@ namespace Umbraco.Web.BackOffice.Security IRequestCache requestCache, IUserService userService, IIpResolver ipResolver, - BackOfficeSessionIdValidator sessionIdValidator) + ISystemClock systemClock, + BackOfficeSessionIdValidator sessionIdValidator, + LinkGenerator linkGenerator) { _umbracoContextAccessor = umbracoContextAccessor; _securitySettings = securitySettings; @@ -58,7 +63,9 @@ namespace Umbraco.Web.BackOffice.Security _requestCache = requestCache; _userService = userService; _ipResolver = ipResolver; + _systemClock = systemClock; _sessionIdValidator = sessionIdValidator; + _linkGenerator = linkGenerator; } public void Configure(string name, CookieAuthenticationOptions options) @@ -98,7 +105,8 @@ namespace Umbraco.Web.BackOffice.Security _runtimeState, _hostingEnvironment, _globalSettings, - _requestCache); + _requestCache, + _linkGenerator); // _explicitPaths); TODO: Implement this once we do OAuth somehow @@ -111,7 +119,7 @@ namespace Umbraco.Web.BackOffice.Security // It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else // our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because // the defaults work fine with our setup. - + OnValidatePrincipal = async ctx => { // We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this) @@ -131,6 +139,7 @@ namespace Umbraco.Web.BackOffice.Security await EnsureValidSessionId(ctx); await securityStampValidator.ValidateAsync(ctx); + EnsureTicketRenewalIfKeepUserLoggedIn(ctx); // add a claim to track when the cookie expires, we use this to track time remaining backOfficeIdentity.AddClaim(new Claim( @@ -140,7 +149,7 @@ namespace Umbraco.Web.BackOffice.Security UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity)); - + }, OnSigningIn = ctx => { @@ -175,7 +184,6 @@ namespace Umbraco.Web.BackOffice.Security OnSigningOut = ctx => { //Clear the user's session on sign out - // TODO: We need to test this once we have signout functionality, not sure if the httpcontext.user.identity will still be set here if (ctx.HttpContext?.User?.Identity != null) { var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity; @@ -192,7 +200,9 @@ namespace Umbraco.Web.BackOffice.Security BackOfficeSessionIdValidator.CookieName, _securitySettings.AuthCookieName, Constants.Web.PreviewCookieName, - Constants.Security.BackOfficeExternalCookieName + Constants.Security.BackOfficeExternalCookieName, + Constants.Web.AngularCookieName, + Constants.Web.CsrfValidationCookieName, }; foreach (var cookie in cookies) { @@ -218,5 +228,31 @@ namespace Umbraco.Web.BackOffice.Security if (_runtimeState.Level == RuntimeLevel.Run) await _sessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context); } + + /// + /// Ensures the ticket is renewed if the is set to true + /// and the current request is for the get user seconds endpoint + /// + /// + private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContext context) + { + if (!_securitySettings.KeepUserLoggedIn) return; + + var currentUtc = _systemClock.UtcNow; + var issuedUtc = context.Properties.IssuedUtc; + var expiresUtc = context.Properties.ExpiresUtc; + + if (expiresUtc.HasValue && issuedUtc.HasValue) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + //if it's time to renew, then do it + if (timeRemaining < timeElapsed) + { + context.ShouldRenew = true; + } + } + } } } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index 5a38d6c0ab..787da05ca4 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Filters; using Umbraco.Web.Features; using Umbraco.Web.WebApi.Filters; -using Umbraco.Web.Common.Attributes; namespace Umbraco.Web.Common.Controllers { diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 60f7b95b53..83f9aae30b 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -7,6 +7,7 @@ using Umbraco.Web.Common.Install; using Umbraco.Core.Hosting; using System.Linq.Expressions; using Umbraco.Web.Common.Controllers; +using System.Linq; namespace Umbraco.Extensions { @@ -58,6 +59,23 @@ namespace Umbraco.Extensions return linkGenerator.GetUmbracoApiService(actionName, typeof(T), id); } + public static string GetUmbracoApiService(this LinkGenerator url, Expression> methodSelector) + where T : UmbracoApiControllerBase + { + var method = ExpressionHelper.GetMethodInfo(methodSelector); + var methodParams = ExpressionHelper.GetMethodParams(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + + if (methodParams.Any() == false) + { + return url.GetUmbracoApiService(method.Name); + } + return url.GetUmbracoApiService(method.Name, methodParams.Values.First()); + } + public static string GetUmbracoApiServiceBaseUrl(this LinkGenerator linkGenerator, Expression> methodSelector) where T : UmbracoApiControllerBase { diff --git a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs index 0eabbf0f54..85eb55b6d9 100644 --- a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs @@ -1,5 +1,4 @@ - -using System.Buffers; +using System.Buffers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Options; @@ -16,7 +15,7 @@ namespace Umbraco.Web.Common.Filters { } - private class AngularJsonOnlyConfigurationFilter : IResultFilter + private class AngularJsonOnlyConfigurationFilter : IResultFilter { private readonly IOptions _mvcNewtonsoftJsonOptions; private readonly ArrayPool _arrayPool; @@ -31,7 +30,6 @@ namespace Umbraco.Web.Common.Filters public void OnResultExecuted(ResultExecutedContext context) { - } public void OnResultExecuting(ResultExecutingContext context) diff --git a/src/Umbraco.Web.Common/Install/InstallApiController.cs b/src/Umbraco.Web.Common/Install/InstallApiController.cs index 1f41d34123..edc73ef293 100644 --- a/src/Umbraco.Web.Common/Install/InstallApiController.cs +++ b/src/Umbraco.Web.Common/Install/InstallApiController.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Logging; @@ -13,7 +12,6 @@ using Umbraco.Net; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; -using Umbraco.Web.Common.ModelBinding; using Umbraco.Web.Common.Security; using Umbraco.Web.Install; using Umbraco.Web.Install.Models; diff --git a/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs b/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs index e5ebdd177c..1349145357 100644 --- a/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs @@ -128,7 +128,7 @@ namespace Umbraco.Web.Common.Routing object constraints = null) => endpoints.MapUmbracoRoute(controllerType, rootSegment, areaName, isBackOffice - ? (areaName.IsNullOrWhiteSpace() ? "BackOffice/Api" : $"BackOffice/{areaName}") + ? (areaName.IsNullOrWhiteSpace() ? $"{Core.Constants.Web.Mvc.BackOfficePathSegment}/Api" : $"{Core.Constants.Web.Mvc.BackOfficePathSegment}/{areaName}") : (areaName.IsNullOrWhiteSpace() ? "Api" : areaName), defaultAction, true, constraints); } diff --git a/src/Umbraco.Web.Common/Routing/PublishedRouter.cs b/src/Umbraco.Web.Common/Routing/PublishedRouter.cs deleted file mode 100644 index 6618d6edf5..0000000000 --- a/src/Umbraco.Web.Common/Routing/PublishedRouter.cs +++ /dev/null @@ -1,766 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Globalization; -using System.IO; -using Umbraco.Core; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -namespace Umbraco.Web.Common.Routing -{ - /// - /// Provides the default implementation. - /// - public class PublishedRouter : IPublishedRouter - { - private readonly IWebRoutingSettings _webRoutingSettings; - private readonly ContentFinderCollection _contentFinders; - private readonly IContentLastChanceFinder _contentLastChanceFinder; - private readonly IProfilingLogger _profilingLogger; - private readonly IVariationContextAccessor _variationContextAccessor; - private readonly ILogger _logger; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IRequestAccessor _requestAccessor; - private readonly IPublishedValueFallback _publishedValueFallback; - private readonly IPublicAccessChecker _publicAccessChecker; - private readonly IFileService _fileService; - private readonly IContentTypeService _contentTypeService; - private readonly IPublicAccessService _publicAccessService; - - /// - /// Initializes a new instance of the class. - /// - public PublishedRouter( - IWebRoutingSettings webRoutingSettings, - ContentFinderCollection contentFinders, - IContentLastChanceFinder contentLastChanceFinder, - IVariationContextAccessor variationContextAccessor, - IProfilingLogger proflog, - IPublishedUrlProvider publishedUrlProvider, - IRequestAccessor requestAccessor, - IPublishedValueFallback publishedValueFallback, - IPublicAccessChecker publicAccessChecker, - IFileService fileService, - IContentTypeService contentTypeService, - IPublicAccessService publicAccessService) - { - _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); - _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); - _contentLastChanceFinder = contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); - _profilingLogger = proflog ?? throw new ArgumentNullException(nameof(proflog)); - _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - _logger = proflog; - _publishedUrlProvider = publishedUrlProvider; - _requestAccessor = requestAccessor; - _publishedValueFallback = publishedValueFallback; - _publicAccessChecker = publicAccessChecker; - _fileService = fileService; - _contentTypeService = contentTypeService; - _publicAccessService = publicAccessService; - } - - /// - public IPublishedRequest CreateRequest(IUmbracoContext umbracoContext, Uri uri = null) - { - return new PublishedRequest(this, umbracoContext, _webRoutingSettings, uri ?? umbracoContext.CleanedUmbracoUrl); - } - - #region Request - - /// - public bool TryRouteRequest(IPublishedRequest request) - { - // disabled - is it going to change the routing? - //_pcr.OnPreparing(); - - FindDomain(request); - if (request.IsRedirect) return false; - if (request.HasPublishedContent) return true; - FindPublishedContent(request); - if (request.IsRedirect) return false; - - // don't handle anything - we just want to ensure that we find the content - //HandlePublishedContent(); - //FindTemplate(); - //FollowExternalRedirect(); - //HandleWildcardDomains(); - - // disabled - we just want to ensure that we find the content - //_pcr.OnPrepared(); - - return request.HasPublishedContent; - } - - private void SetVariationContext(string culture) - { - var variationContext = _variationContextAccessor.VariationContext; - if (variationContext != null && variationContext.Culture == culture) return; - _variationContextAccessor.VariationContext = new VariationContext(culture); - } - - /// - public bool PrepareRequest(IPublishedRequest request) - { - // note - at that point the original legacy module did something do handle IIS custom 404 errors - // ie pages looking like /anything.aspx?404;/path/to/document - I guess the reason was to support - // "directory urls" without having to do wildcard mapping to ASP.NET on old IIS. This is a pain - // to maintain and probably not used anymore - removed as of 06/2012. @zpqrtbnk. - // - // to trigger Umbraco's not-found, one should configure IIS and/or ASP.NET custom 404 errors - // so that they point to a non-existing page eg /redirect-404.aspx - // TODO: SD: We need more information on this for when we release 4.10.0 as I'm not sure what this means. - - // trigger the Preparing event - at that point anything can still be changed - // the idea is that it is possible to change the uri - // - request.OnPreparing(); - - //find domain - FindDomain(request); - - // if request has been flagged to redirect then return - // whoever called us is in charge of actually redirecting - if (request.IsRedirect) - { - return false; - } - - // set the culture on the thread - once, so it's set when running document lookups - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = request.Culture; - SetVariationContext(request.Culture.Name); - - //find the published content if it's not assigned. This could be manually assigned with a custom route handler, or - // with something like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. Those in turn call this method - // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. - if (request.PublishedContent == null) - { - // find the document & template - FindPublishedContentAndTemplate(request); - } - - // handle wildcard domains - HandleWildcardDomains(request); - - // set the culture on the thread -- again, 'cos it might have changed due to a finder or wildcard domain - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = request.Culture; - SetVariationContext(request.Culture.Name); - - // trigger the Prepared event - at that point it is still possible to change about anything - // even though the request might be flagged for redirection - we'll redirect _after_ the event - // - // also, OnPrepared() will make the PublishedRequest readonly, so nothing can change - // - request.OnPrepared(); - - // we don't take care of anything so if the content has changed, it's up to the user - // to find out the appropriate template - - //complete the PCR and assign the remaining values - return ConfigureRequest(request); - } - - /// - /// Called by PrepareRequest once everything has been discovered, resolved and assigned to the PCR. This method - /// finalizes the PCR with the values assigned. - /// - /// - /// Returns false if the request was not successfully configured - /// - /// - /// This method logic has been put into it's own method in case developers have created a custom PCR or are assigning their own values - /// but need to finalize it themselves. - /// - public bool ConfigureRequest(IPublishedRequest frequest) - { - if (frequest.HasPublishedContent == false) - { - return false; - } - - // set the culture on the thread -- again, 'cos it might have changed in the event handler - Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = frequest.Culture; - SetVariationContext(frequest.Culture.Name); - - // if request has been flagged to redirect, or has no content to display, - // then return - whoever called us is in charge of actually redirecting - if (frequest.IsRedirect || frequest.HasPublishedContent == false) - { - return false; - } - - // we may be 404 _and_ have a content - - // can't go beyond that point without a PublishedContent to render - // it's ok not to have a template, in order to give MVC a chance to hijack routes - - return true; - } - - /// - public void UpdateRequestToNotFound(IPublishedRequest request) - { - // clear content - var content = request.PublishedContent; - request.PublishedContent = null; - - HandlePublishedContent(request); // will go 404 - FindTemplate(request); - - // if request has been flagged to redirect then return - // whoever called us is in charge of redirecting - if (request.IsRedirect) - return; - - if (request.HasPublishedContent == false) - { - // means the engine could not find a proper document to handle 404 - // restore the saved content so we know it exists - request.PublishedContent = content; - return; - } - - if (request.HasTemplate == false) - { - // means we may have a document, but we have no template - // at that point there isn't much we can do and there is no point returning - // to Mvc since Mvc can't do much either - return; - } - } - - #endregion - - #region Domain - - /// - /// Finds the site root (if any) matching the http request, and updates the PublishedRequest accordingly. - /// - /// A value indicating whether a domain was found. - internal bool FindDomain(IPublishedRequest request) - { - const string tracePrefix = "FindDomain: "; - - // note - we are not handling schemes nor ports here. - - _logger.Debug("{TracePrefix}Uri={RequestUri}", tracePrefix, request.Uri); - - var domainsCache = request.UmbracoContext.PublishedSnapshot.Domains; - var domains = domainsCache.GetAll(includeWildcards: false).ToList(); - - // determines whether a domain corresponds to a published document, since some - // domains may exist but on a document that has been unpublished - as a whole - or - // that is not published for the domain's culture - in which case the domain does - // not apply - bool IsPublishedContentDomain(Domain domain) - { - // just get it from content cache - optimize there, not here - var domainDocument = request.UmbracoContext.PublishedSnapshot.Content.GetById(domain.ContentId); - - // not published - at all - if (domainDocument == null) - return false; - - // invariant - always published - if (!domainDocument.ContentType.VariesByCulture()) - return true; - - // variant, ensure that the culture corresponding to the domain's language is published - return domainDocument.Cultures.ContainsKey(domain.Culture.Name); - } - - domains = domains.Where(IsPublishedContentDomain).ToList(); - - var defaultCulture = domainsCache.DefaultCulture; - - // try to find a domain matching the current request - var domainAndUri = DomainUtilities.SelectDomain(domains, request.Uri, defaultCulture: defaultCulture); - - // handle domain - always has a contentId and a culture - if (domainAndUri != null) - { - // matching an existing domain - _logger.Debug("{TracePrefix}Matches domain={Domain}, rootId={RootContentId}, culture={Culture}", tracePrefix, domainAndUri.Name, domainAndUri.ContentId, domainAndUri.Culture); - - request.Domain = domainAndUri; - request.Culture = domainAndUri.Culture; - - // canonical? not implemented at the moment - // if (...) - // { - // _pcr.RedirectUrl = "..."; - // return true; - // } - } - else - { - // not matching any existing domain - _logger.Debug("{TracePrefix}Matches no domain", tracePrefix); - - request.Culture = defaultCulture == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultCulture); - } - - _logger.Debug("{TracePrefix}Culture={CultureName}", tracePrefix, request.Culture.Name); - - return request.Domain != null; - } - - /// - /// Looks for wildcard domains in the path and updates Culture accordingly. - /// - internal void HandleWildcardDomains(IPublishedRequest request) - { - const string tracePrefix = "HandleWildcardDomains: "; - - if (request.HasPublishedContent == false) - return; - - var nodePath = request.PublishedContent.Path; - _logger.Debug("{TracePrefix}Path={NodePath}", tracePrefix, nodePath); - var rootNodeId = request.HasDomain ? request.Domain.ContentId : (int?)null; - var domain = DomainUtilities.FindWildcardDomainInPath(request.UmbracoContext.PublishedSnapshot.Domains.GetAll(true), nodePath, rootNodeId); - - // always has a contentId and a culture - if (domain != null) - { - request.Culture = domain.Culture; - _logger.Debug("{TracePrefix}Got domain on node {DomainContentId}, set culture to {CultureName}", tracePrefix, domain.ContentId, request.Culture.Name); - } - else - { - _logger.Debug("{TracePrefix}No match.", tracePrefix); - } - } - - #endregion - - #region Rendering engine - - internal bool FindTemplateRenderingEngineInDirectory(DirectoryInfo directory, string alias, string[] extensions) - { - if (directory == null || directory.Exists == false) - return false; - - var pos = alias.IndexOf('/'); - if (pos > 0) - { - // recurse - var subdir = directory.GetDirectories(alias.Substring(0, pos)).FirstOrDefault(); - alias = alias.Substring(pos + 1); - return subdir != null && FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); - } - - // look here - return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); - } - - #endregion - - #region Document and template - - /// - public ITemplate GetTemplate(string alias) - { - return _fileService.GetTemplate(alias); - } - - /// - /// Finds the Umbraco document (if any) matching the request, and updates the PublishedRequest accordingly. - /// - /// A value indicating whether a document and template were found. - private void FindPublishedContentAndTemplate(IPublishedRequest request) - { - _logger.Debug("FindPublishedContentAndTemplate: Path={UriAbsolutePath}", request.Uri.AbsolutePath); - - // run the document finders - FindPublishedContent(request); - - // if request has been flagged to redirect then return - // whoever called us is in charge of actually redirecting - // -- do not process anything any further -- - if (request.IsRedirect) - return; - - // not handling umbracoRedirect here but after LookupDocument2 - // so internal redirect, 404, etc has precedence over redirect - - // handle not-found, redirects, access... - HandlePublishedContent(request); - - // find a template - FindTemplate(request); - - // handle umbracoRedirect - FollowExternalRedirect(request); - } - - /// - /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. - /// - /// There is no finder collection. - internal void FindPublishedContent(IPublishedRequest request) - { - const string tracePrefix = "FindPublishedContent: "; - - // look for the document - // the first successful finder, if any, will set this.PublishedContent, and may also set this.Template - // some finders may implement caching - - using (_profilingLogger.DebugDuration( - $"{tracePrefix}Begin finders", - $"{tracePrefix}End finders, {(request.HasPublishedContent ? "a document was found" : "no document was found")}")) - { - //iterate but return on first one that finds it - var found = _contentFinders.Any(finder => - { - _logger.Debug("Finder {ContentFinderType}", finder.GetType().FullName); - return finder.TryFindContent(request); - }); - } - - // indicate that the published content (if any) we have at the moment is the - // one that was found by the standard finders before anything else took place. - request.SetIsInitialPublishedContent(); - } - - /// - /// Handles the published content (if any). - /// - /// - /// Handles "not found", internal redirects, access validation... - /// things that must be handled in one place because they can create loops - /// - private void HandlePublishedContent(IPublishedRequest request) - { - // because these might loop, we have to have some sort of infinite loop detection - int i = 0, j = 0; - const int maxLoop = 8; - do - { - _logger.Debug("HandlePublishedContent: Loop {LoopCounter}", i); - - // handle not found - if (request.HasPublishedContent == false) - { - request.Is404 = true; - _logger.Debug("HandlePublishedContent: No document, try last chance lookup"); - - // if it fails then give up, there isn't much more that we can do - if (_contentLastChanceFinder.TryFindContent(request) == false) - { - _logger.Debug("HandlePublishedContent: Failed to find a document, give up"); - break; - } - - _logger.Debug("HandlePublishedContent: Found a document"); - } - - // follow internal redirects as long as it's not running out of control ie infinite loop of some sort - j = 0; - while (FollowInternalRedirects(request) && j++ < maxLoop) - { } - if (j == maxLoop) // we're running out of control - break; - - // ensure access - if (request.HasPublishedContent) - EnsurePublishedContentAccess(request); - - // loop while we don't have page, ie the redirect or access - // got us to nowhere and now we need to run the notFoundLookup again - // as long as it's not running out of control ie infinite loop of some sort - - } while (request.HasPublishedContent == false && i++ < maxLoop); - - if (i == maxLoop || j == maxLoop) - { - _logger.Debug("HandlePublishedContent: Looks like we are running into an infinite loop, abort"); - request.PublishedContent = null; - } - - _logger.Debug("HandlePublishedContent: End"); - } - - /// - /// Follows internal redirections through the umbracoInternalRedirectId document property. - /// - /// A value indicating whether redirection took place and led to a new published document. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - /// As per legacy, if the redirect does not work, we just ignore it. - /// - private bool FollowInternalRedirects(IPublishedRequest request) - { - if (request.PublishedContent == null) - throw new InvalidOperationException("There is no PublishedContent."); - - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Core.Constants.Conventions.Content.InternalRedirectId) == false) - return false; - - var redirect = false; - var valid = false; - IPublishedContent internalRedirectNode = null; - var internalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); - - if (internalRedirectId > 0) - { - // try and get the redirect node from a legacy integer ID - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectId); - } - else - { - var udiInternalRedirectId = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.InternalRedirectId); - if (udiInternalRedirectId != null) - { - // try and get the redirect node from a UDI Guid - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(udiInternalRedirectId.Guid); - } - } - - if (valid == false) - { - // bad redirect - log and display the current page (legacy behavior) - _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: value is not an int nor a GuidUdi.", - request.PublishedContent.GetProperty(Core.Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); - } - - if (internalRedirectNode == null) - { - _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", - request.PublishedContent.GetProperty(Core.Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); - } - else if (internalRedirectId == request.PublishedContent.Id) - { - // redirect to self - _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); - } - else - { - request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here - redirect = true; - _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); - } - - return redirect; - } - - /// - /// Ensures that access to current node is permitted. - /// - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - private void EnsurePublishedContentAccess(IPublishedRequest request) - { - if (request.PublishedContent == null) - throw new InvalidOperationException("There is no PublishedContent."); - - var path = request.PublishedContent.Path; - - var publicAccessAttempt = _publicAccessService.IsProtected(path); - - if (publicAccessAttempt) - { - _logger.Debug("EnsurePublishedContentAccess: Page is protected, check for access"); - - var status = _publicAccessChecker.HasMemberAccessToContent(request.PublishedContent.Id); - switch (status) - { - case PublicAccessStatus.NotLoggedIn: - _logger.Debug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.LoginNodeId); - break; - case PublicAccessStatus.AccessDenied: - _logger.Debug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.LockedOut: - _logger.Debug("Current member is locked out, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.NotApproved: - _logger.Debug("Current member is unapproved, redirect to error page"); - SetPublishedContentAsOtherPage(request, publicAccessAttempt.Result.NoAccessNodeId); - break; - case PublicAccessStatus.AccessAccepted: - _logger.Debug("Current member has access"); - break; - } - } - else - { - _logger.Debug("EnsurePublishedContentAccess: Page is not protected"); - } - } - - private static void SetPublishedContentAsOtherPage(IPublishedRequest request, int errorPageId) - { - if (errorPageId != request.PublishedContent.Id) - request.PublishedContent = request.UmbracoContext.PublishedSnapshot.Content.GetById(errorPageId); - } - - /// - /// Finds a template for the current node, if any. - /// - private void FindTemplate(IPublishedRequest request) - { - // NOTE: at the moment there is only 1 way to find a template, and then ppl must - // use the Prepared event to change the template if they wish. Should we also - // implement an ITemplateFinder logic? - - if (request.PublishedContent == null) - { - request.TemplateModel = null; - return; - } - - // read the alternate template alias, from querystring, form, cookie or server vars, - // only if the published content is the initial once, else the alternate template - // does not apply - // + optionally, apply the alternate template on internal redirects - var useAltTemplate = request.IsInitialPublishedContent - || (_webRoutingSettings.InternalRedirectPreservesTemplate && request.IsInternalRedirectPublishedContent); - var altTemplate = useAltTemplate - ? _requestAccessor.GetRequestValue(Core.Constants.Conventions.Url.AltTemplate) - : null; - - if (string.IsNullOrWhiteSpace(altTemplate)) - { - // we don't have an alternate template specified. use the current one if there's one already, - // which can happen if a content lookup also set the template (LookupByNiceUrlAndTemplate...), - // else lookup the template id on the document then lookup the template with that id. - - if (request.HasTemplate) - { - _logger.Debug("FindTemplate: Has a template already, and no alternate template."); - return; - } - - // TODO: When we remove the need for a database for templates, then this id should be irrelevant, - // not sure how were going to do this nicely. - - // TODO: We need to limit altTemplate to only allow templates that are assigned to the current document type! - // if the template isn't assigned to the document type we should log a warning and return 404 - - var templateId = request.PublishedContent.TemplateId; - request.TemplateModel = GetTemplateModel(templateId); - } - else - { - // we have an alternate template specified. lookup the template with that alias - // this means the we override any template that a content lookup might have set - // so /path/to/page/template1?altTemplate=template2 will use template2 - - // ignore if the alias does not match - just trace - - if (request.HasTemplate) - _logger.Debug("FindTemplate: Has a template already, but also an alternative template."); - _logger.Debug("FindTemplate: Look for alternative template alias={AltTemplate}", altTemplate); - - // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings - if (request.PublishedContent.IsAllowedTemplate( - _fileService, - _contentTypeService, - _webRoutingSettings.DisableAlternativeTemplates, - _webRoutingSettings.ValidateAlternativeTemplates, - altTemplate)) - { - // allowed, use - var template = _fileService.GetTemplate(altTemplate); - - if (template != null) - { - request.TemplateModel = template; - _logger.Debug("FindTemplate: Got alternative template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - } - else - { - _logger.Debug("FindTemplate: The alternative template with alias={AltTemplate} does not exist, ignoring.", altTemplate); - } - } - else - { - _logger.Warn("FindTemplate: Alternative template {TemplateAlias} is not allowed on node {NodeId}, ignoring.", altTemplate, request.PublishedContent.Id); - - // no allowed, back to default - var templateId = request.PublishedContent.TemplateId; - request.TemplateModel = GetTemplateModel(templateId); - } - } - - if (request.HasTemplate == false) - { - _logger.Debug("FindTemplate: No template was found."); - - // initial idea was: if we're not already 404 and UmbracoSettings.HandleMissingTemplateAs404 is true - // then reset _pcr.Document to null to force a 404. - // - // but: because we want to let MVC hijack routes even though no template is defined, we decide that - // a missing template is OK but the request will then be forwarded to MVC, which will need to take - // care of everything. - // - // so, don't set _pcr.Document to null here - } - else - { - _logger.Debug("FindTemplate: Running with template id={TemplateId} alias={TemplateAlias}", request.TemplateModel.Id, request.TemplateModel.Alias); - } - } - - private ITemplate GetTemplateModel(int? templateId) - { - if (templateId.HasValue == false || templateId.Value == default) - { - _logger.Debug("GetTemplateModel: No template."); - return null; - } - - _logger.Debug("GetTemplateModel: Get template id={TemplateId}", templateId); - - if (templateId == null) - throw new InvalidOperationException("The template is not set, the page cannot render."); - - var template = _fileService.GetTemplate(templateId.Value); - if (template == null) - throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render."); - _logger.Debug("GetTemplateModel: Got template id={TemplateId} alias={TemplateAlias}", template.Id, template.Alias); - return template; - } - - /// - /// Follows external redirection through umbracoRedirect document property. - /// - /// As per legacy, if the redirect does not work, we just ignore it. - private void FollowExternalRedirect(IPublishedRequest request) - { - if (request.HasPublishedContent == false) return; - - // don't try to find a redirect if the property doesn't exist - if (request.PublishedContent.HasProperty(Core.Constants.Conventions.Content.Redirect) == false) - return; - - var redirectId = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.Redirect, defaultValue: -1); - var redirectUrl = "#"; - if (redirectId > 0) - { - redirectUrl = _publishedUrlProvider.GetUrl(redirectId); - } - else - { - // might be a UDI instead of an int Id - var redirectUdi = request.PublishedContent.Value(_publishedValueFallback, Core.Constants.Conventions.Content.Redirect); - if (redirectUdi != null) - redirectUrl = _publishedUrlProvider.GetUrl(redirectUdi.Guid); - } - if (redirectUrl != "#") - request.SetRedirect(redirectUrl); - } - - #endregion - } -} diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 6f98dbca6e..01d73568ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -5,14 +5,14 @@ @scope @description -Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. -In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. -You can easily add you own tours to the Help-drawer or show and start tours from +Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. +In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. +You can easily add you own tours to the Help-drawer or show and start tours from anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install The Starter Kit in Umbraco 7.8

Extending the help drawer with custom tours

-The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. -Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be +The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. +Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be picked up by Umbraco and shown in the Help-drawer.

The tour object

@@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list "groupOrder": 200 // Control the order of tour groups "allowDisable": // Adds a "Don't" show this tour again"-button to the intro step "culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users - "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. + "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. "steps": [] // tour steps - see next example } @@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list "backdropOpacity": 0.4 // the backdrop opacity "view": "" // add a custom view "customProperties" : {} // add any custom properties needed for the custom view + "skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step }

Adding tours to other parts of the Umbraco backoffice

-It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, +It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.

Using the tour service

@@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um "element": "[data-element='my-tour-button']", "title": "Click the button", "content": "Click the button", - "event": "click" + "event": "click", + "skipStepIfVisible": "[data-element='my-other-tour-button']" } ] }; @@ -257,9 +259,26 @@ In the following example you see how to run some custom logic before a step goes // make sure we don't go too far if (scope.model.currentStepIndex !== scope.model.steps.length) { + + var upcomingStep = scope.model.steps[scope.model.currentStepIndex]; + + // If the currentStep JSON object has 'skipStepIfVisible' + // It's a DOM selector - if we find it then we ship over this step + if(upcomingStep.skipStepIfVisible) { + let tryFindDomEl = document.querySelector(upcomingStep.element); + if(tryFindDomEl) { + // check if element is visible: + if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) { + // if it was visible then we skip the step. + nextStep(); + } + } + } + startStep(); - // tour completed - final step + } else { + // tour completed - final step scope.loadingStep = true; waitForPendingRerequests().then(function () { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index 9a3760444d..8a24d948ac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -48,6 +48,9 @@ label.umb-form-check--checkbox{ &:checked ~ .umb-form-check__state .umb-form-check__check { border-color: @ui-option-type; } + &[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check { + background-color: @ui-option-type; + } &:checked:hover ~ .umb-form-check__state .umb-form-check__check { &::before { background: @ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 3ff9d0cff2..53fb1e6a14 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -52,6 +52,11 @@ namespace Umbraco.Web.UI.BackOffice options.ShouldProfile = request => false; // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile }); + //We need to have runtime compilation of views when using umbraco. We could consider having only this when a specific config is set. + //But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. + services.AddControllersWithViews().AddRazorRuntimeCompilation(); + + // If using Kestrel: https://stackoverflow.com/a/55196057 services.Configure(options => { diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index d0aa1a1c34..3709e703d9 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -188,27 +188,21 @@ "event": "click" }, { - "element": "[data-element~='editor-data-type-picker']", + "element": "[ng-controller*='Umbraco.Editors.DataTypePickerController'] [data-element='editor-data-type-picker']", "elementPreventClick": true, "title": "Editor picker", - "content": "

In the editor picker dialog we can pick one of the many built-in editors.

You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors).

" + "content": "

In the editor picker dialog we can pick one of the many built-in editors.

" }, { - "element": "[data-element~='editor-data-type-picker'] [data-element='editor-Textarea']", + "element": "[data-element~='editor-data-type-picker'] [data-element='datatype-Textarea']", "title": "Select editor", "content": "Select the Textarea editor. This will add a textarea to the Welcome Text property.", "event": "click" }, { - "element": "[data-element~='editor-data-type-settings']", - "elementPreventClick": true, + "element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea'] > a", "title": "Editor settings", - "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed." - }, - { - "element": "[data-element~='editor-data-type-settings'] [data-element='button-submit']", - "title": "Save editor", - "content": "Click Submit to save the changes.", + "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed.", "event": "click" }, { @@ -317,7 +311,8 @@ "content": "

To see all our templates click the small triangle to the left of the templates node.

", "event": "click", "eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']", - "view": "templatetree" + "view": "templatetree", + "skipStepIfVisible": "#tree [data-element='tree-item-templates'] > div > button[data-element=tree-item-expand].icon-navigation-down" }, { "element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']", diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 7ce8890018..68120947df 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -11,7 +11,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.Mapping; -using Umbraco.Core.Net; +using Umbraco.Core.Templates; using Umbraco.Net; using Umbraco.Core.PackageActions; using Umbraco.Core.Packaging; diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 8ae88b62ef..fe8a93f67a 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -386,30 +386,7 @@ namespace Umbraco.Web.Editors } - /// - /// Logs the current user out - /// - /// - [ClearAngularAntiForgeryToken] - [ValidateAngularAntiForgeryToken] - public HttpResponseMessage PostLogout() - { - var owinContext = Request.TryGetOwinContext().Result; - - owinContext.Authentication.SignOut( - Core.Constants.Security.BackOfficeAuthenticationType, - Core.Constants.Security.BackOfficeExternalAuthenticationType); - - Logger.Info("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, owinContext.Request.RemoteIpAddress); - - if (UserManager != null) - { - int.TryParse(User.Identity.GetUserId(), out var userId); - UserManager.RaiseLogoutSuccessEvent(User, userId); - } - - return Request.CreateResponse(HttpStatusCode.OK); - } + // NOTE: This has been migrated to netcore, but in netcore we don't explicitly set the principal in this method, that's done in ConfigureUmbracoBackOfficeCookieOptions so don't worry about that private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user, IPrincipal principal) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 99a0c12339..e2f6334a11 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -4,7 +4,7 @@ using Microsoft.AspNet.SignalR; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dictionary; -using Umbraco.Core.Net; +using Umbraco.Core.Templates; using Umbraco.Core.Runtime; using Umbraco.Core.Security; using Umbraco.Core.Services; @@ -41,8 +41,6 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(); - // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index f766a142d9..d95c19bedf 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -29,228 +29,6 @@ namespace Umbraco.Web.Security ///
public static class AppBuilderExtensions { - /// - /// Configure Default Identity User Manager for Umbraco - /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, - ServiceContext services, - IGlobalSettings globalSettings, - UmbracoMapper mapper, - // TODO: This could probably be optional? - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeOwinUserManager.Create( - services.UserService, - services.EntityService, - services.ExternalLoginService, - globalSettings, - mapper, - passwordConfiguration, - ipResolver, - new BackOfficeIdentityErrorDescriber(), - app.GetDataProtectionProvider(), - new NullLogger>())); - - app.SetBackOfficeUserManagerType(); - - //Create a sign in manager per request - app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger())); - } - - /// - /// Configure a custom UserStore with the Identity User Manager for Umbraco - /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, - IRuntimeState runtimeState, - IGlobalSettings globalSettings, - BackOfficeUserStore customUserStore, - // TODO: This could probably be optional? - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver) - { - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - if (customUserStore == null) throw new ArgumentNullException(nameof(customUserStore)); - - //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeOwinUserManager.Create( - passwordConfiguration, - ipResolver, - customUserStore, - new BackOfficeIdentityErrorDescriber(), - app.GetDataProtectionProvider(), - new NullLogger>())); - - app.SetBackOfficeUserManagerType(); - - //Create a sign in manager per request - app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); - } - - /// - /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// By default this will be configured to execute on PipelineStage.Authenticate - /// - public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, - IUmbracoContextAccessor umbracoContextAccessor, - IRuntimeState runtimeState, - IUserService userService, - IGlobalSettings globalSettings, - ISecuritySettings securitySettings, - IHostingEnvironment hostingEnvironment, - IRequestCache requestCache) - { - return app.UseUmbracoBackOfficeCookieAuthentication(umbracoContextAccessor, runtimeState, userService, globalSettings, securitySettings, hostingEnvironment, requestCache, PipelineStage.Authenticate); - } - - /// - /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Configurable pipeline stage - /// - /// - public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, - IUmbracoContextAccessor umbracoContextAccessor, - IRuntimeState runtimeState, - IUserService userService, - IGlobalSettings globalSettings, - ISecuritySettings securitySettings, - IHostingEnvironment hostingEnvironment, - IRequestCache requestCache, - PipelineStage stage) - { - //Create the default options and provider - var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache); - - return app.UseUmbracoBackOfficeCookieAuthentication(umbracoContextAccessor, runtimeState, globalSettings, securitySettings, hostingEnvironment, requestCache, authOptions, stage); - } - - /// - /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline - /// - /// - /// - /// - /// - /// - /// - /// - /// Custom auth cookie options can be specified to have more control over the cookie authentication logic - /// - /// Configurable pipeline stage - /// - /// - public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, - ISecuritySettings securitySettings, IHostingEnvironment hostingEnvironment, IRequestCache requestCache, CookieAuthenticationOptions cookieOptions, PipelineStage stage) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - if (cookieOptions == null) throw new ArgumentNullException(nameof(cookieOptions)); - if (cookieOptions.Provider == null) - throw new ArgumentNullException("cookieOptions.Provider cannot be null.", nameof(cookieOptions)); - if (cookieOptions.Provider is BackOfficeCookieAuthenticationProvider == false) - throw new ArgumentException($"cookieOptions.Provider must be of type {typeof(BackOfficeCookieAuthenticationProvider)}.", nameof(cookieOptions)); - - app.UseUmbracoBackOfficeCookieAuthenticationInternal(cookieOptions, runtimeState, requestCache, stage); - - //don't apply if app is not ready - if (runtimeState.Level != RuntimeLevel.Upgrade && runtimeState.Level != RuntimeLevel.Run) return app; - - var backOfficePath = globalSettings.GetBackOfficePath(hostingEnvironment); - var cookieAuthOptions = app.CreateUmbracoCookieAuthOptions( - umbracoContextAccessor, globalSettings, runtimeState, securitySettings, - //This defines the explicit path read cookies from for this middleware - hostingEnvironment, requestCache, new[] {$"{backOfficePath}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"}); - cookieAuthOptions.Provider = cookieOptions.Provider; - - //This is a custom middleware, we need to return the user's remaining logged in seconds - app.Use( - cookieAuthOptions, - Current.Configs.Global(), - Current.Configs.Security(), - app.CreateLogger(), - Current.HostingEnvironment); - - //This is required so that we can read the auth ticket format outside of this pipeline - app.CreatePerOwinContext( - (options, context) => new UmbracoAuthTicketDataProtector(cookieOptions.TicketDataFormat)); - - return app; - } - - private static bool _markerSet = false; - - /// - /// This registers the exact type of the user manager in owin so we can extract it - /// when required in order to extract the user manager instance - /// - /// - /// - /// - /// - /// This is required because a developer can specify a custom user manager and due to generic types the key name will registered - /// differently in the owin context - /// - private static void SetBackOfficeUserManagerType(this IAppBuilder app) - where TManager : BackOfficeUserManager - where TUser : BackOfficeIdentityUser - { - if (_markerSet) throw new InvalidOperationException("The back office user manager marker has already been set, only one back office user manager can be configured"); - - //on each request set the user manager getter - - // this is required purely because Microsoft.Owin.IOwinContext is super inflexible with it's Get since it can only be - // a generic strongly typed instance - app.Use((context, func) => - { - context.Set(BackOfficeOwinUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); - return func(); - }); - } - - private static void UseUmbracoBackOfficeCookieAuthenticationInternal(this IAppBuilder app, CookieAuthenticationOptions options, IRuntimeState runtimeState, IRequestCache requestCache, PipelineStage stage) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - - //First the normal cookie middleware - app.Use(typeof(CookieAuthenticationMiddleware), app, options); - //don't apply if app is not ready - if (runtimeState.Level == RuntimeLevel.Upgrade || runtimeState.Level == RuntimeLevel.Run) - { - //Then our custom middlewares - app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options, Current.UmbracoContextAccessor, requestCache); - app.Use(typeof(FixWindowsAuthMiddlware)); - } - - //Marks all of the above middlewares to execute on Authenticate - app.UseStageMarker(stage); - } - /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct @@ -297,8 +75,6 @@ namespace Umbraco.Web.Security AuthenticationMode = AuthenticationMode.Passive, CookieName = Constants.Security.BackOfficeExternalCookieName, ExpireTimeSpan = TimeSpan.FromMinutes(5), - //Custom cookie manager so we can filter requests - CookieManager = new BackOfficeCookieManager(umbracoContextAccessor, runtimeState, hostingEnvironment, globalSettings, requestCache), CookiePath = "/", CookieSecure = globalSettings.UseHttps ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest, CookieHttpOnly = true, @@ -353,8 +129,8 @@ namespace Umbraco.Web.Security { if (runtimeState.Level != RuntimeLevel.Run) return app; - var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache); - app.Use(typeof(PreviewAuthenticationMiddleware), authOptions, globalSettings, hostingEnvironment); + //var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache); + app.Use(typeof(PreviewAuthenticationMiddleware), /*authOptions*/null, globalSettings, hostingEnvironment); // This middleware must execute at least on PostAuthentication, by default it is on Authorize // The middleware needs to execute after the RoleManagerModule executes which is during PostAuthenticate, @@ -372,40 +148,6 @@ namespace Umbraco.Web.Security Thread.CurrentThread.SanitizeThreadCulture(); } - /// - /// Create the default umb cookie auth options - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static UmbracoBackOfficeCookieAuthOptions CreateUmbracoCookieAuthOptions(this IAppBuilder app, - IUmbracoContextAccessor umbracoContextAccessor, - IGlobalSettings globalSettings, IRuntimeState runtimeState, ISecuritySettings securitySettings, IHostingEnvironment hostingEnvironment, IRequestCache requestCache, string[] explicitPaths = null) - { - //this is how aspnet wires up the default AuthenticationTicket protector so we'll use the same code - var ticketDataFormat = new TicketDataFormat( - app.CreateDataProtector(typeof (CookieAuthenticationMiddleware).FullName, - Constants.Security.BackOfficeAuthenticationType, - "v1")); - - var authOptions = new UmbracoBackOfficeCookieAuthOptions( - explicitPaths, - umbracoContextAccessor, - securitySettings, - globalSettings, - hostingEnvironment, - runtimeState, - ticketDataFormat, - requestCache); - - return authOptions; - } public static IAppBuilder CreatePerOwinContext(this IAppBuilder app, Func createCallback) where T : class, IDisposable { diff --git a/src/Umbraco.Web/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/BackOfficeCookieManager.cs deleted file mode 100644 index 73c143afd4..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeCookieManager.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using Microsoft.Owin; -using Microsoft.Owin.Infrastructure; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Security; - -namespace Umbraco.Web.Security -{ - /// - /// A custom cookie manager that is used to read the cookie from the request. - /// - /// - /// Umbraco's back office cookie needs to be read on two paths: /umbraco and /install and /base therefore we cannot just set the cookie path to be /umbraco, - /// instead we'll specify our own cookie manager and return null if the request isn't for an acceptable path. - /// - internal class BackOfficeCookieManager : ChunkingCookieManager, Microsoft.Owin.Infrastructure.ICookieManager - { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IRuntimeState _runtime; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IGlobalSettings _globalSettings; - private readonly IRequestCache _requestCache; - private readonly string[] _explicitPaths; - private readonly string _getRemainingSecondsPath; - - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache) - : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, null) - { } - - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache, IEnumerable explicitPaths) - { - _umbracoContextAccessor = umbracoContextAccessor; - _runtime = runtime; - _hostingEnvironment = hostingEnvironment; - _globalSettings = globalSettings; - _requestCache = requestCache; - _explicitPaths = explicitPaths?.ToArray(); - var backOfficePath = _globalSettings.GetBackOfficePath(_hostingEnvironment); - _getRemainingSecondsPath = $"{backOfficePath}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"; - } - - /// - /// Explicitly implement this so that we filter the request - /// - /// - /// - /// - string Microsoft.Owin.Infrastructure.ICookieManager.GetRequestCookie(IOwinContext context, string key) - { - if (_umbracoContextAccessor.UmbracoContext == null || context.Request.Uri.IsClientSideRequest()) - { - return null; - } - - return ShouldAuthenticateRequest( - context, - _umbracoContextAccessor.UmbracoContext.OriginalRequestUrl) == false - //Don't auth request, don't return a cookie - ? null - //Return the default implementation - : GetRequestCookie(context, key); - } - - /// - /// Determines if we should authenticate the request - /// - /// - /// - /// - /// - /// - /// We auth the request when: - /// * it is a back office request - /// * it is an installer request - /// * it is a /base request - /// * it is a preview request - /// - internal bool ShouldAuthenticateRequest(IOwinContext owinContext, Uri originalRequestUrl, bool checkForceAuthTokens = true) - { - // Do not authenticate the request if we are not running (don't have a db, are not configured) - since we will never need - // to know a current user in this scenario - we treat it as a new install. Without this we can have some issues - // when people have older invalid cookies on the same domain since our user managers might attempt to lookup a user - // and we don't even have a db. - // was: app.IsConfigured == false (equiv to !Run) && dbContext.IsDbConfigured == false (equiv to Install) - // so, we handle .Install here and NOT .Upgrade - if (_runtime.Level == RuntimeLevel.Install) - return false; - - var request = owinContext.Request; - //check the explicit paths - if (_explicitPaths != null) - { - return _explicitPaths.Any(x => x.InvariantEquals(request.Uri.AbsolutePath)); - } - - //check user seconds path - if (request.Uri.AbsolutePath.InvariantEquals(_getRemainingSecondsPath)) return false; - - if (//check the explicit flag - (checkForceAuthTokens && owinContext.Get(Constants.Security.ForceReAuthFlag) != null) - || (checkForceAuthTokens && _requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null) - //check back office - || request.Uri.IsBackOfficeRequest(_globalSettings, _hostingEnvironment) - //check installer - || request.Uri.IsInstallerRequest(_hostingEnvironment)) - { - return true; - } - return false; - } - - } -} diff --git a/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs b/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs index b3092b90c9..deb8e9fd63 100644 --- a/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs +++ b/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs @@ -57,19 +57,19 @@ namespace Umbraco.Web.Security protected override Task ApplyResponseGrantAsync() { if (_umbracoContextAccessor.UmbracoContext == null || Context.Request.Uri.IsClientSideRequest()) - { + { return Task.FromResult(0); } //Now we need to check if we should force renew this based on a flag in the context and whether this is a request that is not normally renewed by OWIN... // which means that it is not a normal URL that is authenticated. - var normalAuthUrl = ((BackOfficeCookieManager) Options.CookieManager) - .ShouldAuthenticateRequest(Context, _umbracoContextAccessor.UmbracoContext.OriginalRequestUrl, - //Pass in false, we want to know if this is a normal auth'd page - checkForceAuthTokens: false); - //This is auth'd normally, so OWIN will naturally take care of the cookie renewal - if (normalAuthUrl) return Task.FromResult(0); + //var normalAuthUrl = ((BackOfficeCookieManager) Options.CookieManager) + // .ShouldAuthenticateRequest(Context, _umbracoContextAccessor.UmbracoContext.OriginalRequestUrl, + // //Pass in false, we want to know if this is a normal auth'd page + // checkForceAuthTokens: false); + ////This is auth'd normally, so OWIN will naturally take care of the cookie renewal + //if (normalAuthUrl) return Task.FromResult(0); //check for the special flag in either the owin or http context var shouldRenew = Context.Get(Constants.Security.ForceReAuthFlag) != null || (_requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null); diff --git a/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs index 68155f47b7..34669bc5ae 100644 --- a/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs +++ b/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs @@ -18,34 +18,6 @@ namespace Umbraco.Web.Security { public int LoginTimeoutMinutes { get; } - public UmbracoBackOfficeCookieAuthOptions( - string[] explicitPaths, - IUmbracoContextAccessor umbracoContextAccessor, - ISecuritySettings securitySettings, - IGlobalSettings globalSettings, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState, - ISecureDataFormat secureDataFormat, - IRequestCache requestCache) - { - var secureDataFormat1 = secureDataFormat ?? throw new ArgumentNullException(nameof(secureDataFormat)); - LoginTimeoutMinutes = globalSettings.TimeOutInMinutes; - AuthenticationType = Constants.Security.BackOfficeAuthenticationType; - - SlidingExpiration = true; - ExpireTimeSpan = TimeSpan.FromMinutes(LoginTimeoutMinutes); - CookieDomain = securitySettings.AuthCookieDomain; - CookieName = securitySettings.AuthCookieName; - CookieHttpOnly = true; - CookieSecure = globalSettings.UseHttps ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; - - TicketDataFormat = new UmbracoSecureDataFormat(LoginTimeoutMinutes, secureDataFormat1); - - //Custom cookie manager so we can filter requests - CookieManager = new BackOfficeCookieManager(umbracoContextAccessor, runtimeState, hostingEnvironment, globalSettings, requestCache, explicitPaths); - } - /// /// Creates the cookie options for saving the auth cookie /// diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs deleted file mode 100644 index 4a1a6c97ec..0000000000 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Models; -using Umbraco.Web.Mvc; -using Umbraco.Web.Routing; - -namespace Umbraco.Web.Templates -{ - /// - /// This is used purely for the RenderTemplate functionality in Umbraco - /// - /// - /// This allows you to render an MVC template based purely off of a node id and an optional alttemplate id as string output. - /// - internal class TemplateRenderer : ITemplateRenderer - { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedRouter _publishedRouter; - private readonly IFileService _fileService; - private readonly ILocalizationService _languageService; - private readonly IWebRoutingSettings _webRoutingSettings; - private readonly IShortStringHelper _shortStringHelper; - private readonly IHttpContextAccessor _httpContextAccessor; - - public TemplateRenderer(IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, IFileService fileService, ILocalizationService textService, IWebRoutingSettings webRoutingSettings, IShortStringHelper shortStringHelper, IHttpContextAccessor httpContextAccessor) - { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); - _fileService = fileService ?? throw new ArgumentNullException(nameof(fileService)); - _languageService = textService ?? throw new ArgumentNullException(nameof(textService)); - _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); - _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); - _httpContextAccessor = httpContextAccessor; - } - - public void Render(int pageId, int? altTemplateId, StringWriter writer) - { - if (writer == null) throw new ArgumentNullException(nameof(writer)); - - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - // instantiate a request and process - // important to use CleanedUmbracoUrl - lowercase path-only version of the current url, though this isn't going to matter - // terribly much for this implementation since we are just creating a doc content request to modify it's properties manually. - var contentRequest = _publishedRouter.CreateRequest(umbracoContext); - - var doc = contentRequest.UmbracoContext.Content.GetById(pageId); - - if (doc == null) - { - writer.Write("", pageId); - return; - } - - //in some cases the UmbracoContext will not have a PublishedRequest assigned to it if we are not in the - //execution of a front-end rendered page. In this case set the culture to the default. - //set the culture to the same as is currently rendering - if (umbracoContext.PublishedRequest == null) - { - var defaultLanguage = _languageService.GetAllLanguages().FirstOrDefault(); - contentRequest.Culture = defaultLanguage == null - ? CultureInfo.CurrentUICulture - : defaultLanguage.CultureInfo; - } - else - { - contentRequest.Culture = umbracoContext.PublishedRequest.Culture; - } - - //set the doc that was found by id - contentRequest.PublishedContent = doc; - //set the template, either based on the AltTemplate found or the standard template of the doc - var templateId = _webRoutingSettings.DisableAlternativeTemplates || !altTemplateId.HasValue - ? doc.TemplateId - : altTemplateId.Value; - if (templateId.HasValue) - contentRequest.TemplateModel = _fileService.GetTemplate(templateId.Value); - - //if there is not template then exit - if (contentRequest.HasTemplate == false) - { - if (altTemplateId.HasValue == false) - { - writer.Write("", doc.TemplateId); - } - else - { - writer.Write("", altTemplateId); - } - return; - } - - //First, save all of the items locally that we know are used in the chain of execution, we'll need to restore these - //after this page has rendered. - SaveExistingItems(out var oldPublishedRequest); - - try - { - //set the new items on context objects for this templates execution - SetNewItemsOnContextObjects(contentRequest); - - //Render the template - ExecuteTemplateRendering(writer, contentRequest); - } - finally - { - //restore items on context objects to continuing rendering the parent template - RestoreItems(oldPublishedRequest); - } - - } - - private void ExecuteTemplateRendering(TextWriter sw, IPublishedRequest request) - { - //NOTE: Before we used to build up the query strings here but this is not necessary because when we do a - // Server.Execute in the TemplateRenderer, we pass in a 'true' to 'preserveForm' which automatically preserves all current - // query strings so there's no need for this. HOWEVER, once we get MVC involved, we might have to do some fun things, - // though this will happen in the TemplateRenderer. - - //var queryString = _umbracoContext.HttpContext.Request.QueryString.AllKeys - // .ToDictionary(key => key, key => context.Request.QueryString[key]); - - var requestContext = new RequestContext(_httpContextAccessor.GetRequiredHttpContext(), new RouteData() - { - Route = RouteTable.Routes["Umbraco_default"] - }); - var routeHandler = new RenderRouteHandler(_umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory(), _shortStringHelper); - var routeDef = routeHandler.GetUmbracoRouteDefinition(requestContext, request); - var renderModel = new ContentModel(request.PublishedContent); - //manually add the action/controller, this is required by mvc - requestContext.RouteData.Values.Add("action", routeDef.ActionName); - requestContext.RouteData.Values.Add("controller", routeDef.ControllerName); - //add the rest of the required route data - routeHandler.SetupRouteDataForRequest(renderModel, requestContext, request); - - var stringOutput = RenderUmbracoRequestToString(requestContext); - - sw.Write(stringOutput); - } - - /// - /// This will execute the UmbracoMvcHandler for the request specified and get the string output. - /// - /// - /// Assumes the RequestContext is setup specifically to render an Umbraco view. - /// - /// - /// - /// To achieve this we temporarily change the output text writer of the current HttpResponse, then - /// execute the controller via the handler which inevitably writes the result to the text writer - /// that has been assigned to the response. Then we change the response textwriter back to the original - /// before continuing . - /// - private string RenderUmbracoRequestToString(RequestContext requestContext) - { - var currentWriter = requestContext.HttpContext.Response.Output; - var newWriter = new StringWriter(); - requestContext.HttpContext.Response.Output = newWriter; - - var handler = new UmbracoMvcHandler(requestContext); - handler.ExecuteUmbracoRequest(); - - //reset it - requestContext.HttpContext.Response.Output = currentWriter; - return newWriter.ToString(); - } - - private void SetNewItemsOnContextObjects(IPublishedRequest request) - { - //now, set the new ones for this page execution - _umbracoContextAccessor.UmbracoContext.PublishedRequest = request; - } - - /// - /// Save all items that we know are used for rendering execution to variables so we can restore after rendering - /// - private void SaveExistingItems(out IPublishedRequest oldPublishedRequest) - { - //Many objects require that these legacy items are in the http context items... before we render this template we need to first - //save the values in them so that we can re-set them after we render so the rest of the execution works as per normal - oldPublishedRequest = _umbracoContextAccessor.UmbracoContext.PublishedRequest; - } - - /// - /// Restores all items back to their context's to continue normal page rendering execution - /// - private void RestoreItems(IPublishedRequest oldPublishedRequest) - { - _umbracoContextAccessor.UmbracoContext.PublishedRequest = oldPublishedRequest; - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index fb93ca5435..f871c4dc2b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -319,7 +319,6 @@ - @@ -328,7 +327,6 @@ - @@ -425,6 +423,8 @@ + + diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 22d7bd77d7..f800707476 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -63,7 +63,6 @@ namespace Umbraco.Web protected virtual void ConfigureServices(IAppBuilder app, ServiceContext services) { app.SetUmbracoLoggerFactory(); - ConfigureUmbracoUserManager(app); } /// @@ -81,21 +80,6 @@ namespace Umbraco.Web .FinalizeMiddlewareConfiguration(); } - /// - /// Configure the Identity user manager for use with Umbraco Back office - /// - /// - protected virtual void ConfigureUmbracoUserManager(IAppBuilder app) - { - // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) - app.ConfigureUserManagerForUmbracoBackOffice( - Services, - GlobalSettings, - Mapper, - UserPasswordConfig, - IpResolver); - } - /// /// Configure external/OAuth login providers /// @@ -105,7 +89,8 @@ namespace Umbraco.Web // Ensure owin is configured for Umbraco back office authentication. // Front-end OWIN cookie configuration must be declared after this code. app - .UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate) + // already moved to netcore + //.UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate) .UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate) .UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authorize); } diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 469837c5f2..5b72e9cda0 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -7,7 +7,7 @@ using Umbraco.Composing; using Umbraco.Core; using Umbraco.Core.Dictionary; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Net; +using Umbraco.Core.Templates; using Umbraco.Core.Strings; using Umbraco.Core.Xml; using Umbraco.Web.Mvc; diff --git a/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs deleted file mode 100644 index 2aa2f1c52f..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Web.Http.Filters; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Clears the angular csrf cookie if the request was successful - /// - public sealed class ClearAngularAntiForgeryTokenAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(HttpActionExecutedContext context) - { - if (context.Response == null) return; - if (context.Response.IsSuccessStatusCode == false) return; - - //remove the cookies - var angularCookie = new CookieHeaderValue(Core.Constants.Web.AngularCookieName, "null") - { - Expires = DateTime.Now.AddYears(-1), - //must be js readable - HttpOnly = false, - Path = "/" - }; - var validationCookie = new CookieHeaderValue(Core.Constants.Web.CsrfValidationCookieName, "null") - { - Expires = DateTime.Now.AddYears(-1), - HttpOnly = true, - Path = "/" - }; - context.Response.Headers.AddCookies(new[] { angularCookie, validationCookie }); - } - } -}