diff --git a/src/Umbraco.Core/Trees/ITree.cs b/src/Umbraco.Core/Trees/ITree.cs index 567accbd9e..7f7ff816be 100644 --- a/src/Umbraco.Core/Trees/ITree.cs +++ b/src/Umbraco.Core/Trees/ITree.cs @@ -3,7 +3,7 @@ // TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used // leave as internal for now, maybe we'll use in the future, means we could pass around ITree // TODO: We should make this a thing, a tree should just be an interface *not* a controller - internal interface ITree + public interface ITree { /// /// Gets or sets the sort order. diff --git a/src/Umbraco.Core/Trees/Tree.cs b/src/Umbraco.Core/Trees/Tree.cs index 4747d2495b..b528443eb1 100644 --- a/src/Umbraco.Core/Trees/Tree.cs +++ b/src/Umbraco.Core/Trees/Tree.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.Trees /// public Type TreeControllerType { get; } - internal static string GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) + public static string GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) { var label = $"[{tree.TreeAlias}]"; diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitDownloadStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitDownloadStep.cs index d8986cacb7..24133d3be1 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitDownloadStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitDownloadStep.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Models.Packaging; using Umbraco.Net; using Umbraco.Web.Install.Models; +using Umbraco.Web.Security; namespace Umbraco.Web.Install.InstallSteps { @@ -16,16 +17,16 @@ namespace Umbraco.Web.Install.InstallSteps internal class StarterKitDownloadStep : InstallSetupStep { private readonly InstallHelper _installHelper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly IUmbracoVersion _umbracoVersion; private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; private readonly IContentService _contentService; private readonly IPackagingService _packageService; - public StarterKitDownloadStep(IContentService contentService, IPackagingService packageService, InstallHelper installHelper, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoVersion umbracoVersion, IUmbracoApplicationLifetime umbracoApplicationLifetime) + public StarterKitDownloadStep(IContentService contentService, IPackagingService packageService, InstallHelper installHelper, IWebSecurity webSecurity, IUmbracoVersion umbracoVersion, IUmbracoApplicationLifetime umbracoApplicationLifetime) { _installHelper = installHelper; - _umbracoContextAccessor = umbracoContextAccessor; + _webSecurity = webSecurity; _umbracoVersion = umbracoVersion; _umbracoApplicationLifetime = umbracoApplicationLifetime; _contentService = contentService; @@ -66,7 +67,7 @@ namespace Umbraco.Web.Install.InstallSteps private async Task<(string packageFile, int packageId)> DownloadPackageFilesAsync(Guid kitGuid) { //Go get the package file from the package repo - var packageFile = await _packageService.FetchPackageFileAsync(kitGuid, _umbracoVersion.Current, _umbracoContextAccessor.UmbracoContext.Security.GetUserId().ResultOr(0)); + var packageFile = await _packageService.FetchPackageFileAsync(kitGuid, _umbracoVersion.Current, _webSecurity.GetUserId().ResultOr(0)); if (packageFile == null) throw new InvalidOperationException("Could not fetch package file " + kitGuid); //add an entry to the installedPackages.config @@ -76,7 +77,7 @@ namespace Umbraco.Web.Install.InstallSteps _packageService.SaveInstalledPackage(packageDefinition); - _packageService.InstallCompiledPackageFiles(packageDefinition, packageFile, _umbracoContextAccessor.UmbracoContext.Security.GetUserId().ResultOr(-1)); + _packageService.InstallCompiledPackageFiles(packageDefinition, packageFile, _webSecurity.GetUserId().ResultOr(-1)); return (compiledPackage.PackageFile.Name, packageDefinition.Id); } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitInstallStep.cs index 4e14da30b7..daf8255132 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/StarterKitInstallStep.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Umbraco.Net; using Umbraco.Core.Services; using Umbraco.Web.Install.Models; +using Umbraco.Web.Security; namespace Umbraco.Web.Install.InstallSteps { @@ -14,13 +15,13 @@ namespace Umbraco.Web.Install.InstallSteps internal class StarterKitInstallStep : InstallSetupStep { private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly IPackagingService _packagingService; - public StarterKitInstallStep(IUmbracoApplicationLifetime umbracoApplicationLifetime, IUmbracoContextAccessor umbracoContextAccessor, IPackagingService packagingService) + public StarterKitInstallStep(IUmbracoApplicationLifetime umbracoApplicationLifetime, IWebSecurity webSecurity, IPackagingService packagingService) { _umbracoApplicationLifetime = umbracoApplicationLifetime; - _umbracoContextAccessor = umbracoContextAccessor; + _webSecurity = webSecurity; _packagingService = packagingService; } @@ -47,7 +48,7 @@ namespace Umbraco.Web.Install.InstallSteps var packageFile = new FileInfo(definition.PackagePath); - _packagingService.InstallCompiledPackageData(definition, packageFile, _umbracoContextAccessor.UmbracoContext.Security.GetUserId().ResultOr(-1)); + _packagingService.InstallCompiledPackageData(definition, packageFile, _webSecurity.GetUserId().ResultOr(-1)); } public override bool RequiresExecution(object model) diff --git a/src/Umbraco.Infrastructure/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Infrastructure/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 2cb226bec2..9045be20aa 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -9,6 +9,7 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Core.Dictionary; using Umbraco.Core.Configuration; using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Security; namespace Umbraco.Web.Models.Mapping { @@ -22,7 +23,7 @@ namespace Umbraco.Web.Models.Mapping /// public class MemberTabsAndPropertiesMapper : TabsAndPropertiesMapper { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly ILocalizedTextService _localizedTextService; private readonly IMemberTypeService _memberTypeService; private readonly IMemberService _memberService; @@ -31,7 +32,7 @@ namespace Umbraco.Web.Models.Mapping private readonly PropertyEditorCollection _propertyEditorCollection; public MemberTabsAndPropertiesMapper(ICultureDictionary cultureDictionary, - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, ILocalizedTextService localizedTextService, IMemberTypeService memberTypeService, IMemberService memberService, @@ -41,7 +42,7 @@ namespace Umbraco.Web.Models.Mapping PropertyEditorCollection propertyEditorCollection) : base(cultureDictionary, localizedTextService, contentTypeBaseServiceProvider) { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); @@ -74,10 +75,8 @@ namespace Umbraco.Web.Models.Mapping isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); } - var umbracoContext = _umbracoContextAccessor.UmbracoContext; - if (umbracoContext != null - && umbracoContext.Security.CurrentUser != null - && umbracoContext.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) + if (_webSecurity.CurrentUser != null + && _webSecurity.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) { var memberTypeLink = string.Format("#/member/memberTypes/edit/{0}", source.ContentTypeId); @@ -185,15 +184,13 @@ namespace Umbraco.Web.Models.Mapping var member = (IMember)content; var memberType = _memberTypeService.Get(member.ContentTypeId); - var umbracoContext = _umbracoContextAccessor.UmbracoContext; - // now update the IsSensitive value foreach (var prop in result) { // check if this property is flagged as sensitive var isSensitiveProperty = memberType.IsSensitiveProperty(prop.Alias); // check permissions for viewing sensitive data - if (isSensitiveProperty && (umbracoContext == null || umbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false)) + if (isSensitiveProperty && (_webSecurity.CurrentUser.HasAccessToSensitiveData() == false)) { // mark this property as sensitive prop.IsSensitive = true; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 70490af1a3..360c2be9fb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.PropertyEditors private readonly IContentTypeService _contentTypeService; private readonly IIOHelper _ioHelper; - internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; + public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; public NestedContentPropertyEditor( ILogger logger, diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs index adff28a2ba..ec5424ad9a 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs @@ -9,15 +9,28 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// internal class ContentNestedData { - [JsonProperty("properties")] + //dont serialize empty properties + [JsonProperty("pd")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] public Dictionary PropertyData { get; set; } - [JsonProperty("cultureData")] + [JsonProperty("cd")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] public Dictionary CultureData { get; set; } - [JsonProperty("urlSegment")] + [JsonProperty("us")] public string UrlSegment { get; set; } + + //Legacy properties used to deserialize existing nucache db entries + [JsonProperty("properties")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + private Dictionary LegacyPropertyData { set { PropertyData = value; } } + + [JsonProperty("cultureData")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + private Dictionary LegacyCultureData { set { CultureData = value; } } + + [JsonProperty("urlSegment")] + private string LegacyUrlSegment { set { UrlSegment = value; } } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs index 2521d42744..b59e8c403c 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs @@ -8,16 +8,29 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// public class CultureVariation { - [JsonProperty("name")] + [JsonProperty("nm")] public string Name { get; set; } - [JsonProperty("urlSegment")] + [JsonProperty("us")] public string UrlSegment { get; set; } - [JsonProperty("date")] + [JsonProperty("dt")] public DateTime Date { get; set; } - [JsonProperty("isDraft")] + [JsonProperty("isd")] public bool IsDraft { get; set; } + + //Legacy properties used to deserialize existing nucache db entries + [JsonProperty("name")] + private string LegacyName { set { Name = value; } } + + [JsonProperty("urlSegment")] + private string LegacyUrlSegment { set { UrlSegment = value; } } + + [JsonProperty("date")] + private DateTime LegacyDate { set { Date = value; } } + + [JsonProperty("isDraft")] + private bool LegacyIsDraft { set { IsDraft = value; } } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs index 9ab9401d71..cf7ab95360 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Newtonsoft.Json; namespace Umbraco.Web.PublishedCache.NuCache.DataSource @@ -8,21 +9,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private string _culture; private string _segment; - [JsonProperty("culture")] + [DefaultValue("")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")] public string Culture { get => _culture; set => _culture = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null } - [JsonProperty("seg")] + [DefaultValue("")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")] public string Segment { get => _segment; set => _segment = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null } - [JsonProperty("val")] + [JsonProperty("v")] public object Value { get; set; } + + + //Legacy properties used to deserialize existing nucache db entries + [JsonProperty("culture")] + private string LegacyCulture + { + set => Culture = value; + } + + [JsonProperty("seg")] + private string LegacySegment + { + set => Segment = value; + } + + [JsonProperty("val")] + private object LegacyValue + { + set => Value = value; + } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs index c91660563e..f087a4851c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttributeTests.cs @@ -96,20 +96,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Filters .SetupGet(x => x.CurrentUser) .Returns(currentUserMock.Object); - var umbracoContextMock = new Mock(); - umbracoContextMock - .SetupGet(x => x.Security) - .Returns(webSecurityMock.Object); - - var umbracoContextAccessorMock = new Mock(); - umbracoContextAccessorMock - .SetupGet(x => x.UmbracoContext) - .Returns(umbracoContextMock.Object); var serviceProviderMock = new Mock(); serviceProviderMock - .Setup(x => x.GetService(typeof(IUmbracoContextAccessor))) - .Returns(umbracoContextAccessorMock.Object); + .Setup(x => x.GetService(typeof(IWebSecurity))) + .Returns(webSecurityMock.Object); httpContext.RequestServices = serviceProviderMock.Object; diff --git a/src/Umbraco.Tests/Composing/TypeFinderTests.cs b/src/Umbraco.Tests/Composing/TypeFinderTests.cs index aad8337d00..a872ad03b7 100644 --- a/src/Umbraco.Tests/Composing/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeFinderTests.cs @@ -70,10 +70,10 @@ namespace Umbraco.Tests.Composing Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree] typesFound = typeFinder.FindClassesWithAttribute(new[] { typeof (UmbracoContext).Assembly }); - Assert.AreEqual(22, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(21, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] typesFound = typeFinder.FindClassesWithAttribute(); - Assert.AreEqual(22, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] + Assert.AreEqual(21, typesFound.Count()); // + classes in Umbraco.Web are marked with [Tree] } private static IProfilingLogger GetTestProfilingLogger() diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 0d98574504..ab9f85aa3c 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -99,8 +99,8 @@ namespace Umbraco.Tests.Services private void AssertJsonStartsWith(int id, string expected) { var json = GetJson(id).Replace('"', '\''); - var pos = json.IndexOf("'cultureData':", StringComparison.InvariantCultureIgnoreCase); - json = json.Substring(0, pos + "'cultureData':".Length); + var pos = json.IndexOf("'cd':", StringComparison.InvariantCultureIgnoreCase); + json = json.Substring(0, pos + "'cd':".Length); Assert.AreEqual(expected, json); } @@ -606,7 +606,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch content type to Nothing contentType.Variations = ContentVariation.Nothing; @@ -623,7 +623,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); // switch content back to Culture contentType.Variations = ContentVariation.Culture; @@ -640,7 +640,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); // switch property back to Culture contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; @@ -656,7 +656,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); } [Test] @@ -697,7 +697,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); // switch content type to Culture contentType.Variations = ContentVariation.Culture; @@ -713,7 +713,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); // switch property to Culture contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; @@ -728,7 +728,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); // switch content back to Nothing contentType.Variations = ContentVariation.Nothing; @@ -745,7 +745,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); } [Test] @@ -783,7 +783,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch property type to Nothing contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; @@ -800,7 +800,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); // switch property back to Culture contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; @@ -816,7 +816,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch other property to Culture contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture; @@ -834,7 +834,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'c':'en','v':'v2'}]},'cd':"); } [TestCase(ContentVariation.Culture, ContentVariation.Nothing)] @@ -1065,7 +1065,7 @@ namespace Umbraco.Tests.Services // both value11 and value21 are variant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composed.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composed); @@ -1073,7 +1073,7 @@ namespace Umbraco.Tests.Services // both value11 and value21 are invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); composed.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed); @@ -1081,7 +1081,7 @@ namespace Umbraco.Tests.Services // value11 is variant again, but value21 is still invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed); @@ -1089,7 +1089,7 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composing); @@ -1097,7 +1097,7 @@ namespace Umbraco.Tests.Services // value11 is invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1105,7 +1105,7 @@ namespace Umbraco.Tests.Services // value11 is still invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1113,7 +1113,7 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); } [Test] @@ -1178,11 +1178,11 @@ namespace Umbraco.Tests.Services // both value11 and value21 are variant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composed1.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composed1); @@ -1190,11 +1190,11 @@ namespace Umbraco.Tests.Services // both value11 and value21 are invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composed1.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed1); @@ -1202,11 +1202,11 @@ namespace Umbraco.Tests.Services // value11 is variant again, but value21 is still invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composed1.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed1); @@ -1214,11 +1214,11 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composing.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composing); @@ -1226,11 +1226,11 @@ namespace Umbraco.Tests.Services // value11 is invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composing.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1238,11 +1238,11 @@ namespace Umbraco.Tests.Services // value11 is still invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1250,11 +1250,11 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); } private void CreateFrenchAndEnglishLangs() diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index bbc869fc65..531c4e24bc 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -313,6 +313,7 @@ namespace Umbraco.Tests.Testing Composition.RegisterUnique(); Composition.RegisterUnique(); + Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 3a3c936cbe..9e6691ee48 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -1,10 +1,7 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Net; -using System.Security.Claims; -using System.Security.Principal; using System.Threading.Tasks; -using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; @@ -25,11 +22,11 @@ namespace Umbraco.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] // TODO: Maybe this could be applied with our Application Model conventions //[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied - [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] // TODO: This could be applied with our Application Model conventions + [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions [IsBackOffice] // TODO: This could be applied with our Application Model conventions public class AuthenticationController : UmbracoApiControllerBase { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly BackOfficeUserManager _userManager; private readonly BackOfficeSignInManager _signInManager; private readonly IUserService _userService; @@ -40,14 +37,14 @@ namespace Umbraco.Web.BackOffice.Controllers // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here public AuthenticationController( - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, BackOfficeUserManager backOfficeUserManager, BackOfficeSignInManager signInManager, IUserService userService, UmbracoMapper umbracoMapper, IGlobalSettings globalSettings) { - _umbracoContextAccessor = umbracoContextAccessor; + _webSecurity = webSecurity; _userManager = backOfficeUserManager; _signInManager = signInManager; _userService = userService; @@ -62,8 +59,7 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpGet] public bool IsAuthenticated() { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var attempt = umbracoContext.Security.AuthorizeRequest(); + var attempt = _webSecurity.AuthorizeRequest(); if (attempt == ValidateRequestAttempt.Success) { return true; diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 2d0ffc5d33..a02220774d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,25 +6,26 @@ using System.Net; using System.Net.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; - using Umbraco.Core.Configuration; - using Umbraco.Core.Configuration.Legacy; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Legacy; using Umbraco.Core.IO; - using Umbraco.Core.Mapping; - using Umbraco.Core.Models; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; using Umbraco.Core.Services; - using Umbraco.Core.Strings; - using Umbraco.Core.Strings.Css; - using Umbraco.Extensions; - using Umbraco.Web.Models.ContentEditing; +using Umbraco.Core.Strings; +using Umbraco.Core.Strings.Css; +using Umbraco.Extensions; +using Umbraco.Web.Models.ContentEditing; using Stylesheet = Umbraco.Core.Models.Stylesheet; using StylesheetRule = Umbraco.Web.Models.ContentEditing.StylesheetRule; using Umbraco.Web.BackOffice.Filters; - using Umbraco.Web.Common.ActionsResults; - using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.ActionsResults; +using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; +using Umbraco.Web.BackOffice.Trees; - namespace Umbraco.Web.BackOffice.Controllers +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/ diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 6a8e37a8ec..e204a3431e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -27,7 +27,7 @@ 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")] [ValidationFilter] - [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] // TODO: This could be applied with our Application Model conventions + [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions [IsBackOffice] [UmbracoAuthorize] public class DashboardController : UmbracoApiController diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs index 9bc2be8a39..97dc74ac31 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs @@ -1,25 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Services; -using Umbraco.Core.Strings; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; -using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.Routing; -using Umbraco.Web.WebApi.Filters; +using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { @@ -33,7 +26,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IImageUrlGenerator _imageUrlGenerator; private readonly IAuditService _auditService; private readonly UmbracoMapper _umbracoMapper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly IUserService _userService; private readonly AppCaches _appCaches; private readonly ISqlContext _sqlContext; @@ -43,7 +36,7 @@ namespace Umbraco.Web.BackOffice.Controllers IImageUrlGenerator imageUrlGenerator, IAuditService auditService, UmbracoMapper umbracoMapper, - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, IUserService userService, AppCaches appCaches, ISqlContext sqlContext) @@ -52,7 +45,7 @@ namespace Umbraco.Web.BackOffice.Controllers _imageUrlGenerator = imageUrlGenerator ?? throw new ArgumentNullException(nameof(imageUrlGenerator)); _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); _sqlContext = sqlContext ?? throw new ArgumentNullException(nameof(sqlContext)); @@ -95,9 +88,8 @@ namespace Umbraco.Web.BackOffice.Controllers } long totalRecords; - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); var dateQuery = sinceDate.HasValue ? _sqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; - var userId = umbracoContext.Security.GetUserId().ResultOr(0); + var userId = _webSecurity.GetUserId().ResultOr(0); var result = _auditService.GetPagedItemsByUser(userId, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter:dateQuery); var mapped = _umbracoMapper.MapEnumerable(result); return new PagedResult(totalRecords, pageNumber, pageSize) diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 23ba2e5771..8994046cb2 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -10,13 +10,12 @@ using Microsoft.Net.Http.Headers; using Semver; using Umbraco.Core; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; using Umbraco.Core.Models.Packaging; using Umbraco.Core.Services; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.Editors; +using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { @@ -29,16 +28,16 @@ namespace Umbraco.Web.BackOffice.Controllers { private readonly IHostingEnvironment _hostingEnvironment; private readonly IPackagingService _packagingService; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; public PackageController( IHostingEnvironment hostingEnvironment, IPackagingService packagingService, - IUmbracoContextAccessor umbracoContextAccessor) + IWebSecurity webSecurity) { _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _packagingService = packagingService ?? throw new ArgumentNullException(nameof(packagingService)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); } public IEnumerable GetCreatedPackages() @@ -92,8 +91,7 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpDelete] public IActionResult DeleteCreatedPackage(int packageId) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - _packagingService.DeleteCreatedPackage(packageId, umbracoContext.Security.GetUserId().ResultOr(0)); + _packagingService.DeleteCreatedPackage(packageId, _webSecurity.GetUserId().ResultOr(0)); return Ok(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 5330d4466f..0216e6f09d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -11,7 +10,6 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; -using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Packaging; using Umbraco.Net; using Umbraco.Core.Packaging; @@ -20,9 +18,9 @@ using Umbraco.Core.WebAssets; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.Editors; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { @@ -40,7 +38,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IRuntimeMinifier _runtimeMinifier; private readonly IPackagingService _packagingService; private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly ILocalizedTextService _localizedTextService; public PackageInstallController( @@ -50,7 +48,7 @@ namespace Umbraco.Web.BackOffice.Controllers IRuntimeMinifier runtimeMinifier, IPackagingService packagingService, ILogger logger, - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, ILocalizedTextService localizedTextService) { _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); @@ -59,7 +57,7 @@ namespace Umbraco.Web.BackOffice.Controllers _runtimeMinifier = runtimeMinifier ?? throw new ArgumentNullException(nameof(runtimeMinifier)); _packagingService = packagingService ?? throw new ArgumentNullException(nameof(packagingService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } @@ -89,15 +87,14 @@ namespace Umbraco.Web.BackOffice.Controllers var package = _packagingService.GetInstalledPackageById(packageId); if (package == null) return NotFound(); - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var summary = _packagingService.UninstallPackage(package.Name, umbracoContext.Security.GetUserId().ResultOr(0)); + var summary = _packagingService.UninstallPackage(package.Name, _webSecurity.GetUserId().ResultOr(0)); //now get all other packages by this name since we'll uninstall all versions foreach (var installed in _packagingService.GetAllInstalledPackages() .Where(x => x.Name == package.Name && x.Id != package.Id)) { //remove from the xml - _packagingService.DeleteInstalledPackage(installed.Id, umbracoContext.Security.GetUserId().ResultOr(0)); + _packagingService.DeleteInstalledPackage(installed.Id, _webSecurity.GetUserId().ResultOr(0)); } } catch (Exception ex) @@ -223,11 +220,10 @@ namespace Umbraco.Web.BackOffice.Controllers string fileName = packageGuid + ".umb"; if (System.IO.File.Exists(Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), fileName)) == false) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); var packageFile = await _packagingService.FetchPackageFileAsync( Guid.Parse(packageGuid), _umbracoVersion.Current, - umbracoContext.Security.GetUserId().ResultOr(0)); + _webSecurity.GetUserId().ResultOr(0)); fileName = packageFile.Name; } @@ -314,8 +310,7 @@ namespace Umbraco.Web.BackOffice.Controllers if (definition == null) throw new InvalidOperationException("Not package definition found with id " + model.Id); var zipFile = new FileInfo(definition.PackagePath); - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var installedFiles = _packagingService.InstallCompiledPackageFiles(definition, zipFile, umbracoContext.Security.GetUserId().ResultOr(0)); + var installedFiles = _packagingService.InstallCompiledPackageFiles(definition, zipFile, _webSecurity.GetUserId().ResultOr(0)); //set a restarting marker and reset the app pool _umbracoApplicationLifetime.Restart(); @@ -347,8 +342,7 @@ namespace Umbraco.Web.BackOffice.Controllers if (definition == null) throw new InvalidOperationException("Not package definition found with id " + model.Id); var zipFile = new FileInfo(definition.PackagePath); - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var installSummary = _packagingService.InstallCompiledPackageData(definition, zipFile, umbracoContext.Security.GetUserId().ResultOr(0)); + var installSummary = _packagingService.InstallCompiledPackageData(definition, zipFile, _webSecurity.GetUserId().ResultOr(0)); return model; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 9000dfd911..3c5ac1c007 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -15,7 +15,7 @@ using Umbraco.Web.Common.Filters; using Umbraco.Web.Editors; using Umbraco.Web.Features; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Trees; +using Umbraco.Web.Security; using Umbraco.Web.WebAssets; using Constants = Umbraco.Core.Constants; @@ -28,7 +28,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly UmbracoFeatures _features; private readonly IGlobalSettings _globalSettings; private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly ILocalizationService _localizationService; private readonly IUmbracoVersion _umbracoVersion; private readonly IContentSettings _contentSettings; @@ -44,7 +44,7 @@ namespace Umbraco.Web.BackOffice.Controllers UmbracoFeatures features, IGlobalSettings globalSettings, IPublishedSnapshotService publishedSnapshotService, - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, ILocalizationService localizationService, IUmbracoVersion umbracoVersion, IContentSettings contentSettings, @@ -59,7 +59,7 @@ namespace Umbraco.Web.BackOffice.Controllers _features = features; _globalSettings = globalSettings; _publishedSnapshotService = publishedSnapshotService; - _umbracoContextAccessor = umbracoContextAccessor; + _webSecurity = webSecurity; _localizationService = localizationService; _umbracoVersion = umbracoVersion; _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings)); @@ -112,7 +112,7 @@ namespace Umbraco.Web.BackOffice.Controllers [UmbracoAuthorize] public ActionResult Frame(int id, string culture) { - var user = _umbracoContextAccessor.UmbracoContext.Security.CurrentUser; + var user = _webSecurity.CurrentUser; var previewToken = _publishedSnapshotService.EnterPreview(user, id); diff --git a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs index c749e85839..bee20f58e7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Mapping; using Umbraco.Core.Services; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { @@ -19,21 +20,21 @@ namespace Umbraco.Web.BackOffice.Controllers { private readonly ILogger _logger; private readonly IWebRoutingSettings _webRoutingSettings; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly IRedirectUrlService _redirectUrlService; private readonly UmbracoMapper _umbracoMapper; private readonly IHostingEnvironment _hostingEnvironment; public RedirectUrlManagementController(ILogger logger, IWebRoutingSettings webRoutingSettings, - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, IRedirectUrlService redirectUrlService, UmbracoMapper umbracoMapper, IHostingEnvironment hostingEnvironment) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _redirectUrlService = redirectUrlService ?? throw new ArgumentNullException(nameof(redirectUrlService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); @@ -47,8 +48,7 @@ namespace Umbraco.Web.BackOffice.Controllers public IActionResult GetEnableState() { var enabled = _webRoutingSettings.DisableRedirectUrlTracking == false; - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var userIsAdmin = umbracoContext.Security.CurrentUser.IsAdmin(); + var userIsAdmin = _webSecurity.CurrentUser.IsAdmin(); return Ok(new { enabled, userIsAdmin }); } @@ -104,8 +104,7 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpPost] public IActionResult ToggleUrlTracker(bool disable) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var userIsAdmin = umbracoContext.Security.CurrentUser.IsAdmin(); + var userIsAdmin = _webSecurity.CurrentUser.IsAdmin(); if (userIsAdmin == false) { var errorMessage = "User is not a member of the administrators group and so is not allowed to toggle the URL tracker"; diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs similarity index 53% rename from src/Umbraco.Web/Editors/SectionController.cs rename to src/Umbraco.Web.BackOffice/Controllers/SectionController.cs index f352d9154f..eb1694c34f 100644 --- a/src/Umbraco.Web/Editors/SectionController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs @@ -1,62 +1,70 @@ using System.Collections.Generic; -using Umbraco.Web.Mvc; using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Trees; -using Section = Umbraco.Web.Models.ContentEditing.Section; -using Umbraco.Web.Models.Trees; -using Umbraco.Web.Services; +using Microsoft.AspNetCore.Mvc.Controllers; using Umbraco.Core.Mapping; -using Umbraco.Web.Routing; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Security; +using Umbraco.Web.Services; +using Umbraco.Web.Trees; namespace Umbraco.Web.Editors { /// - /// The API controller used for using the list of sections + /// The API controller used for using the list of sections /// [PluginController("UmbracoApi")] public class SectionController : UmbracoAuthorizedJsonController { + private readonly IControllerFactory _controllerFactory; private readonly IDashboardService _dashboardService; + private readonly ILocalizedTextService _localizedTextService; private readonly ISectionService _sectionService; private readonly ITreeService _treeService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IWebSecurity _webSecurity; - public SectionController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, - IDashboardService dashboardService, ISectionService sectionService, ITreeService treeService, IShortStringHelper shortStringHelper, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + public SectionController( + IWebSecurity webSecurity, + ILocalizedTextService localizedTextService, + IDashboardService dashboardService, ISectionService sectionService, ITreeService treeService, + UmbracoMapper umbracoMapper, IControllerFactory controllerFactory) { + _webSecurity = webSecurity; + _localizedTextService = localizedTextService; _dashboardService = dashboardService; _sectionService = sectionService; _treeService = treeService; + _umbracoMapper = umbracoMapper; + _controllerFactory = controllerFactory; } public IEnumerable
GetSections() { - var sections = _sectionService.GetAllowedSections(Security.GetUserId().ResultOr(0)); + var sections = _sectionService.GetAllowedSections(_webSecurity.GetUserId().ResultOr(0)); - var sectionModels = sections.Select(Mapper.Map
).ToArray(); + var sectionModels = sections.Select(_umbracoMapper.Map
).ToArray(); // this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that // since tree's by nature are controllers and require request contextual data - var appTreeController = new ApplicationTreeController(GlobalSettings, UmbracoContextAccessor, SqlContext, Services, AppCaches, Logger, RuntimeState, _treeService, _sectionService, Mapper, PublishedUrlProvider) - { - ControllerContext = ControllerContext - }; + var appTreeController = + new ApplicationTreeController(_treeService, _sectionService, _localizedTextService, _controllerFactory) + { + ControllerContext = ControllerContext + }; - var dashboards = _dashboardService.GetDashboards(Security.CurrentUser); + var dashboards = _dashboardService.GetDashboards(_webSecurity.CurrentUser); //now we can add metadata for each section so that the UI knows if there's actually anything at all to render for //a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree) foreach (var section in sectionModels) { - var hasDashboards = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && dashboardsForSection.Any(); + var hasDashboards = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && + dashboardsForSection.Any(); if (hasDashboards) continue; // get the first tree in the section and get its root node route path @@ -68,7 +76,7 @@ namespace Umbraco.Web.Editors } /// - /// Returns the first non root/group node's route path + /// Returns the first non root/group node's route path /// /// /// @@ -77,30 +85,28 @@ namespace Umbraco.Web.Editors if (!rootNode.IsContainer || !rootNode.ContainsTrees) return rootNode.RoutePath; - foreach(var node in rootNode.Children) + foreach (var node in rootNode.Children) { if (node is TreeRootNode groupRoot) - return GetRoutePathForFirstTree(groupRoot);//recurse to get the first tree in the group - else - return node.RoutePath; + return GetRoutePathForFirstTree(groupRoot); //recurse to get the first tree in the group + return node.RoutePath; } return string.Empty; } /// - /// Returns all the sections that the user has access to + /// Returns all the sections that the user has access to /// /// public IEnumerable
GetAllSections() { var sections = _sectionService.GetSections(); - var mapped = sections.Select(Mapper.Map
); - if (Security.CurrentUser.IsAdmin()) + var mapped = sections.Select(_umbracoMapper.Map
); + if (_webSecurity.CurrentUser.IsAdmin()) return mapped; - return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray(); + return mapped.Where(x => _webSecurity.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray(); } - } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs index 0288804d7d..bf8e89ae0d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Services; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models; +using Umbraco.Web.Security; using Umbraco.Web.Tour; namespace Umbraco.Web.BackOffice.Controllers @@ -18,21 +19,21 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly TourFilterCollection _filters; private readonly IHostingEnvironment _hostingEnvironment; private readonly ITourSettings _tourSettings; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly IContentTypeService _contentTypeService; public TourController( TourFilterCollection filters, IHostingEnvironment hostingEnvironment, ITourSettings tourSettings, - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, IContentTypeService contentTypeService) { _filters = filters; _hostingEnvironment = hostingEnvironment; _tourSettings = tourSettings; - _umbracoContextAccessor = umbracoContextAccessor; + _webSecurity = webSecurity; _contentTypeService = contentTypeService; } @@ -43,7 +44,7 @@ namespace Umbraco.Web.BackOffice.Controllers if (_tourSettings.EnableTours == false) return result; - var user = _umbracoContextAccessor.UmbracoContext.Security.CurrentUser; + var user = _webSecurity.CurrentUser; if (user == null) return result; @@ -185,7 +186,7 @@ namespace Umbraco.Web.BackOffice.Controllers var backOfficeTours = tours.Where(x => aliasFilters.Count == 0 || aliasFilters.All(filter => filter.IsMatch(x.Alias)) == false); - var user = _umbracoContextAccessor.UmbracoContext.Security.CurrentUser; + var user = _webSecurity.CurrentUser; var localizedTours = backOfficeTours.Where(x => string.IsNullOrWhiteSpace(x.Culture) || x.Culture.Equals(user.Language, diff --git a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs index 6a68651a50..2c1b106aa3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedJsonController.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// methods that are not called by Angular or don't contain a valid csrf header will NOT work. /// [TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))] - [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] // TODO: This could be applied with our Application Model conventions + [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions public abstract class UmbracoAuthorizedJsonController : UmbracoAuthorizedApiController { diff --git a/src/Umbraco.Web.BackOffice/Extensions/CompositionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/CompositionExtensions.cs new file mode 100644 index 0000000000..5cc481c018 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Extensions/CompositionExtensions.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.Composing; +using Umbraco.Web.Trees; + +// the namespace here is intentional - although defined in Umbraco.Web assembly, +// this class should be visible when using Umbraco.Core.Components, alongside +// Umbraco.Core's own CompositionExtensions class + +// ReSharper disable once CheckNamespace +namespace Umbraco.Extensions +{ + /// + /// Provides extension methods to the class. + /// + public static class WebCompositionExtensions + { + #region Collection Builders + + /// + /// Gets the back office tree collection builder + /// + /// + /// + public static TreeCollectionBuilder Trees(this Composition composition) + => composition.WithCollectionBuilder(); + + #endregion + + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs index 50ef8cf906..6541d122ab 100644 --- a/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/AppendUserModifiedHeaderAttribute.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core; +using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters { @@ -42,8 +43,8 @@ namespace Umbraco.Web.BackOffice.Filters throw new InvalidOperationException($"No argument found for the current action with the name: {_userIdParameter}"); } - var umbracoContextAccessor = context.HttpContext.RequestServices.GetService(); - var user = umbracoContextAccessor.UmbracoContext.Security.CurrentUser; + var webSecurity = context.HttpContext.RequestServices.GetService(); + var user = webSecurity.CurrentUser; if (user == null) { return; diff --git a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs index 5c9e646ba0..d433ba9886 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Mvc; +using System; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core; using Umbraco.Web.Editors; +using Umbraco.Web.Security; namespace Umbraco.Web.WebApi.Filters { @@ -11,10 +13,12 @@ namespace Umbraco.Web.WebApi.Filters internal sealed class OutgoingEditorModelEventAttribute : ActionFilterAttribute { private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; - public OutgoingEditorModelEventAttribute(IUmbracoContextAccessor umbracoContextAccessor) + public OutgoingEditorModelEventAttribute(IUmbracoContextAccessor umbracoContextAccessor, IWebSecurity webSecurity) { - _umbracoContextAccessor = umbracoContextAccessor; + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); } public override void OnActionExecuted(ActionExecutedContext context) @@ -22,7 +26,7 @@ namespace Umbraco.Web.WebApi.Filters if (context.Result == null) return; var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var user = umbracoContext.Security.CurrentUser; + var user = _webSecurity.CurrentUser; if (user == null) return; if (context.Result is ObjectResult objectContent) @@ -41,6 +45,5 @@ namespace Umbraco.Web.WebApi.Filters base.OnActionExecuted(context); } - } } diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs index 81e61af5bf..4465436e77 100644 --- a/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs @@ -1,9 +1,7 @@ -using System; -using System.Linq; +using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core; +using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters { @@ -24,18 +22,19 @@ namespace Umbraco.Web.BackOffice.Filters /// internal static bool Enable = true; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly string[] _appNames; /// /// Constructor to set any number of applications that the user needs access to be authorized /// + /// /// /// If the user has access to any of the specified apps, they will be authorized. /// - public UmbracoApplicationAuthorizeFilter(IUmbracoContextAccessor umbracoContextAccessor, params string[] appName) + public UmbracoApplicationAuthorizeFilter(IWebSecurity webSecurity, params string[] appName) { - _umbracoContextAccessor = umbracoContextAccessor; + _webSecurity = webSecurity; _appNames = appName; } @@ -55,10 +54,9 @@ namespace Umbraco.Web.BackOffice.Filters return true; } - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var authorized = umbracoContext.Security.CurrentUser != null - && _appNames.Any(app => umbracoContext.Security.UserHasSectionAccess( - app, umbracoContext.Security.CurrentUser)); + var authorized = _webSecurity.CurrentUser != null + && _appNames.Any(app => _webSecurity.UserHasSectionAccess( + app, _webSecurity.CurrentUser)); return authorized; } diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs index 6db37d16f6..d6b18b4b29 100644 --- a/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs @@ -1,7 +1,9 @@ -using System.Linq; +using System; +using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core; +using Umbraco.Web.Security; using Umbraco.Web.Services; namespace Umbraco.Web.BackOffice.Filters @@ -31,22 +33,22 @@ namespace Umbraco.Web.BackOffice.Filters internal static bool Enable = true; private readonly ITreeService _treeService; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly string[] _treeAliases; /// /// Constructor to set authorization to be based on a tree alias for which application security will be applied /// - /// + /// /// /// If the user has access to the application that the treeAlias is specified in, they will be authorized. /// Multiple trees may be specified. /// /// - public UmbracoTreeAuthorizeFilter(ITreeService treeService, IUmbracoContextAccessor umbracoContextAccessor, params string[] treeAliases) + public UmbracoTreeAuthorizeFilter(ITreeService treeService, IWebSecurity webSecurity, params string[] treeAliases) { - _treeService = treeService; - _umbracoContextAccessor = umbracoContextAccessor; + _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _treeAliases = treeAliases; } @@ -64,10 +66,9 @@ namespace Umbraco.Web.BackOffice.Filters .Distinct() .ToArray(); - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - return umbracoContext.Security.CurrentUser != null - && apps.Any(app => umbracoContext.Security.UserHasSectionAccess( - app, umbracoContext.Security.CurrentUser)); + return _webSecurity.CurrentUser != null + && apps.Any(app => _webSecurity.UserHasSectionAccess( + app, _webSecurity.CurrentUser)); } public void OnAuthorization(AuthorizationFilterContext context) diff --git a/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs b/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs index b13cb4a192..c29ae43dbd 100644 --- a/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs +++ b/src/Umbraco.Web.BackOffice/Runtime/BackOfficeComposer.cs @@ -1,23 +1,34 @@ -using Microsoft.AspNetCore.Identity; -using System.Linq; +using System.Linq; using Umbraco.Core; -using Umbraco.Core.BackOffice; using Umbraco.Core.Composing; using Umbraco.Extensions; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Routing; using Umbraco.Web.BackOffice.Security; +using Umbraco.Web.BackOffice.Trees; +using Umbraco.Web.Common.Runtime; namespace Umbraco.Web.BackOffice.Runtime { + [ComposeBefore(typeof(ICoreComposer))] + [ComposeAfter(typeof(AspNetCoreComposer))] public class BackOfficeComposer : IComposer { public void Compose(Composition composition) { + + composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + + // register back office trees + // the collection builder only accepts types inheriting from TreeControllerBase + // and will filter out those that are not attributed with TreeAttribute + var umbracoApiControllerTypes = composition.TypeLoader.GetUmbracoApiControllers().ToList(); + composition.Trees() + .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); } } } diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs similarity index 68% rename from src/Umbraco.Web/Trees/ApplicationTreeController.cs rename to src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index d815f76b40..7728ee017a 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -3,25 +3,25 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Formatting; +using System.Reflection; using System.Threading.Tasks; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Routing; -using System.Web.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Mapping; -using Umbraco.Core.Persistence; using Umbraco.Core.Services; +using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.BackOffice.Trees; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Common.Filters; +using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.Models.Trees; -using Umbraco.Web.Mvc; -using Umbraco.Web.Routing; using Umbraco.Web.Services; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees @@ -35,15 +35,21 @@ namespace Umbraco.Web.Trees { private readonly ITreeService _treeService; private readonly ISectionService _sectionService; + private readonly ILocalizedTextService _localizedTextService; + private readonly IControllerFactory _controllerFactory; - public ApplicationTreeController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, - IRuntimeState runtimeState, ITreeService treeService, ISectionService sectionService, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider) - : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoMapper, publishedUrlProvider) - { + public ApplicationTreeController( + ITreeService treeService, + ISectionService sectionService, + ILocalizedTextService localizedTextService, + IControllerFactory controllerFactory + ) + { _treeService = treeService; _sectionService = sectionService; - } + _localizedTextService = localizedTextService; + _controllerFactory = controllerFactory; + } /// /// Returns the tree nodes for an application @@ -53,7 +59,7 @@ namespace Umbraco.Web.Trees /// /// Tree use. /// - public async Task GetApplicationTrees(string application, string tree, [System.Web.Http.ModelBinding.ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings, TreeUse use = TreeUse.Main) + public async Task GetApplicationTrees(string application, string tree, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings, TreeUse use = TreeUse.Main) { application = application.CleanForXss(); @@ -72,7 +78,7 @@ namespace Umbraco.Web.Trees { //if there are no trees defined for this section but the section is defined then we can have a simple //full screen section without trees - var name = Services.TextService.Localize("sections/" + application); + var name = _localizedTextService.Localize("sections/" + application); return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true); } @@ -105,7 +111,7 @@ namespace Umbraco.Web.Trees nodes.Add(node); } - var name = Services.TextService.Localize("sections/" + application); + var name = _localizedTextService.Localize("sections/" + application); if (nodes.Count > 0) { @@ -140,7 +146,7 @@ namespace Umbraco.Web.Trees var name = groupName.IsNullOrWhiteSpace() ? "thirdPartyGroup" : groupName; var groupRootNode = TreeRootNode.CreateGroupNode(nodes, application); - groupRootNode.Name = Services.TextService.Localize("treeHeaders/" + name); + groupRootNode.Name = _localizedTextService.Localize("treeHeaders/" + name); treeRootNodes.Add(groupRootNode); } @@ -154,7 +160,7 @@ namespace Umbraco.Web.Trees /// Returns null if the root node could not be obtained due to an HttpResponseException, /// which probably indicates that the user isn't authorized to view that tree. /// - private async Task TryGetRootNode(Tree tree, FormDataCollection querystring) + private async Task TryGetRootNode(Tree tree, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); @@ -174,7 +180,7 @@ namespace Umbraco.Web.Trees /// /// Get the tree root node of a tree. /// - private async Task GetTreeRootNode(Tree tree, int id, FormDataCollection querystring) + private async Task GetTreeRootNode(Tree tree, int id, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); @@ -203,7 +209,7 @@ namespace Umbraco.Web.Trees /// /// Gets the root node of a tree. /// - private async Task GetRootNode(Tree tree, FormDataCollection querystring) + private async Task GetRootNode(Tree tree, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); @@ -217,16 +223,16 @@ namespace Umbraco.Web.Trees /// /// Get the child nodes of a tree node. /// - private async Task GetChildren(Tree tree, int id, FormDataCollection querystring) + private async Task GetChildren(Tree tree, int id, FormCollection querystring) { if (tree == null) throw new ArgumentNullException(nameof(tree)); // the method we proxy has an 'id' parameter which is *not* in the querystring, // we need to add it for the proxy to work (else, it does not find the method, // when trying to run auth filters etc). - var d = querystring?.ToDictionary(x => x.Key, x => x.Value) ?? new Dictionary(); - d["id"] = null; - var proxyQuerystring = new FormDataCollection(d); + var d = querystring?.ToDictionary(x => x.Key, x => x.Value) ?? new Dictionary(); + d["id"] = StringValues.Empty; + var proxyQuerystring = new FormCollection(d); var controller = (TreeController) await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring); return controller.GetNodes(id.ToInvariantString(), querystring); @@ -244,49 +250,74 @@ namespace Umbraco.Web.Trees /// and context etc. so it can execute the specified . Runs the authorization /// filters for that action, to ensure that the user has permission to execute it. /// - private async Task GetApiControllerProxy(Type controllerType, string action, FormDataCollection querystring) + private async Task GetApiControllerProxy(Type controllerType, string action, FormCollection querystring) { // note: this is all required in order to execute the auth-filters for the sub request, we // need to "trick" web-api into thinking that it is actually executing the proxied controller. - var context = ControllerContext; - - // get the controller - var controller = (ApiController) DependencyResolver.Current.GetService(controllerType) - ?? throw new Exception($"Failed to create controller of type {controllerType.FullName}."); - - // create the proxy URL for the controller action - var proxyUrl = context.Request.RequestUri.GetLeftPart(UriPartial.Authority) - + context.Request.GetUrlHelper().GetUmbracoApiService(action, controllerType) - + "?" + querystring.ToQueryString(); // create proxy route data specifying the action & controller to execute - var proxyRoute = new HttpRouteData( - context.RouteData.Route, - new HttpRouteValueDictionary(new { action, controller = ControllerExtensions.GetControllerName(controllerType) })); - - // create a proxy request - var proxyRequest = new HttpRequestMessage(HttpMethod.Get, proxyUrl); - - // create a proxy controller context - var proxyContext = new HttpControllerContext(context.Configuration, proxyRoute, proxyRequest) + var routeData = new RouteData(new RouteValueDictionary() { - ControllerDescriptor = new HttpControllerDescriptor(context.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(controllerType), controllerType), - RequestContext = context.RequestContext, - Controller = controller - }; + ["action"] = action, + ["controller"] = controllerType.Name.Substring(0,controllerType.Name.Length-10) // remove controller part of name; - // wire everything - controller.ControllerContext = proxyContext; - controller.Request = proxyContext.Request; - controller.RequestContext.RouteData = proxyRoute; + }); - // auth - var authResult = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(); - if (authResult != null) - throw new HttpResponseException(authResult); + + var controllerContext = new ControllerContext( + new ActionContext( + HttpContext, + routeData, + new ControllerActionDescriptor() + { + ControllerTypeInfo = controllerType.GetTypeInfo() + } + )); + + var controller = (TreeController) _controllerFactory.CreateController(controllerContext); + + + //TODO Refactor trees or reimplement this hacks to check authentication. + //https://dev.azure.com/umbraco/D-Team%20Tracker/_workitems/edit/3694 + + // var context = ControllerContext; + // + // // get the controller + // var controller = (TreeController) DependencyResolver.Current.GetService(controllerType) + // ?? throw new Exception($"Failed to create controller of type {controllerType.FullName}."); + // + // // create the proxy URL for the controller action + // var proxyUrl = HttpContext.Request.RequestUri.GetLeftPart(UriPartial.Authority) + // + HttpContext.Request.GetUrlHelper().GetUmbracoApiService(action, controllerType) + // + "?" + querystring.ToQueryString(); + // + // + // + // // create a proxy request + // var proxyRequest = new HttpRequestMessage(HttpMethod.Get, proxyUrl); + // + // // create a proxy controller context + // var proxyContext = new HttpControllerContext(context.Configuration, proxyRoute, proxyRequest) + // { + // ControllerDescriptor = new HttpControllerDescriptor(context.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(controllerType), controllerType), + // RequestContext = context.RequestContext, + // Controller = controller + // }; + // + // // wire everything + // controller.ControllerContext = proxyContext; + // controller.Request = proxyContext.Request; + // controller.RequestContext.RouteData = proxyRoute; + // + // // auth + // var authResult = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(); + // if (authResult != null) + // throw new HttpResponseException(authResult); return controller; } + + } } diff --git a/src/Umbraco.Web/Trees/LanguageTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs similarity index 69% rename from src/Umbraco.Web/Trees/LanguageTreeController.cs rename to src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs index 944cbd103c..d34c26b07d 100644 --- a/src/Umbraco.Web/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs @@ -1,8 +1,10 @@ -using System.Net.Http.Formatting; -using Umbraco.Core; +using Microsoft.AspNetCore.Http; +using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.BackOffice.Trees; +using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi.Filters; +using Umbraco.Web.WebApi; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees @@ -13,13 +15,20 @@ namespace Umbraco.Web.Trees [CoreTree] public class LanguageTreeController : TreeController { - protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + + public LanguageTreeController( + ILocalizedTextService textService, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection) + : base(textService, umbracoApiControllerTypeCollection) + { + } + protected override TreeNodeCollection GetTreeNodes(string id, FormCollection queryStrings) { //We don't have any child nodes & only use the root node to load a custom UI return new TreeNodeCollection(); } - protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + protected override MenuItemCollection GetMenuForNode(string id, FormCollection queryStrings) { //We don't have any menu item options (such as create/delete/reload) & only use the root node to load a custom UI return null; @@ -29,7 +38,7 @@ namespace Umbraco.Web.Trees /// Helper method to create a root model for a tree /// /// - protected override TreeNode CreateRootNode(FormDataCollection queryStrings) + protected override TreeNode CreateRootNode(FormCollection queryStrings) { var root = base.CreateRootNode(queryStrings); @@ -41,5 +50,7 @@ namespace Umbraco.Web.Trees return root; } + + } } diff --git a/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs new file mode 100644 index 0000000000..74a557854f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class MenuRenderingEventArgs : TreeRenderingEventArgs + { + /// + /// The tree node id that the menu is rendering for + /// + public string NodeId { get; private set; } + + /// + /// The menu being rendered + /// + public MenuItemCollection Menu { get; private set; } + + public MenuRenderingEventArgs(string nodeId, MenuItemCollection menu, FormCollection queryStrings) + : base(queryStrings) + { + NodeId = nodeId; + Menu = menu; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs b/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs new file mode 100644 index 0000000000..ba24dea1c1 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs @@ -0,0 +1,56 @@ +using System; +using Umbraco.Web.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// Identifies a section tree. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class TreeAttribute : Attribute, ITree + { + /// + /// Initializes a new instance of the class. + /// + public TreeAttribute(string sectionAlias, string treeAlias) + { + SectionAlias = sectionAlias; + TreeAlias = treeAlias; + } + + /// + /// Gets the section alias. + /// + public string SectionAlias { get; } + + /// + /// Gets the tree alias. + /// + public string TreeAlias { get; } + + /// + /// Gets or sets the tree title. + /// + public string TreeTitle { get; set; } + + /// + /// Gets or sets the group of the tree. + /// + public string TreeGroup { get; set; } + + /// + /// Gets the usage of the tree. + /// + public TreeUse TreeUse { get; set; } = TreeUse.Main | TreeUse.Dialog; + + /// + /// Gets or sets the tree sort order. + /// + public int SortOrder { get; set; } + + /// + /// Gets or sets a value indicating whether the tree is a single-node tree (no child nodes, full screen app). + /// + public bool IsSingleNodeTree { get; set; } + } +} diff --git a/src/Umbraco.Web/Trees/TreeCollectionBuilder.cs b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs similarity index 98% rename from src/Umbraco.Web/Trees/TreeCollectionBuilder.cs rename to src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs index d324ad93b2..fb1b642bc8 100644 --- a/src/Umbraco.Web/Trees/TreeCollectionBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Web.BackOffice.Trees; namespace Umbraco.Web.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeController.cs b/src/Umbraco.Web.BackOffice/Trees/TreeController.cs new file mode 100644 index 0000000000..9a342f5519 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeController.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Web.Trees; +using Umbraco.Web.WebApi; + +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// The base controller for all tree requests + /// + public abstract class TreeController : TreeControllerBase + { + private static readonly ConcurrentDictionary _treeAttributeCache = new ConcurrentDictionary(); + + private readonly TreeAttribute _treeAttribute; + + private readonly ILocalizedTextService _textService; + + protected TreeController(ILocalizedTextService textService, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection) + : base(umbracoApiControllerTypeCollection) + { + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _treeAttribute = GetTreeAttribute(); + } + + /// + public override string RootNodeDisplayName => Tree.GetRootNodeDisplayName(this, _textService); + + /// + public override string TreeGroup => _treeAttribute.TreeGroup; + + /// + public override string TreeAlias => _treeAttribute.TreeAlias; + + /// + public override string TreeTitle => _treeAttribute.TreeTitle; + + /// + public override TreeUse TreeUse => _treeAttribute.TreeUse; + + /// + public override string SectionAlias => _treeAttribute.SectionAlias; + + /// + public override int SortOrder => _treeAttribute.SortOrder; + + /// + public override bool IsSingleNodeTree => _treeAttribute.IsSingleNodeTree; + + private TreeAttribute GetTreeAttribute() + { + return _treeAttributeCache.GetOrAdd(GetType(), type => + { + var treeAttribute = type.GetCustomAttribute(false); + if (treeAttribute == null) + throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute"); + return treeAttribute; + }); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs new file mode 100644 index 0000000000..15130002df --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs @@ -0,0 +1,401 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.Common.Filters; +using Umbraco.Web.Common.ModelBinders; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Trees; +using Umbraco.Web.WebApi; + +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// A base controller reference for non-attributed trees (un-registered). + /// + /// + /// Developers should generally inherit from TreeController. + /// + [AngularJsonOnlyConfiguration] + public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree + { + // TODO: Need to set this, but from where? + // Presumably not injecting as this will be a base controller for package/solution developers. + private readonly UmbracoApiControllerTypeCollection _apiControllers; + + protected TreeControllerBase(UmbracoApiControllerTypeCollection apiControllers) + { + _apiControllers = apiControllers; + } + + /// + /// The method called to render the contents of the tree structure + /// + /// + /// + /// All of the query string parameters passed from jsTree + /// + /// + /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end + /// to the back end to be used in the query for model data. + /// + protected abstract TreeNodeCollection GetTreeNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings); + + /// + /// Returns the menu structure for the node + /// + /// + /// + /// + protected abstract MenuItemCollection GetMenuForNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings); + + /// + /// The name to display on the root node + /// + public abstract string RootNodeDisplayName { get; } + + /// + public abstract string TreeGroup { get; } + + /// + public abstract string TreeAlias { get; } + + /// + public abstract string TreeTitle { get; } + + /// + public abstract TreeUse TreeUse { get; } + + /// + public abstract string SectionAlias { get; } + + /// + public abstract int SortOrder { get; } + + /// + public abstract bool IsSingleNodeTree { get; } + + /// + /// Returns the root node for the tree + /// + /// + /// + public TreeNode GetRootNode([ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + { + if (queryStrings == null) queryStrings = FormCollection.Empty; + var node = CreateRootNode(queryStrings); + + //add the tree alias to the root + node.AdditionalData["treeAlias"] = TreeAlias; + + AddQueryStringsToAdditionalData(node, queryStrings); + + //check if the tree is searchable and add that to the meta data as well + if (this is ISearchableTree) + node.AdditionalData.Add("searchable", "true"); + + //now update all data based on some of the query strings, like if we are running in dialog mode + if (IsDialog(queryStrings)) + node.RoutePath = "#"; + + OnRootNodeRendering(this, new TreeNodeRenderingEventArgs(node, queryStrings)); + + return node; + } + + /// + /// The action called to render the contents of the tree structure + /// + /// + /// + /// All of the query string parameters passed from jsTree + /// + /// JSON markup for jsTree + /// + /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end + /// to the back end to be used in the query for model data. + /// + public TreeNodeCollection GetNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + { + if (queryStrings == null) queryStrings = FormCollection.Empty; + var nodes = GetTreeNodes(id, queryStrings); + + foreach (var node in nodes) + AddQueryStringsToAdditionalData(node, queryStrings); + + //now update all data based on some of the query strings, like if we are running in dialog mode + if (IsDialog(queryStrings)) + foreach (var node in nodes) + node.RoutePath = "#"; + + //raise the event + OnTreeNodesRendering(this, new TreeNodesRenderingEventArgs(nodes, queryStrings)); + + return nodes; + } + + /// + /// The action called to render the menu for a tree node + /// + /// + /// + /// + public MenuItemCollection GetMenu(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + { + if (queryStrings == null) queryStrings = FormCollection.Empty; + var menu = GetMenuForNode(id, queryStrings); + //raise the event + OnMenuRendering(this, new MenuRenderingEventArgs(id, menu, queryStrings)); + return menu; + } + + /// + /// Helper method to create a root model for a tree + /// + /// + protected virtual TreeNode CreateRootNode(FormCollection queryStrings) + { + var rootNodeAsString = Constants.System.RootString; + queryStrings.TryGetValue(TreeQueryStringParameters.Application, out var currApp); + + var node = new TreeNode( + rootNodeAsString, + null, //this is a root node, there is no parent + Url.GetTreeUrl(_apiControllers, GetType(), rootNodeAsString, queryStrings), + Url.GetMenuUrl(_apiControllers, GetType(), rootNodeAsString, queryStrings)) + { + HasChildren = true, + RoutePath = currApp, + Name = RootNodeDisplayName + }; + + return node; + } + + #region Create TreeNode methods + + /// + /// Helper method to create tree nodes + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title) + { + var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings); + var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings); + var node = new TreeNode(id, parentId, jsonUrl, menuUrl) { Name = title }; + return node; + } + + /// + /// Helper method to create tree nodes + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon) + { + var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings); + var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings); + var node = new TreeNode(id, parentId, jsonUrl, menuUrl) + { + Name = title, + Icon = icon, + NodeType = TreeAlias + }; + return node; + } + + /// + /// Helper method to create tree nodes + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, string routePath) + { + var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings); + var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings); + var node = new TreeNode(id, parentId, jsonUrl, menuUrl) { Name = title, RoutePath = routePath, Icon = icon }; + return node; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(IEntitySlim entity, Guid entityObjectType, string parentId, FormCollection queryStrings, bool hasChildren) + { + var contentTypeIcon = entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, contentTypeIcon); + treeNode.Path = entity.Path; + treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); + treeNode.HasChildren = hasChildren; + treeNode.Trashed = entity.Trashed; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(IUmbracoEntity entity, Guid entityObjectType, string parentId, FormCollection queryStrings, string icon, bool hasChildren) + { + var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, icon); + treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); + treeNode.Path = entity.Path; + treeNode.HasChildren = hasChildren; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren) + { + var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon); + treeNode.HasChildren = hasChildren; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren, string routePath) + { + var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon); + treeNode.HasChildren = hasChildren; + treeNode.RoutePath = routePath; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren, string routePath, Udi udi) + { + var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon); + treeNode.HasChildren = hasChildren; + treeNode.RoutePath = routePath; + treeNode.Udi = udi; + return treeNode; + } + + #endregion + + /// + /// The AdditionalData of a node is always populated with the query string data, this method performs this + /// operation and ensures that special values are not inserted or that duplicate keys are not added. + /// + /// + /// + protected void AddQueryStringsToAdditionalData(TreeNode node, FormCollection queryStrings) + { + foreach (var q in queryStrings.Where(x => node.AdditionalData.ContainsKey(x.Key) == false)) + node.AdditionalData.Add(q.Key, q.Value); + } + + /// + /// If the request is for a dialog mode tree + /// + /// + /// + protected bool IsDialog(FormCollection queryStrings) + { + queryStrings.TryGetValue(TreeQueryStringParameters.Use, out var use); + return use == "dialog"; + } + + /// + /// An event that allows developers to modify the tree node collection that is being rendered + /// + /// + /// Developers can add/remove/replace/insert/update/etc... any of the tree items in the collection. + /// + public static event TypedEventHandler TreeNodesRendering; + + private static void OnTreeNodesRendering(TreeControllerBase instance, TreeNodesRenderingEventArgs e) + { + var handler = TreeNodesRendering; + handler?.Invoke(instance, e); + } + + /// + /// An event that allows developer to modify the root tree node that is being rendered + /// + public static event TypedEventHandler RootNodeRendering; + + // internal for temp class below - kill eventually! + internal static void OnRootNodeRendering(TreeControllerBase instance, TreeNodeRenderingEventArgs e) + { + var handler = RootNodeRendering; + handler?.Invoke(instance, e); + } + + /// + /// An event that allows developers to modify the menu that is being rendered + /// + /// + /// Developers can add/remove/replace/insert/update/etc... any of the tree items in the collection. + /// + public static event TypedEventHandler MenuRendering; + + private static void OnMenuRendering(TreeControllerBase instance, MenuRenderingEventArgs e) + { + var handler = MenuRendering; + handler?.Invoke(instance, e); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs new file mode 100644 index 0000000000..50d7b627d9 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class TreeNodeRenderingEventArgs : TreeRenderingEventArgs + { + public TreeNode Node { get; private set; } + + public TreeNodeRenderingEventArgs(TreeNode node, FormCollection queryStrings) + : base(queryStrings) + { + Node = node; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs new file mode 100644 index 0000000000..8c9cfebd83 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class TreeNodesRenderingEventArgs : TreeRenderingEventArgs + { + public TreeNodeCollection Nodes { get; private set; } + + public TreeNodesRenderingEventArgs(TreeNodeCollection nodes, FormCollection queryStrings) + : base(queryStrings) + { + Nodes = nodes; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs new file mode 100644 index 0000000000..80fba4bb34 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// Common query string parameters used for tree query strings + /// + internal struct TreeQueryStringParameters + { + public const string Use = "use"; + public const string Application = "application"; + public const string StartNodeId = "startNodeId"; + public const string DataTypeKey = "dataTypeKey"; + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs new file mode 100644 index 0000000000..a132e52dad --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class TreeRenderingEventArgs : EventArgs + { + public FormCollection QueryStrings { get; private set; } + + public TreeRenderingEventArgs(FormCollection queryStrings) + { + QueryStrings = queryStrings; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs index 3c94e3f9a0..d822688202 100644 --- a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs @@ -2,8 +2,12 @@ using System.Linq; using System.Text; using System.Web; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; +using Umbraco.Web.BackOffice.Trees; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.WebApi; namespace Umbraco.Extensions { @@ -38,5 +42,28 @@ namespace Umbraco.Extensions } return sb.ToString().TrimEnd(","); } + + public static string GetTreeUrl(this IUrlHelper urlHelper, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Type treeType, string nodeId, FormCollection queryStrings) + { + var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetNodes", treeType) + .EnsureEndsWith('?'); + + //now we need to append the query strings + actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings.ToQueryString("id", + //Always ignore the custom start node id when generating URLs for tree nodes since this is a custom once-only parameter + // that should only ever be used when requesting a tree to render (root), not a tree node + TreeQueryStringParameters.StartNodeId); + return actionUrl; + } + + public static string GetMenuUrl(this IUrlHelper urlHelper, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Type treeType, string nodeId, FormCollection queryStrings) + { + var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetMenu", treeType) + .EnsureEndsWith('?'); + + //now we need to append the query strings + actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings.ToQueryString("id"); + return actionUrl; + } } } diff --git a/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs new file mode 100644 index 0000000000..59b29ffa9b --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Http; +using Umbraco.Core; + +namespace Umbraco.Extensions +{ + public static class FormCollectionExtensions + { + /// + /// Converts a dictionary object to a query string representation such as: + /// firstname=shannon&lastname=deminick + /// + /// + /// Any keys found in this collection will be removed from the output + /// + public static string ToQueryString(this FormCollection items, params string[] keysToIgnore) + { + if (items == null) return ""; + if (items.Any() == false) return ""; + + var builder = new StringBuilder(); + foreach (var i in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false)) + builder.Append(string.Format("{0}={1}&", i.Key, i.Value)); + return builder.ToString().TrimEnd('&'); + } + + /// + /// Converts the FormCollection to a dictionary + /// + /// + /// + public static IDictionary ToDictionary(this FormCollection items) + { + return items.ToDictionary(x => x.Key, x => (object)x.Value); + } + + /// + /// Returns the value of a mandatory item in the FormCollection + /// + /// + /// + /// + public static string GetRequiredString(this FormCollection items, string key) + { + if (items.HasKey(key) == false) + throw new ArgumentNullException("The " + key + " query string parameter was not found but is required"); + return items.Single(x => x.Key.InvariantEquals(key)).Value; + } + + /// + /// Checks if the collection contains the key + /// + /// + /// + /// + public static bool HasKey(this FormCollection items, string key) + { + return items.Any(x => x.Key.InvariantEquals(key)); + } + + /// + /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert a null object is returned. + /// + /// + /// + /// + /// + public static T GetValue(this FormCollection items, string key) + { + if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val)) + { + return default; + } + + var converted = val.TryConvertTo(); + return converted.Success + ? converted.Result + : default; + } + + /// + /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert or the query string is no there an exception is thrown + /// + /// + /// + /// + /// + public static T GetRequiredValue(this FormCollection items, string key) + { + if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val)) + { + throw new InvalidOperationException($"The required query string parameter {key} is missing"); + } + + var converted = val.TryConvertTo(); + return converted.Success + ? converted.Result + : throw new InvalidOperationException($"The required query string parameter {key} cannot be converted to type {typeof(T)}"); + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs index 192b5e7df0..0eabbf0f54 100644 --- a/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs @@ -1,12 +1,8 @@ -using System.Buffers; -using System.Collections.Generic; -using System.Text.Json; + +using System.Buffers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Umbraco.Web.Common.Formatters; namespace Umbraco.Web.Common.Filters @@ -14,29 +10,40 @@ namespace Umbraco.Web.Common.Filters /// /// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. /// - public class AngularJsonOnlyConfigurationAttribute : ActionFilterAttribute + public class AngularJsonOnlyConfigurationAttribute : TypeFilterAttribute { - private readonly IOptions _mvcNewtonsoftJsonOptions; - private readonly ArrayPool _arrayPool; - private readonly IOptions _options; - - public AngularJsonOnlyConfigurationAttribute(IOptions mvcNewtonsoftJsonOptions, ArrayPool arrayPool, IOptions options) + public AngularJsonOnlyConfigurationAttribute() : base(typeof(AngularJsonOnlyConfigurationFilter)) { - _mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions; - _arrayPool = arrayPool; - _options = options; } - public override void OnResultExecuting(ResultExecutingContext context) + private class AngularJsonOnlyConfigurationFilter : IResultFilter { - if (context.Result is ObjectResult objectResult) + private readonly IOptions _mvcNewtonsoftJsonOptions; + private readonly ArrayPool _arrayPool; + private readonly IOptions _options; + + public AngularJsonOnlyConfigurationFilter(IOptions mvcNewtonsoftJsonOptions, ArrayPool arrayPool, IOptions options) { - objectResult.Formatters.Clear(); - objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(_mvcNewtonsoftJsonOptions.Value.SerializerSettings, _arrayPool, _options.Value)); + _mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions; + _arrayPool = arrayPool; + _options = options; } - base.OnResultExecuting(context); + public void OnResultExecuted(ResultExecutedContext context) + { + + } + + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult objectResult) + { + objectResult.Formatters.Clear(); + objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(_mvcNewtonsoftJsonOptions.Value.SerializerSettings, _arrayPool, _options.Value)); + } + } } } + } diff --git a/src/Umbraco.Web.Common/Install/InstallApiController.cs b/src/Umbraco.Web.Common/Install/InstallApiController.cs index c171b09fa8..207b8f49c3 100644 --- a/src/Umbraco.Web.Common/Install/InstallApiController.cs +++ b/src/Umbraco.Web.Common/Install/InstallApiController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Common.Install [UmbracoApiController] [TypeFilter(typeof(HttpResponseExceptionFilter))] - [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] + [AngularJsonOnlyConfiguration] [InstallAuthorize] [Area(Umbraco.Core.Constants.Web.Mvc.InstallArea)] public class InstallApiController : ControllerBase @@ -95,7 +95,7 @@ namespace Umbraco.Web.Common.Install /// /// Installs. - /// + /// public async Task PostPerformInstall(InstallInstructions installModel) { if (installModel == null) throw new ArgumentNullException(nameof(installModel)); diff --git a/src/Umbraco.Web.Common/Install/InstallController.cs b/src/Umbraco.Web.Common/Install/InstallController.cs index a4f659379f..f09d38440b 100644 --- a/src/Umbraco.Web.Common/Install/InstallController.cs +++ b/src/Umbraco.Web.Common/Install/InstallController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Common.Install [Area(Umbraco.Core.Constants.Web.Mvc.InstallArea)] public class InstallController : Controller { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly InstallHelper _installHelper; private readonly IRuntimeState _runtime; private readonly IGlobalSettings _globalSettings; @@ -33,7 +33,7 @@ namespace Umbraco.Web.Common.Install private readonly IRuntimeMinifier _runtimeMinifier; public InstallController( - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, InstallHelper installHelper, IRuntimeState runtime, IGlobalSettings globalSettings, @@ -43,7 +43,7 @@ namespace Umbraco.Web.Common.Install ILogger logger, LinkGenerator linkGenerator) { - _umbracoContextAccessor = umbracoContextAccessor; + _webSecurity = webSecurity; _installHelper = installHelper; _runtime = runtime; _globalSettings = globalSettings; @@ -69,7 +69,7 @@ namespace Umbraco.Web.Common.Install // Update ClientDependency version and delete its temp directories to make sure we get fresh caches _runtimeMinifier.Reset(); - var result = _umbracoContextAccessor.UmbracoContext.Security.ValidateCurrentUser(false); + var result = _webSecurity.ValidateCurrentUser(false); switch (result) { diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index ee74e56528..b16763f139 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -18,15 +18,12 @@ using Umbraco.Web.Common.Install; using Umbraco.Extensions; using System.Linq; using Umbraco.Web.Common.Controllers; -using System; using Umbraco.Web.Common.Middleware; using Umbraco.Web.Common.ModelBinding; using Umbraco.Web.Common.Routing; using Umbraco.Web.Common.Templates; -using Umbraco.Web.Search; using Umbraco.Web.Security; using Umbraco.Web.Templates; -using Umbraco.Web.Trees; namespace Umbraco.Web.Common.Runtime { @@ -86,13 +83,6 @@ namespace Umbraco.Web.Common.Runtime composition.WithCollectionBuilder() .Add(umbracoApiControllerTypes); - // register back office trees - // the collection builder only accepts types inheriting from TreeControllerBase - // and will filter out those that are not attributed with TreeAttribute - // composition.Trees() - // .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); - composition.RegisterUnique(); //TODO replace with collection builder above - composition.RegisterUnique(); diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index 67a47a0a15..7b157179c3 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -3,10 +3,16 @@ module.exports = { compile: { build: { - sourcemaps: false + sourcemaps: false, + embedtemplates: true }, dev: { - sourcemaps: true + sourcemaps: true, + embedtemplates: true + }, + test: { + sourcemaps: false, + embedtemplates: true } }, sources: { @@ -17,7 +23,7 @@ module.exports = { installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.css" }, nonodes: { files: "./src/less/pages/nonodes.less", watch: "./src/less/**/*.less", out: "nonodes.style.min.css"}, preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.css" }, - umbraco: { files: "./src/less/belle.less", watch: "./src/less/**/*.less", out: "umbraco.css" }, + umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.css" }, rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" } }, diff --git a/src/Umbraco.Web.UI.Client/gulp/modes.js b/src/Umbraco.Web.UI.Client/gulp/modes.js index dc2947f2cc..21609cdcf8 100644 --- a/src/Umbraco.Web.UI.Client/gulp/modes.js +++ b/src/Umbraco.Web.UI.Client/gulp/modes.js @@ -10,4 +10,14 @@ function setDevelopmentMode(cb) { return cb(); }; -module.exports = { setDevelopmentMode: setDevelopmentMode }; +function setTestMode(cb) { + + config.compile.current = config.compile.test; + + return cb(); +}; + +module.exports = { + setDevelopmentMode: setDevelopmentMode, + setTestMode: setTestMode + }; diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js index 1e8d074f7e..255fe17435 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js @@ -6,11 +6,24 @@ var karmaServer = require('karma').Server; * Build tests **************************/ - // Karma test +// Karma test function testUnit() { + return new karmaServer({ + configFile: __dirname + "/../../test/config/karma.conf.js" + }) + .start(); +}; + +// Run karma test server +function runUnitTestServer() { + return new karmaServer({ configFile: __dirname + "/../../test/config/karma.conf.js", + autoWatch: true, + port: 9999, + singleRun: false, + browsers: ['ChromeDebugging'], keepalive: true }) .start(); @@ -24,4 +37,4 @@ function testE2e() { .start(); }; -module.exports = { testUnit: testUnit, testE2e: testE2e }; +module.exports = { testUnit: testUnit, testE2e: testE2e, runUnitTestServer: runUnitTestServer }; diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index db24dae23e..744481391a 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -1,7 +1,7 @@ 'use strict'; const config = require('../config'); -const {watch, parallel, dest, src} = require('gulp'); +const {watch, series, parallel, dest, src} = require('gulp'); var _ = require('lodash'); var MergeStream = require('merge-stream'); @@ -9,9 +9,7 @@ var MergeStream = require('merge-stream'); var processJs = require('../util/processJs'); var processLess = require('../util/processLess'); -//const { less } = require('./less'); -//const { views } = require('./views'); - +var {js} = require('./js'); function watchTask(cb) { @@ -30,24 +28,27 @@ function watchTask(cb) { watch(group.watch, { ignoreInitial: true, interval: watchInterval }, function Less_Group_Compile() { return processLess(group.files, group.out); }); } }); - + //Setup a watcher for all groups of view files var viewWatcher; _.forEach(config.sources.views, function (group) { if(group.watch !== false) { - viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }); - viewWatcher.on('change', function(path, stats) { - - var task = src(group.files); + viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }, + parallel( + function MoveViewsAndRegenerateJS() { + var task = src(group.files); - _.forEach(config.roots, function(root){ - console.log("copying " + group.files + " to " + root + config.targets.views + group.folder); - task = task.pipe( dest(root + config.targets.views + group.folder) ); - }) - }); + _.forEach(config.roots, function(root){ + console.log("copying " + group.files + " to " + root + config.targets.views + group.folder); + task = task.pipe( dest(root + config.targets.views + group.folder) ); + }); + }, + js + ) + ); } }); - + return cb(); }; diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js index 9ff9a93c09..6c6f1276e7 100644 --- a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js +++ b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js @@ -28,7 +28,9 @@ module.exports = function (files, out) { .pipe(sort()); //in production, embed the templates - task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })) + if(config.compile.current.embedtemplates === true) { + task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })); + } task = task.pipe(concat(out)).pipe(wrap('(function(){\n%= body %\n})();')) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 705c54bf04..542d45c479 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -13,11 +13,11 @@ const { src, dest, series, parallel, lastRun } = require('gulp'); const config = require('./gulp/config'); -const { setDevelopmentMode } = require('./gulp/modes'); +const { setDevelopmentMode, setTestMode } = require('./gulp/modes'); const { dependencies } = require('./gulp/tasks/dependencies'); const { js } = require('./gulp/tasks/js'); const { less } = require('./gulp/tasks/less'); -const { testE2e, testUnit } = require('./gulp/tasks/test'); +const { testE2e, testUnit, runUnitTestServer } = require('./gulp/tasks/test'); const { views } = require('./gulp/tasks/views'); const { watchTask } = require('./gulp/tasks/watchTask'); @@ -31,6 +31,6 @@ exports.build = series(parallel(dependencies, js, less, views), testUnit); exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views), watchTask); exports.watch = series(watchTask); // -exports.runTests = series(js, testUnit); -exports.testUnit = series(testUnit); -exports.testE2e = series(testE2e); +exports.runTests = series(setTestMode, parallel(js, testUnit)); +exports.runUnit = series(setTestMode, parallel(js, runUnitTestServer), watchTask); +exports.testE2e = series(setTestMode, parallel(testE2e)); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js index 19a33a8351..8d223e427e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js @@ -12,9 +12,9 @@
prepareEntryForStorage(data)); + var copiedDatas = datas.map(data => prepareEntryForStorage(data, firstLevelClearupMethod)); // remove previous copies of this entry: storage.entries = storage.entries.filter( diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less index 96636a3096..6238900bf6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less @@ -32,14 +32,18 @@ .umb-lightbox__close { position: absolute; top: 20px; - right: 60px; + right: 20px; + height: 40px; + width: 40px; } .umb-lightbox__close i { font-size: 20px; - cursor: pointer; - height: 40px; - width: 40px; + height: inherit; + width: inherit; + position: absolute; + top: 0; + left: 0; } .umb-lightbox__images { @@ -75,12 +79,20 @@ right: 20px; top: 50%; transform: translate(0, -50%); + + .umb-lightbox__control-icon { + margin-right: -4px; + } } .umb-lightbox__control.-prev { left: 20px; top: 50%; transform: translate(0, -50%); + + .umb-lightbox__control-icon { + margin-left: -4px; + } } .umb-lightbox__control-icon { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html index 769cd422ef..872fdbe41f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html @@ -7,18 +7,19 @@
-
- - {{notification.headline}} + +
+ + -
- {{notification.headline}} +
+
- diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html index b45a3bb843..1b88c9f988 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html @@ -7,10 +7,16 @@ on-outside-click="clickCancel()">
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-lightbox.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-lightbox.html index 1379072db2..6fa28d6042 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-lightbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-lightbox.html @@ -1,9 +1,12 @@
-
- -
+
@@ -11,12 +14,18 @@
-
- -
+ -
- -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-settings.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-settings.html index 8b4aeba3a0..4e2edbe2a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-settings.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-settings.html @@ -44,6 +44,6 @@ aria-label="Edit">
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index c6675ccec8..0499bea713 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -9,7 +9,7 @@ - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 7cad5a5f05..066cbd6c17 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -1,6 +1,58 @@ (function () { 'use strict'; + /** + * When performing a copy, we do copy the ElementType Data Model, but each inner Nested Content property is still stored as the Nested Content Model, aka. each property is just storing its value. To handle this we need to ensure we handle both scenarios. + */ + + + angular.module('umbraco').run(['clipboardService', function (clipboardService) { + + function clearNestedContentPropertiesForStorage(prop, propClearingMethod) { + + // if prop.editor is "Umbraco.NestedContent" + if ((typeof prop === 'object' && prop.editor === "Umbraco.NestedContent")) { + + var value = prop.value; + for (var i = 0; i < value.length; i++) { + var obj = value[i]; + + // remove the key + delete obj.key; + + // Loop through all inner properties: + for (var k in obj) { + propClearingMethod(obj[k]); + } + } + } + } + + clipboardService.registrerClearPropertyResolver(clearNestedContentPropertiesForStorage) + + + function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { + + // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property inside a NestedContent property. + if ((Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined)) { + + for (var i = 0; i < prop.length; i++) { + var obj = prop[i]; + + // remove the key + delete obj.key; + + // Loop through all inner properties: + for (var k in obj) { + propClearingMethod(obj[k]); + } + } + } + } + + clipboardService.registrerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage) + }]); + angular .module('umbraco') .component('nestedContentPropertyEditor', { @@ -13,7 +65,7 @@ } }); - function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService, $routeParams, editorState) { + function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { var vm = this; var model = $scope.$parent.$parent.model; @@ -76,7 +128,7 @@ } localizationService.localize("clipboard_labelForArrayOfItemsFrom", [model.label, nodeName]).then(function (data) { - clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id); + clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id, clearNodeForCopy); }); } @@ -385,6 +437,11 @@ }); } + function clearNodeForCopy(clonedData) { + delete clonedData.key; + delete clonedData.$$hashKey; + } + vm.showCopy = clipboardService.isSupported(); vm.showPaste = false; @@ -392,7 +449,7 @@ syncCurrentNode(); - clipboardService.copy("elementType", node.contentTypeAlias, node); + clipboardService.copy("elementType", node.contentTypeAlias, node, null, null, null, clearNodeForCopy); $event.stopPropagation(); } diff --git a/src/Umbraco.Web.UI.Client/test/config/app.unit.js b/src/Umbraco.Web.UI.Client/test/config/app.unit.js index 1f49d237e6..9e265215dd 100644 --- a/src/Umbraco.Web.UI.Client/test/config/app.unit.js +++ b/src/Umbraco.Web.UI.Client/test/config/app.unit.js @@ -13,8 +13,8 @@ var app = angular.module('umbraco', [ 'ngSanitize', //'ngMessages', - 'tmh.dynamicLocale' + 'tmh.dynamicLocale', //'ngFileUpload', - //'LocalStorageModule', + 'LocalStorageModule' //'chart.js' ]); diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs new file mode 100644 index 0000000000..5d1aa8cb85 --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Web.Compose +{ + public class NestedContentPropertyComponent : IComponent + { + public void Initialize() + { + ContentService.Copying += ContentService_Copying; + ContentService.Saving += ContentService_Saving; + } + + private void ContentService_Copying(IContentService sender, CopyEventArgs e) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + UpdateNestedContentProperties(nestedContentProps, false); + } + + private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + { + // One or more content nodes could be saved in a bulk publish + foreach (var entity in e.SavedEntities) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + UpdateNestedContentProperties(nestedContentProps, true); + } + } + + public void Terminate() + { + ContentService.Copying -= ContentService_Copying; + ContentService.Saving -= ContentService_Saving; + } + + private void UpdateNestedContentProperties(IEnumerable nestedContentProps, bool onlyMissingKeys) + { + // Each NC Property on a doctype + foreach (var nestedContentProp in nestedContentProps) + { + // A NC Prop may have one or more values due to cultures + var propVals = nestedContentProp.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested NC's + var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested NC's + var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + + + // internal for tests + internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null) + { + // used so we can test nicely + if (createGuid == null) + createGuid = () => Guid.NewGuid(); + + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) + return rawJson; + + // Parse JSON + var complexEditorValue = JToken.Parse(rawJson); + + UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); + + return complexEditorValue.ToString(); + } + + private void UpdateNestedContentKeysRecursively(JToken json, bool onlyMissingKeys, Func createGuid) + { + // check if this is NC + var isNestedContent = json.SelectTokens($"$..['{NestedContentPropertyEditor.ContentTypeAliasPropertyKey}']", false).Any(); + + // select all values (flatten) + var allProperties = json.SelectTokens("$..*").OfType().Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (var prop in allProperties) + { + if (prop.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey) + { + // get it's sibling 'key' property + var ncKeyVal = prop.Parent["key"] as JValue; + // TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys? + if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null)) + { + // create or replace + prop.Parent["key"] = createGuid().ToString(); + } + } + else if (!isNestedContent || prop.Name != "key") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested NC + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) + { + // recurse + var parsed = JToken.Parse(propVal); + UpdateNestedContentKeysRecursively(parsed, onlyMissingKeys, createGuid); + // set the value to the updated one + prop.Value = parsed.ToString(); + } + } + } + } + + } +} diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs new file mode 100644 index 0000000000..4c9d9dee1c --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs @@ -0,0 +1,9 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Compose +{ + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class NestedContentPropertyComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Web/CompositionExtensions.cs b/src/Umbraco.Web/CompositionExtensions.cs index 29a892f1d5..85b7b68c35 100644 --- a/src/Umbraco.Web/CompositionExtensions.cs +++ b/src/Umbraco.Web/CompositionExtensions.cs @@ -41,16 +41,6 @@ namespace Umbraco.Web - - /// - /// Gets the back office tree collection builder - /// - /// - /// - public static TreeCollectionBuilder Trees(this Composition composition) - => composition.WithCollectionBuilder(); - - #endregion #region Uniques diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index cd2876647c..f6b908e878 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -88,7 +88,7 @@ namespace Umbraco.Web.Editors [WebApi.UmbracoAuthorize(requireApproval: false)] public IDictionary GetPasswordConfig(int userId) { - return _passwordConfiguration.GetConfiguration(userId != UmbracoContext.Security.CurrentUser.Id); + return _passwordConfiguration.GetConfiguration(userId != Security.CurrentUser.Id); } /// @@ -171,7 +171,7 @@ namespace Umbraco.Web.Editors [CheckIfUserTicketDataIsStale] public UserDetail GetCurrentUser() { - var user = UmbracoContext.Security.CurrentUser; + var user = Security.CurrentUser; var result = Mapper.Map(user); var httpContextAttempt = TryGetHttpContext(); if (httpContextAttempt.Success) @@ -195,7 +195,7 @@ namespace Umbraco.Web.Editors [SetAngularAntiForgeryTokens] public UserDetail GetCurrentInvitedUser() { - var user = UmbracoContext.Security.CurrentUser; + var user = Security.CurrentUser; if (user.IsApproved) { @@ -219,7 +219,7 @@ namespace Umbraco.Web.Editors [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { - var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId().ResultOr(0).ToString()); + var identityUser = await UserManager.FindByIdAsync(Security.GetUserId().ResultOr(0).ToString()); return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); } diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 0b001e7229..2e809bc93a 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -172,14 +172,7 @@ namespace Umbraco.Web.Editors // "imagesApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( // controller => controller.GetBigThumbnail("")) // }, - { - "sectionApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.GetSections()) - }, - { - "treeApplicationApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.GetApplicationTrees(null, null, null, TreeUse.None)) - }, + { "contentTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllowedChildren(0)) diff --git a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs index bd27a872d0..7841e547aa 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; namespace Umbraco.Web.Editors.Filters { @@ -18,13 +19,13 @@ namespace Umbraco.Web.Editors.Filters /// internal abstract class ContentModelValidator { - protected IUmbracoContextAccessor UmbracoContextAccessor { get; } + protected IWebSecurity WebSecurity { get; } protected ILogger Logger { get; } - protected ContentModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor) + protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity) { Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - UmbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + WebSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); } } @@ -45,7 +46,7 @@ namespace Umbraco.Web.Editors.Filters { private readonly ILocalizedTextService _textService; - protected ContentModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor) + protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity) { _textService = textService ?? throw new ArgumentNullException(nameof(textService)); } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs index 39bd6ab0f4..3e2b0e5cfa 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; namespace Umbraco.Web.Editors.Filters { @@ -10,7 +11,7 @@ namespace Umbraco.Web.Editors.Filters /// internal class ContentSaveModelValidator : ContentModelValidator { - public ContentSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService) + public ContentSaveModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity, textService) { } } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs index c8ef56363c..e60e771970 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs @@ -25,19 +25,19 @@ namespace Umbraco.Web.Editors.Filters internal sealed class ContentSaveValidationAttribute : ActionFilterAttribute { private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly ILocalizedTextService _textService; private readonly IContentService _contentService; private readonly IUserService _userService; private readonly IEntityService _entityService; - public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService) + public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService) { } - public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService) + public ContentSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); @@ -47,11 +47,11 @@ namespace Umbraco.Web.Editors.Filters public override void OnActionExecuting(HttpActionContext actionContext) { var model = (ContentItemSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new ContentSaveModelValidator(_logger, _umbracoContextAccessor, _textService); + var contentItemValidator = new ContentSaveModelValidator(_logger, _webSecurity, _textService); if (!ValidateAtLeastOneVariantIsBeingSaved(model, actionContext)) return; if (!contentItemValidator.ValidateExistingContent(model, actionContext)) return; - if (!ValidateUserAccess(model, actionContext, _umbracoContextAccessor.UmbracoContext.Security)) return; + if (!ValidateUserAccess(model, actionContext, _webSecurity)) return; //validate for each variant that is being updated foreach (var variant in model.Variants.Where(x => x.Save)) diff --git a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs index 449ef95675..8e07bc6e7c 100644 --- a/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/MediaItemSaveValidationAttribute.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; using Umbraco.Web.WebApi; namespace Umbraco.Web.Editors.Filters @@ -19,19 +20,19 @@ namespace Umbraco.Web.Editors.Filters internal class MediaItemSaveValidationAttribute : ActionFilterAttribute { private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly ILocalizedTextService _textService; private readonly IMediaService _mediaService; private readonly IEntityService _entityService; - public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService) + public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService) { } - public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService) + public MediaItemSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); @@ -40,7 +41,7 @@ namespace Umbraco.Web.Editors.Filters public override void OnActionExecuting(HttpActionContext actionContext) { var model = (MediaItemSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new MediaSaveModelValidator(_logger, _umbracoContextAccessor, _textService); + var contentItemValidator = new MediaSaveModelValidator(_logger, _webSecurity, _textService); if (ValidateUserAccess(model, actionContext)) { @@ -90,7 +91,7 @@ namespace Umbraco.Web.Editors.Filters if (MediaController.CheckPermissions( actionContext.Request.Properties, - _umbracoContextAccessor.UmbracoContext.Security.CurrentUser, + _webSecurity.CurrentUser, _mediaService, _entityService, contentIdToCheck, contentToCheck) == false) { diff --git a/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs index 87b55fea76..19a2c12cbb 100644 --- a/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/MediaSaveModelValidator.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; namespace Umbraco.Web.Editors.Filters { @@ -10,7 +11,7 @@ namespace Umbraco.Web.Editors.Filters /// internal class MediaSaveModelValidator : ContentModelValidator> { - public MediaSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService) + public MediaSaveModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity, textService) { } } diff --git a/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs index 1b2ddf2ace..77dc1a1c27 100644 --- a/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/MemberSaveModelValidator.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; namespace Umbraco.Web.Editors.Filters { @@ -25,12 +26,12 @@ namespace Umbraco.Web.Editors.Filters public MemberSaveModelValidator( ILogger logger, - IUmbracoContextAccessor umbracoContextAccessor, + IWebSecurity webSecurity, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper) - : base(logger, umbracoContextAccessor, textService) + : base(logger, webSecurity, textService) { _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); @@ -101,7 +102,7 @@ namespace Umbraco.Web.Editors.Filters //if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check //if a sensitive value is being submitted. - if (UmbracoContextAccessor.UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false) + if (WebSecurity.CurrentUser.HasAccessToSensitiveData() == false) { var contentType = _memberTypeService.Get(model.PersistedContent.ContentTypeId); var sensitiveProperties = contentType diff --git a/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs index 51fa5652ea..a3739c1002 100644 --- a/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs +++ b/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Composing; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; namespace Umbraco.Web.Editors.Filters { @@ -15,20 +16,20 @@ namespace Umbraco.Web.Editors.Filters internal class MemberSaveValidationAttribute : ActionFilterAttribute { private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IWebSecurity _webSecurity; private readonly ILocalizedTextService _textService; private readonly IMemberTypeService _memberTypeService; private readonly IMemberService _memberService; private readonly IShortStringHelper _shortStringHelper; public MemberSaveValidationAttribute() - : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MemberTypeService, Current.Services.MemberService, Current.ShortStringHelper) + : this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.MemberTypeService, Current.Services.MemberService, Current.ShortStringHelper) { } - public MemberSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper) + public MemberSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); @@ -38,7 +39,7 @@ namespace Umbraco.Web.Editors.Filters public override void OnActionExecuting(HttpActionContext actionContext) { var model = (MemberSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new MemberSaveModelValidator(_logger, _umbracoContextAccessor,_textService, _memberTypeService, _memberService, _shortStringHelper); + var contentItemValidator = new MemberSaveModelValidator(_logger, _webSecurity, _textService, _memberTypeService, _memberService, _shortStringHelper); //now do each validation step if (contentItemValidator.ValidateExistingContent(model, actionContext)) if (contentItemValidator.ValidateProperties(model, model, actionContext)) diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 63ec9c62c7..50d31de5a8 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -136,7 +136,7 @@ namespace Umbraco.Web.Editors var ctId = Convert.ToInt32(contentTypeSave.Id); var ct = ctId > 0 ? Services.MemberTypeService.Get(ctId) : null; - if (UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false) + if (Security.CurrentUser.HasAccessToSensitiveData() == false) { //We need to validate if any properties on the contentTypeSave have had their IsSensitiveValue changed, //and if so, we need to check if the current user has access to sensitive values. If not, we have to return an error diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 073929b7a7..10d881d55e 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -92,7 +92,7 @@ namespace Umbraco.Web.Editors /// public string[] GetCurrentUserAvatarUrls() { - var urls = UmbracoContext.Security.CurrentUser.GetUserAvatarUrls(AppCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator); + var urls = Security.CurrentUser.GetUserAvatarUrls(AppCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator); if (urls == null) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Could not access Gravatar endpoint")); diff --git a/src/Umbraco.Web/FormDataCollectionExtensions.cs b/src/Umbraco.Web/FormDataCollectionExtensions.cs index 52f86dcc59..3f2840c8ca 100644 --- a/src/Umbraco.Web/FormDataCollectionExtensions.cs +++ b/src/Umbraco.Web/FormDataCollectionExtensions.cs @@ -7,7 +7,7 @@ using Umbraco.Core; namespace Umbraco.Web { - + // Migrated to .NET Core (as FormCollectionExtensions) public static class FormDataCollectionExtensions { /// diff --git a/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs b/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs index 07a3f2a5e3..eb39cb8faa 100644 --- a/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs +++ b/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs @@ -25,7 +25,10 @@ namespace Umbraco.Web.Mvc //we have no choice but to instantiate the controller var instance = factory.CreateController(requestContext, controllerName); - return instance?.GetType(); + var controllerType = instance?.GetType(); + factory.ReleaseController(instance); + + return controllerType; } } } diff --git a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs index 98ba7f47de..cfc931fc71 100644 --- a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs @@ -82,7 +82,10 @@ namespace Umbraco.Web.Mvc //we have no choice but to instantiate the controller var instance = factory.CreateController(requestContext, controllerName); - return instance?.GetType(); + var controllerType = instance?.GetType(); + factory.ReleaseController(instance); + + return controllerType; } return GetControllerType(requestContext, controllerName); diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs index dc647a5fe3..b5c6185069 100644 --- a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs @@ -4,6 +4,7 @@ using System.Web.Mvc; using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Core.Configuration; +using Umbraco.Web.Security; namespace Umbraco.Web.Mvc { @@ -12,25 +13,23 @@ namespace Umbraco.Web.Mvc public sealed class UmbracoAuthorizeAttribute : AuthorizeAttribute { // see note in HttpInstallAuthorizeAttribute - private readonly IUmbracoContext _umbracoContext; + private readonly IWebSecurity _webSecurity; private readonly IRuntimeState _runtimeState; private readonly string _redirectUrl; private IRuntimeState RuntimeState => _runtimeState ?? Current.RuntimeState; - private IUmbracoContext UmbracoContext => _umbracoContext ?? Current.UmbracoContext; + private IWebSecurity WebSecurity => _webSecurity ?? Current.UmbracoContext.Security; /// /// THIS SHOULD BE ONLY USED FOR UNIT TESTS /// - /// + /// /// - public UmbracoAuthorizeAttribute(IUmbracoContext umbracoContext, IRuntimeState runtimeState) + public UmbracoAuthorizeAttribute(IWebSecurity webSecurity, IRuntimeState runtimeState) { - if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext)); - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - _umbracoContext = umbracoContext; - _runtimeState = runtimeState; + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); } /// @@ -75,7 +74,7 @@ namespace Umbraco.Web.Mvc // otherwise we need to ensure that a user is logged in return RuntimeState.Level == RuntimeLevel.Install || RuntimeState.Level == RuntimeLevel.Upgrade - || UmbracoContext.Security.ValidateCurrentUser(); + || WebSecurity.ValidateCurrentUser(); } catch (Exception) { diff --git a/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs b/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs index 078eb73026..bb7c43dcdb 100644 --- a/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs @@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class MenuRenderingEventArgs : TreeRenderingEventArgs { /// diff --git a/src/Umbraco.Web/Trees/TreeAttribute.cs b/src/Umbraco.Web/Trees/TreeAttribute.cs index 25921b06f8..1170de5cfa 100644 --- a/src/Umbraco.Web/Trees/TreeAttribute.cs +++ b/src/Umbraco.Web/Trees/TreeAttribute.cs @@ -5,6 +5,7 @@ namespace Umbraco.Web.Trees /// /// Identifies a section tree. /// + /// // Migrated to .NET Core [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class TreeAttribute : Attribute, ITree { diff --git a/src/Umbraco.Web/Trees/TreeController.cs b/src/Umbraco.Web/Trees/TreeController.cs index e76b45e623..730fd04bf2 100644 --- a/src/Umbraco.Web/Trees/TreeController.cs +++ b/src/Umbraco.Web/Trees/TreeController.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.Trees /// /// The base controller for all tree requests /// + /// // Migrated to .NET Core public abstract class TreeController : TreeControllerBase { private static readonly ConcurrentDictionary TreeAttributeCache = new ConcurrentDictionary(); diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 49f7eea845..18cb25c912 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -27,6 +27,7 @@ namespace Umbraco.Web.Trees /// /// Developers should generally inherit from TreeController. /// + /// Migrated to .NET Core [AngularJsonOnlyConfiguration] public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree { diff --git a/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs index 30ef008cf7..1914d427e9 100644 --- a/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs @@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class TreeNodeRenderingEventArgs : TreeRenderingEventArgs { public TreeNode Node { get; private set; } diff --git a/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs index 9eaaad18ee..753e727b05 100644 --- a/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs @@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class TreeNodesRenderingEventArgs : TreeRenderingEventArgs { public TreeNodeCollection Nodes { get; private set; } diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 02a198401b..130f4c2486 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -3,6 +3,7 @@ /// /// Common query string parameters used for tree query strings /// + /// Migrated to .NET Core internal struct TreeQueryStringParameters { public const string Use = "use"; diff --git a/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs index d8b67890a3..3f26c8cf50 100644 --- a/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs @@ -3,6 +3,7 @@ using System.Net.Http.Formatting; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class TreeRenderingEventArgs : EventArgs { public FormDataCollection QueryStrings { get; private set; } diff --git a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs index f4b516a764..52f7668aef 100644 --- a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs @@ -8,6 +8,7 @@ using Umbraco.Core; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public static class UrlHelperExtensions { public static string GetTreeUrl(this UrlHelper urlHelper, Type treeType, string nodeId, FormDataCollection queryStrings) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index fc92b6a747..da1bf5ae33 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -144,6 +144,7 @@ + @@ -182,6 +183,7 @@ + @@ -199,7 +201,6 @@ - @@ -338,7 +339,6 @@ - @@ -368,7 +368,6 @@ - @@ -378,7 +377,6 @@ - diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs index 000c4860c6..69c697d0fc 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizeAttribute.cs @@ -19,24 +19,22 @@ namespace Umbraco.Web.WebApi internal static bool Enable = true; // TODO: inject! - private readonly IUmbracoContext _umbracoContext; + private readonly IWebSecurity _webSecurity; private readonly IRuntimeState _runtimeState; private IRuntimeState RuntimeState => _runtimeState ?? Current.RuntimeState; - private IUmbracoContext UmbracoContext => _umbracoContext ?? Current.UmbracoContext; + private IWebSecurity WebSecurity => _webSecurity ?? Current.UmbracoContext.Security; /// /// THIS SHOULD BE ONLY USED FOR UNIT TESTS /// - /// + /// /// - public UmbracoAuthorizeAttribute(IUmbracoContext umbracoContext, IRuntimeState runtimeState) + public UmbracoAuthorizeAttribute(IWebSecurity webSecurity, IRuntimeState runtimeState) { - if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext)); - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - _umbracoContext = umbracoContext; - _runtimeState = runtimeState; + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); } public UmbracoAuthorizeAttribute() : this(true) @@ -60,7 +58,7 @@ namespace Umbraco.Web.WebApi // otherwise we need to ensure that a user is logged in return RuntimeState.Level == RuntimeLevel.Install || RuntimeState.Level == RuntimeLevel.Upgrade - || UmbracoContext.Security.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; + || WebSecurity.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; } catch (Exception) {