diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 5cc1a584b1..c79e7aa869 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1695,12 +1695,11 @@ namespace Umbraco.Core.Services.Implement } const int pageSize = 500; - var page = 0; var total = long.MaxValue; - while (page * pageSize < total) + while (total > 0) { //get descendants - ordered from deepest to shallowest - var descendants = GetPagedDescendants(content.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); + var descendants = GetPagedDescendants(content.Id, 0, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); foreach (var c in descendants) DoDelete(c); } @@ -1926,11 +1925,10 @@ namespace Umbraco.Core.Services.Implement paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id; const int pageSize = 500; - var page = 0; var total = long.MaxValue; - while (page * pageSize < total) + while (total > 0) { - var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + var descendants = GetPagedDescendantsLocked(originalPath, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); foreach (var descendant in descendants) { moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 2ff39f7f7d..ab075c4ade 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -201,7 +201,7 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, false); @@ -227,13 +227,13 @@ namespace Umbraco.Core.Services.Implement // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); - var mediaType = GetMediaType(mediaTypeAlias); // + locks // + locks + var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback - var parent = parentId > 0 ? GetById(parentId) : null; // + locks // + locks + var parent = parentId > 0 ? GetById(parentId) : null; // + locks if (parentId > 0 && parent == null) - throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback // causes rollback + throw new ArgumentException("No media with that id.", nameof(parentId)); // causes rollback var media = parentId > 0 ? new Models.Media(name, parent, mediaType) : new Models.Media(name, parentId, mediaType); CreateMedia(scope, media, parent, userId, true); @@ -261,9 +261,9 @@ namespace Umbraco.Core.Services.Implement // locking the media tree secures media types too scope.WriteLock(Constants.Locks.MediaTree); - var mediaType = GetMediaType(mediaTypeAlias); // + locks // + locks + var mediaType = GetMediaType(mediaTypeAlias); // + locks if (mediaType == null) - throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback // causes rollback + throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, true); @@ -645,8 +645,6 @@ namespace Umbraco.Core.Services.Implement } // poor man's validation? - // poor man's validation? - if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); @@ -934,7 +932,7 @@ namespace Umbraco.Core.Services.Implement var parent = parentId == Constants.System.Root ? null : GetById(parentId); if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) - throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback // causes rollback + throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback var moveEventInfo = new MoveEventInfo(media, media.Path, parentId); var moveEventArgs = new MoveEventArgs(true, evtMsgs, moveEventInfo); @@ -947,12 +945,6 @@ namespace Umbraco.Core.Services.Implement // if media was trashed, and since we're not moving to the recycle bin, // indicate that the trashed status should be changed to false, else just // leave it unchanged - // if media was trashed, and since we're not moving to the recycle bin, - - // indicate that the trashed status should be changed to false, else just - - // leave it unchanged - var trashed = media.Trashed ? false : (bool?) null; PerformMoveLocked(media, parentId, parent, userId, moves, trashed); @@ -1042,17 +1034,11 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MediaTree); + // no idea what those events are for, keep a simplified version + // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since // each deleted items will have its own deleting/deleted events. so, files and such // are managed by Delete, and not here. - - // no idea what those events are for, keep a simplified version - // v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since - // each deleted items will have its own deleting/deleted events. so, files and such - - // emptying the recycle bin means deleting whatever is in there - do it properly! - // are managed by Delete, and not here. - // no idea what those events are for, keep a simplified version var args = new RecycleBinEventArgs(nodeObjectType, evtMsgs); if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, args)) @@ -1113,11 +1099,6 @@ namespace Umbraco.Core.Services.Implement { // if the current sort order equals that of the media we don't // need to update it, so just increment the sort order and continue. - // if the current sort order equals that of the media we don't - - // else update - // need to update it, so just increment the sort order and continue. - // save if (media.SortOrder == sortOrder) { sortOrder++; diff --git a/src/Umbraco.Tests/Mapping/MappingTests.cs b/src/Umbraco.Tests/Mapping/MappingTests.cs index 79d383857a..7487667a58 100644 --- a/src/Umbraco.Tests/Mapping/MappingTests.cs +++ b/src/Umbraco.Tests/Mapping/MappingTests.cs @@ -172,6 +172,31 @@ namespace Umbraco.Tests.Mapping } } + [Test] + public void EnumMap() + { + var definitions = new MapDefinitionCollection(new IMapDefinition[] + { + new MapperDefinition4(), + }); + var mapper = new UmbracoMapper(definitions); + + var thing5 = new Thing5() + { + Fruit1 = Thing5Enum.Apple, + Fruit2 = Thing5Enum.Banana, + Fruit3= Thing5Enum.Cherry + }; + + var thing6 = mapper.Map(thing5); + + Assert.IsNotNull(thing6); + Assert.AreEqual(Thing6Enum.Apple, thing6.Fruit1); + Assert.AreEqual(Thing6Enum.Banana, thing6.Fruit2); + Assert.AreEqual(Thing6Enum.Cherry, thing6.Fruit3); + } + + private class Thing1 { public string Value { get; set; } @@ -188,6 +213,34 @@ namespace Umbraco.Tests.Mapping private class Thing4 { } + private class Thing5 + { + public Thing5Enum Fruit1 { get; set; } + public Thing5Enum Fruit2 { get; set; } + public Thing5Enum Fruit3 { get; set; } + } + + private enum Thing5Enum + { + Apple = 0, + Banana = 1, + Cherry = 2 + } + + private class Thing6 + { + public Thing6Enum Fruit1 { get; set; } + public Thing6Enum Fruit2 { get; set; } + public Thing6Enum Fruit3 { get; set; } + } + + private enum Thing6Enum + { + Apple = 0, + Banana = 1, + Cherry = 2 + } + private class MapperDefinition1 : IMapDefinition { public void DefineMaps(UmbracoMapper mapper) @@ -224,5 +277,22 @@ namespace Umbraco.Tests.Mapping mapper.Define(); } } + + private class MapperDefinition4 : IMapDefinition + { + public void DefineMaps(UmbracoMapper mapper) + { + mapper.Define((source, context) => new Thing6(), Map); + mapper.Define( + (source, context) => (Thing6Enum)source); + } + + private void Map(Thing5 source, Thing6 target, MapperContext context) + { + target.Fruit1 = context.Map(source.Fruit1); + target.Fruit2 = context.Map(source.Fruit2); + target.Fruit3 = context.Map(source.Fruit3); + } + } } } diff --git a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs b/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs new file mode 100644 index 0000000000..c81c108e0d --- /dev/null +++ b/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs @@ -0,0 +1,157 @@ +using System.Collections.Specialized; +using System.Web; +using System.Web.Helpers; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.Mvc; +using Umbraco.Web.Security; + +namespace Umbraco.Tests.Security +{ + [TestFixture] + public class UmbracoAntiForgeryAdditionalDataProviderTests + { + [Test] + public void Test_Wrapped_Non_BeginUmbracoForm() + { + var wrapped = Mock.Of(x => x.GetAdditionalData(It.IsAny()) == "custom"); + var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var data = provider.GetAdditionalData(httpContextFactory.HttpContext); + + Assert.IsTrue(data.DetectIsJson()); + var json = JsonConvert.DeserializeObject(data); + Assert.AreEqual(null, json.Ufprt); + Assert.IsTrue(json.Stamp != default); + Assert.AreEqual("custom", json.WrappedValue); + } + + [Test] + public void Null_Wrapped_Non_BeginUmbracoForm() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var data = provider.GetAdditionalData(httpContextFactory.HttpContext); + + Assert.IsTrue(data.DetectIsJson()); + var json = JsonConvert.DeserializeObject(data); + Assert.AreEqual(null, json.Ufprt); + Assert.IsTrue(json.Stamp != default); + Assert.AreEqual("default", json.WrappedValue); + } + + [Test] + public void Validate_Non_Json() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "hello"); + + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_Invalid_Json() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '0'}"); + Assert.IsFalse(isValid); + + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': ''}"); + Assert.IsFalse(isValid); + + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'hello': 'world'}"); + Assert.IsFalse(isValid); + + } + + [Test] + public void Validate_No_Request_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + //there is a ufprt in the additional data, but not in the request + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': 'ASBVDFDFDFDF'}"); + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_No_AdditionalData_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); + requestMock.SetupGet(x => x["ufprt"]).Returns("ABCDEFG"); + + //there is a ufprt in the additional data, but not in the request + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_No_AdditionalData_Or_Request_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + + //there is a ufprt in the additional data, but not in the request + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); + Assert.IsTrue(isValid); + } + + [Test] + public void Validate_Request_And_AdditionalData_Ufprt() + { + var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); + + var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); + requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); + + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsTrue(isValid); + + routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsFalse(isValid); + } + + [Test] + public void Validate_Wrapped_Request_And_AdditionalData_Ufprt() + { + var wrapped = Mock.Of(x => x.ValidateAdditionalData(It.IsAny(), "custom") == true); + var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); + + var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + + var httpContextFactory = new FakeHttpContextFactory("/hello/world"); + var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); + requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); + + var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsFalse(isValid); + + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'custom', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsTrue(isValid); + + routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; + isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); + Assert.IsFalse(isValid); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 4f4a83dc26..357d0c8a73 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -616,4 +616,623 @@ + + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {5D3B8245-ADA6-453F-A008-50ED04BFE770} + Library + Properties + Umbraco.Tests + Umbraco.Tests + v4.7.2 + 512 + ..\ + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + latest + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + latest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.8.14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + SqlResources.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + ImportResources.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + TestFiles.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + Designer + Always + + + Designer + Always + + + Always + + + + Designer + + + + Always + + + + + {31785BC3-256C-4613-B2F5-A1B0BDDED8C1} + Umbraco.Core + + + {651E1350-91B6-44B7-BD60-7207006D7003} + Umbraco.Web + + + {07fbc26b-2927-4a22-8d96-d644c667fecc} + Umbraco.Examine + + + + + ResXFileCodeGenerator + SqlResources.Designer.cs + Designer + + + ResXFileCodeGenerator + ImportResources.Designer.cs + Designer + + + ResXFileCodeGenerator + TestFiles.Designer.cs + Designer + + + + + Designer + Always + + + Always + + + + + + + Designer + + + + + + + + + + Designer + + + + + + + + + + + + + $(NuGetPackageFolders.Split(';')[0]) + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js index 91eb077ba3..8434a96ba5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js @@ -22,13 +22,16 @@ vm.search = search; vm.clickItem = clickItem; vm.clearSearch = clearSearch; - vm.handleKeyUp = handleKeyUp; + vm.handleKeyDown = handleKeyDown; vm.closeSearch = closeSearch; vm.focusSearch = focusSearch; //we need to capture the focus before this element is initialized. vm.focusBeforeOpening = focusService.getLastKnownFocus(); + vm.activeResult = null; + vm.activeResultGroup = null; + function onInit() { vm.searchQuery = ""; vm.searchResults = []; @@ -72,14 +75,66 @@ * Handles all keyboard events * @param {object} event */ - function handleKeyUp(event) { - - event.stopPropagation(); - event.preventDefault(); + function handleKeyDown(event) { // esc if(event.keyCode === 27) { + event.stopPropagation(); + event.preventDefault(); + closeSearch(); + return; + } + + // up/down (navigate search results) + if (vm.hasResults && (event.keyCode === 38 || event.keyCode === 40)) { + event.stopPropagation(); + event.preventDefault(); + + var allGroups = _.values(vm.searchResults); + var down = event.keyCode === 40; + if (vm.activeResultGroup === null) { + // it's the first time navigating, pick the appropriate group and result + // - first group and first result when navigating down + // - last group and last result when navigating up + vm.activeResultGroup = down ? _.first(allGroups) : _.last(allGroups); + vm.activeResult = down ? _.first(vm.activeResultGroup.results) : _.last(vm.activeResultGroup.results); + } + else if (down) { + // handle navigation down through the groups and results + if (vm.activeResult === _.last(vm.activeResultGroup.results)) { + if (vm.activeResultGroup === _.last(allGroups)) { + vm.activeResultGroup = _.first(allGroups); + } + else { + vm.activeResultGroup = allGroups[allGroups.indexOf(vm.activeResultGroup) + 1]; + } + vm.activeResult = _.first(vm.activeResultGroup.results); + } + else { + vm.activeResult = vm.activeResultGroup.results[vm.activeResultGroup.results.indexOf(vm.activeResult) + 1]; + } + } + else { + // handle navigation up through the groups and results + if (vm.activeResult === _.first(vm.activeResultGroup.results)) { + if (vm.activeResultGroup === _.first(allGroups)) { + vm.activeResultGroup = _.last(allGroups); + } + else { + vm.activeResultGroup = allGroups[allGroups.indexOf(vm.activeResultGroup) - 1]; + } + vm.activeResult = _.last(vm.activeResultGroup.results); + } + else { + vm.activeResult = vm.activeResultGroup.results[vm.activeResultGroup.results.indexOf(vm.activeResult) - 1]; + } + } + + $timeout(function () { + var resultElementLink = angular.element(".umb-search-item[active-result='true'] .umb-search-result__link"); + resultElementLink[0].focus(); + }); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js index 6127153a16..83904bd036 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -115,6 +115,7 @@ Use this directive to render an umbraco button. The directive can be used to gen vm.blockElement = false; vm.style = null; vm.innerState = "init"; + vm.generalActions = vm.labelKey === "general_actions"; vm.buttonLabel = vm.label; // is this a primary button style (i.e. anything but an 'info' button)? diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 7055d1a746..fbc0ea1eb0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -229,11 +229,9 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use } /** Method to load in the tree data */ - function loadTree() { - if (!$scope.loading && $scope.section) { - $scope.loading = true; - + if ($scope.section) { + //default args var args = { section: $scope.section, tree: $scope.treealias, cacheKey: $scope.cachekey, isDialog: $scope.isdialog ? $scope.isdialog : false }; @@ -244,20 +242,22 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use return treeService.getTree(args) .then(function (data) { - + //Only use the tree data, if we are still on the correct section + if(data.alias !== $scope.section){ + return $q.reject(); + } + //set the data once we have it $scope.tree = data; - $scope.loading = false; - //set the root as the current active tree $scope.activeTree = $scope.tree.root; emitEvent("treeLoaded", { tree: $scope.tree }); emitEvent("treeNodeExpanded", { tree: $scope.tree, node: $scope.tree.root, children: $scope.tree.root.children }); + return $q.when(data); }, function (reason) { - $scope.loading = false; notificationsService.error("Tree Error", reason); return $q.reject(reason); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index ea57a3fad6..b49d47b979 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -140,10 +140,9 @@ Use this directive to generate a pagination. tempPagination.push({ val: "...", isActive: false }, { name: lastLabel, val: scope.totalPages, isActive: false }); }); } - - scope.pagination = tempPagination; } + scope.pagination = tempPagination; } scope.next = function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/auth.resource.js new file mode 100644 index 0000000000..2953347f55 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/auth.resource.js @@ -0,0 +1,59 @@ +/** +* @ngdoc service +* @name umbraco.mocks.authMocks +* @description +* Mocks data retrival for the auth service +**/ +function authMocks($httpBackend, mocksUtils) { + + /** internal method to mock the current user to be returned */ + function getCurrentUser() { + + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } + + var currentUser = { + "email":"warren@umbraco.com", + "locale":"en-US", + "emailHash":"da0673cb2c930ee247e8ba5ebe4355bf", + "userGroups":[ + "admin", + "sensitiveData" + ], + "remainingAuthSeconds":1178.2645038, + "startContentIds":[-1], + "startMediaIds":[-1], + "avatars":[ + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=30", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=60", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=90", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=150", + "https://www.gravatar.com/avatar/da0673cb2c930ee247e8ba5ebe4355bf?d=404&s=300" + ], + "allowedSections":[ + "content", + "forms", + "media", + "member", + "packages", + "settings", + "users" + ], + "id":-1, + "name":"Warren Buckley" + }; + + return [200, currentUser, null]; + } + + return { + register: function () { + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Authentication/GetCurrentUser')) + .respond(getCurrentUser); + } + }; +} + +angular.module('umbraco.mocks').factory('authMocks', ['$httpBackend', 'mocksUtils', authMocks]); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js index 92c2a67d23..21f2b020dd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.httpbackend.js @@ -1,10 +1,10 @@ var umbracoAppDev = angular.module('umbraco.httpbackend', ['umbraco', 'ngMockE2E', 'umbraco.mocks']); -function initBackEnd($httpBackend, contentMocks, mediaMocks, treeMocks, userMocks, contentTypeMocks, sectionMocks, entityMocks, dataTypeMocks, dashboardMocks, macroMocks, utilMocks, localizationMocks, prevaluesMocks) { +function initBackEnd($httpBackend, contentMocks, mediaMocks, treeMocks, userMocks, contentTypeMocks, sectionMocks, entityMocks, dataTypeMocks, dashboardMocks, macroMocks, utilMocks, localizationMocks, prevaluesMocks, authMocks) { console.log("httpBackend inited"); - + //Register mocked http responses contentMocks.register(); mediaMocks.register(); @@ -19,6 +19,7 @@ function initBackEnd($httpBackend, contentMocks, mediaMocks, treeMocks, userMock localizationMocks.register(); prevaluesMocks.register(); entityMocks.register(); + authMocks.register(); $httpBackend.whenGET(/^..\/config\//).passThrough(); $httpBackend.whenGET(/^views\//).passThrough(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index a97773f77e..72957e1c72 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -368,6 +368,28 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#contentTypePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens a content type picker in infinite editing, the submit callback returns an array of selected items + * + * @param {Object} editor rendering options + * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * + * @returns {Object} editor object + */ + function contentTypePicker(editor) { + editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; + editor.size = "small"; + editor.section = "settings"; + editor.treeAlias = "documentTypes"; + open(editor); + } /** * @ngdoc method * @name umbraco.services.editorService#copy @@ -881,6 +903,7 @@ When building a custom infinite editor view you can use the same components as a mediaEditor: mediaEditor, contentEditor: contentEditor, contentPicker: contentPicker, + contentTypePicker: contentTypePicker, copy: copy, move: move, embed: embed, diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 1e48500bb0..0b5e10379f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -81,6 +81,7 @@ @import "forms/umb-validation-label.less"; // Umbraco Components +@import "components/application/umb-app-a11y.less"; @import "components/application/umb-app-header.less"; @import "components/application/umb-app-content.less"; @import "components/application/umb-tour.less"; @@ -162,6 +163,7 @@ @import "components/umb-textarea.less"; @import "components/umb-dropdown.less"; @import "components/umb-range-slider.less"; +@import "components/umb-number.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index f21c7f3106..91a6c29a17 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -67,6 +67,21 @@ border-color: rgba(0,0,0,0.09); } +// Button Reset - remove the default browser styles from the button element +// -------------------------------------------------- + +.btn-reset { + padding: 0; + margin: 0; + border: none; + background: none; + color: currentColor; + font-family: @baseFontFamily; + font-size: @baseFontSize; + line-height: @baseLineHeight; + cursor: pointer; +} + // Button Sizes // -------------------------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-a11y.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-a11y.less new file mode 100644 index 0000000000..7e127c5db1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-a11y.less @@ -0,0 +1,10 @@ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less index d4b21e66f0..bd1b8ab07a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less @@ -16,27 +16,26 @@ margin-right: -10px; } -.umb-app-header__action a { +.umb-app-header__button { padding-left: 10px; padding-right: 10px; text-decoration: none; display: flex; align-items: center; height: @appHeaderHeight; -} - -.umb-app-header__action a { outline: none; + &:focus { .tabbing-active & { .umb-app-header__action-icon::after { content: ''; position: absolute; z-index:10000; - top: -7px; - left: -7px; + top: 50%; + left: 50%; width: 36px; height: 35px; + transform: translate(-50%, -50%); border-radius: 3px; box-shadow: 0 0 2px @pinkLight, inset 0 0 2px 1px @pinkLight; } @@ -51,7 +50,7 @@ font-size: 22px; } -.umb-app-header__action a:hover .umb-app-header__action-icon, -.umb-app-header__action a:focus .umb-app-header__action-icon { +.umb-app-header__button:hover .umb-app-header__action-icon, +.umb-app-header__button:focus .umb-app-header__action-icon { opacity: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less index 6f23677a1c..a621370d02 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less @@ -21,23 +21,23 @@ background-color: @inputBorder; position: relative; transition: background-color 120ms; - - .umb-toggle:hover &, + + .umb-toggle:hover &, .umb-toggle:focus & { border-color: @inputBorderFocus; } - + .umb-toggle.umb-toggle--checked & { border-color: @ui-btn; background-color: @ui-btn; - + &:hover { background-color: @ui-btn-hover; } } - - .umb-toggle.umb-toggle--checked:focus & { - border-color: black; + + .tabbing-active .umb-toggle:focus & { + box-shadow: 0 0 0 2px highlight; } } @@ -54,12 +54,12 @@ background-color: @white; border-radius: 8px; transition: transform 120ms ease-in-out, background-color 120ms; - + .umb-toggle.umb-toggle--checked & { transform: translateX(20px); background-color: white; } - + } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less index 6af0641d8c..30a32b8123 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-multiple-textbox.less @@ -1,9 +1,10 @@ .umb-multiple-textbox{ &__confirm{ position: relative; + display: inline-block; &-action{ - margin: 0; + margin: -2px 0 0 0; padding: 2px; background: transparent; border: 0 none; @@ -11,6 +12,10 @@ } } +.umb-multiple-textbox .icon-wrapper { + width: 50px; +} + .umb-multiple-textbox .textbox-wrapper { align-items: center; margin-bottom: 15px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index f1a6300481..59c90972d2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -56,13 +56,20 @@ -webkit-user-select: none; -o-user-select: none; user-select: none; + + &:hover { + .umb-nested-content__heading .umb-nested-content__item-name { + padding-right: 60px; + } + } + } .umb-nested-content__heading { line-height: 20px; position: relative; margin-top:1px; - padding: 15px 20px; + padding: 15px 5px; color:@ui-option-type; border-radius: 3px 3px 0 0; @@ -71,16 +78,21 @@ } i { - display: inline; - margin-right: 10px; + position: absolute; + margin-top: -1px; } .umb-nested-content__item-name { - display: inline; + display: block; max-height: 20px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + padding-left: 5px; + + &.--has-icon { + padding-left: 30px; + } } } @@ -89,9 +101,10 @@ opacity: 0; transition: opacity 120ms ease-in-out; position: absolute; - right: 8px; - top: 4px; + right: 0; + top: 3px; padding: 5px; + background-color: @white; } .umb-nested-content__item--active > .umb-nested-content__header-bar { @@ -100,6 +113,9 @@ &:hover { color:@ui-option-type; } + .umb-nested-content__item-name { + padding-right: 60px; + } } .umb-nested-content__icons { background-color: @ui-active; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less new file mode 100644 index 0000000000..c0e3dee8e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-number.less @@ -0,0 +1,3 @@ +.umb-number { + .umb-property-editor--limit-width(); +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less index f39096b565..47a2af9231 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-group-preview.less @@ -34,9 +34,13 @@ margin-top: 2px; } -.umb-user-group-preview__permission { +.umb-user-group-preview__permissions { font-size: 13px; color: @gray-3; + + .umb-user-group-preview__permission:not(:last-child):after { + content: ', '; + } } .umb-user-group-preview__actions { @@ -61,4 +65,4 @@ .umb-user-group-preview__action--red:hover { color: @red; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less index 69bbeef0af..b9c8b909e8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less @@ -57,3 +57,12 @@ .mb5 { margin-bottom: @spacing-extra-large; } .mb6 { margin-bottom: @spacing-extra-extra-large; } .mb7 { margin-bottom: @spacing-extra-extra-extra-large; } + +.ml0 { margin-left: @spacing-none; } +.ml1 { margin-left: @spacing-extra-small; } +.ml2 { margin-left: @spacing-small; } +.ml3 { margin-left: @spacing-medium; } +.ml4 { margin-left: @spacing-large; } +.ml5 { margin-left: @spacing-extra-large; } +.ml6 { margin-left: @spacing-extra-extra-large; } +.ml7 { margin-left: @spacing-extra-extra-extra-large; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 93d7936326..fab6ba4069 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -91,6 +91,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 915abf62b0..b01fe1827b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -92,6 +92,14 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } + if (vm.treeAlias === "documentTypes") { + vm.entityType = "DocumentType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectContentType").then(function(value){ + $scope.model.title = value; + }); + } + } else if (vm.treeAlias === "member" || vm.section === "member") { vm.entityType = "Member"; if (!$scope.model.title) { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index ac919d3e41..93c3a9b50d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,3 +1,4 @@ +
@@ -10,25 +11,28 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html index 56d9eae16c..35bf725e0a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html @@ -1,5 +1,5 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index 95c628376b..dcad858781 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 87e94193e1..37f363f50d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -26,6 +26,8 @@ umb-auto-focus val-server-field="{{serverValidationNameField}}" required + aria-required="true" + aria-invalid="{{contentForm.headerNameForm.headerName.$invalid ? true : false}}" autocomplete="off" maxlength="255" /> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html index 2005666292..46fb11c310 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html @@ -1,5 +1,5 @@ -
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html index 32fc0a687a..00ca425d7a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html @@ -5,10 +5,10 @@
{{ name }}
{{ description }}
-
+
Permissions: - {{ permission.name }}, + {{ permission.name }}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html index 8617f94e5e..33c861c3d0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html @@ -5,15 +5,15 @@
{{ name }}
-
+
Sections: - {{ section.name }}, + {{ section.name }} All sections
-
+
Content start node: No start node selected @@ -21,7 +21,7 @@
-
+
Media start node: No start node selected @@ -29,10 +29,10 @@
-
+
Permissions: - {{ permission.name }}, + {{ permission.name }}
@@ -43,4 +43,4 @@ Remove
-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index f101450705..d04c707b25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -7,86 +7,103 @@ * The controller for the content creation dialog */ function contentCreateController($scope, - $routeParams, - contentTypeResource, - iconHelper, - $location, - navigationService, - blueprintConfig) { - - var mainCulture = $routeParams.mculture ? $routeParams.mculture : null; + $routeParams, + contentTypeResource, + iconHelper, + $location, + navigationService, + blueprintConfig, + authResource, + contentResource) { - function initialize() { - $scope.allowedTypes = null; - contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { - $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - }); + var mainCulture = $routeParams.mculture ? $routeParams.mculture : null; - $scope.selectContentType = true; - $scope.selectBlueprint = false; - $scope.allowBlank = blueprintConfig.allowBlank; - } + function initialize() { + $scope.allowedTypes = null; + contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { + $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); + }); - function close() { - navigationService.hideMenu(); - } + if ($scope.currentNode.id > -1) { + authResource.getCurrentUser().then(function(currentUser) { + if (currentUser.allowedSections.indexOf("settings") > -1) { + $scope.hasSettingsAccess = true; + contentResource.getById($scope.currentNode.id).then(function(data) { + $scope.contentTypeId = data.contentTypeId; + }); + } + }); + } - function createBlank(docType) { - $location - .path("/content/content/edit/" + $scope.currentNode.id) - .search("doctype", docType.alias) - .search("create", "true") - /* when we create a new node we want to make sure it uses the same - language as what is selected in the tree */ - .search("cculture", mainCulture); - close(); - } - - function createOrSelectBlueprintIfAny(docType) { - // map the blueprints into a collection that's sortable in the view - var blueprints = _.map(_.pairs(docType.blueprints || {}), function (pair) { - return { - id: pair[0], - name: pair[1] - }; - }); - $scope.docType = docType; - if (blueprints.length) { - if (blueprintConfig.skipSelect) { - createFromBlueprint(blueprints[0].id); - } else { - $scope.selectContentType = false; - $scope.selectBlueprint = true; - $scope.selectableBlueprints = blueprints; - } - } else { - createBlank(docType); + $scope.selectContentType = true; + $scope.selectBlueprint = false; + $scope.allowBlank = blueprintConfig.allowBlank; } - } - function createFromBlueprint(blueprintId) { - $location - .path("/content/content/edit/" + $scope.currentNode.id) - .search("doctype", $scope.docType.alias) - .search("create", "true") - .search("blueprintId", blueprintId); - close(); - } + function close() { + navigationService.hideMenu(); + } - $scope.closeDialog = function(showMenu) { - navigationService.hideDialog(showMenu); - }; + function createBlank(docType) { + $location + .path("/content/content/edit/" + $scope.currentNode.id) + .search("doctype", docType.alias) + .search("create", "true") + /* when we create a new node we want to make sure it uses the same + language as what is selected in the tree */ + .search("cculture", mainCulture); + close(); + } - $scope.createBlank = createBlank; - $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; - $scope.createFromBlueprint = createFromBlueprint; + function createOrSelectBlueprintIfAny(docType) { + // map the blueprints into a collection that's sortable in the view + var blueprints = _.map(_.pairs(docType.blueprints || {}), function (pair) { + return { + id: pair[0], + name: pair[1] + }; + }); + $scope.docType = docType; + if (blueprints.length) { + if (blueprintConfig.skipSelect) { + createFromBlueprint(blueprints[0].id); + } else { + $scope.selectContentType = false; + $scope.selectBlueprint = true; + $scope.selectableBlueprints = blueprints; + } + } else { + createBlank(docType); + } + } - // the current node changes behind the scenes when the context menu is clicked without closing - // the default menu first, so we must watch the current node and re-initialize accordingly - var unbindModelWatcher = $scope.$watch("currentNode", initialize); - $scope.$on('$destroy', function () { - unbindModelWatcher(); - }); + function createFromBlueprint(blueprintId) { + $location + .path("/content/content/edit/" + $scope.currentNode.id) + .search("doctype", $scope.docType.alias) + .search("create", "true") + .search("blueprintId", blueprintId); + close(); + } + + $scope.close = function() { + close(); + } + + $scope.closeDialog = function (showMenu) { + navigationService.hideDialog(showMenu); + }; + + $scope.createBlank = createBlank; + $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; + $scope.createFromBlueprint = createFromBlueprint; + + // the current node changes behind the scenes when the context menu is clicked without closing + // the default menu first, so we must watch the current node and re-initialize accordingly + var unbindModelWatcher = $scope.$watch("currentNode", initialize); + $scope.$on('$destroy', function () { + unbindModelWatcher(); + }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index 94299f6a54..97306e0ea8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -6,9 +6,15 @@
Create a page under {{currentNode.name}}
Select a blueprint
-

- -

+
+

+
+

+ + + +
+
    @@ -56,4 +62,4 @@
-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js new file mode 100644 index 0000000000..295263a47c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.controller.js @@ -0,0 +1,38 @@ +function ProfilerController($scope, $cookies, $http, umbRequestHelper) { + var vm = this; + + vm.loading = true; + vm.toggle = toggle; + + function toggle() { + if (vm.alwaysOn === true) { + $cookies.remove("UMB-DEBUG", { + path: "/" + }); + vm.alwaysOn = false; + } + else { + $cookies.put("UMB-DEBUG", "true", { + path: "/", + expires: "Tue, 01 Jan 2100 00:00:01 GMT" + }); + vm.alwaysOn = true; + } + } + + function init() { + vm.alwaysOn = $cookies.get("UMB-DEBUG") === "true"; + + umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("webProfilingBaseUrl", "GetStatus")), + "Failed to retrieve status for web profiling" + ).then(function(status) { + vm.loading = false; + vm.profilerEnabled = status.Enabled; + }); + } + + init(); +} + +angular.module("umbraco").controller("Umbraco.Dashboard.ProfilerController", ProfilerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html new file mode 100644 index 0000000000..2a7419c0ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/profiler.html @@ -0,0 +1,41 @@ +
+ + +

Performance profiling

+
+

+ Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

+

+ If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. +

+

+ If you want the profiler to be activated by default for all page renderings, you can use the toggle below. + It will set a cookie in your browser, which then activates the profiler automatically. + In other words, the profiler will only be active by default in your browser - not everyone else's. +

+

 

+
+
+
Activate the profiler by default
+
+
+ +
+
+

Friendly reminder

+

+ You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. +

+
+
+

+ Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. +

+

+ Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. +

+
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 7c1f996931..5ceb5f01f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -56,7 +56,7 @@ function onInit() { // get init values from model when in infinite mode - if(infiniteMode) { + if (infiniteMode) { documentTypeId = $scope.model.id; create = $scope.model.create; noTemplate = $scope.model.notemplate; @@ -89,8 +89,7 @@ "name": vm.labels.design, "alias": "design", "icon": "icon-document-dashed-line", - "view": "views/documenttypes/views/design/design.html", - "active": true + "view": "views/documenttypes/views/design/design.html" }, { "name": vm.labels.listview, @@ -291,6 +290,28 @@ }); vm.page.navigation = buttons; + initializeActiveNavigationPanel(); + } + + function initializeActiveNavigationPanel() { + // Initialise first loaded panel based on page route paramater + // i.e. ?view=design|listview|permissions + var initialViewSetFromRouteParams = false; + var view = $routeParams.view; + if (view) { + var viewPath = "views/documenttypes/views/" + view + "/" + view + ".html"; + for (var i = 0; i < vm.page.navigation.length; i++) { + if (vm.page.navigation[i].view === viewPath) { + vm.page.navigation[i].active = true; + initialViewSetFromRouteParams = true; + break; + } + } + } + + if (initialViewSetFromRouteParams === false) { + vm.page.navigation[0].active = true; + } } /* ---------- SAVE ---------- */ diff --git a/src/Umbraco.Web.UI.Client/src/views/media/create.html b/src/Umbraco.Web.UI.Client/src/views/media/create.html index 13c12f3c9a..d93d4f0e30 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/create.html @@ -4,9 +4,15 @@
Create under {{currentNode.name}}
-

- -

+
+

+
+

+ + + +
+
    @@ -38,7 +44,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.create.controller.js index 487f53a5ba..b1fddb8928 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.create.controller.js @@ -6,13 +6,24 @@ * @description * The controller for the media creation dialog */ -function mediaCreateController($scope, $routeParams, $location, mediaTypeResource, iconHelper, navigationService) { +function mediaCreateController($scope, $location, mediaTypeResource, iconHelper, navigationService, authResource, mediaResource) { function initialize() { $scope.allowedTypes = null; mediaTypeResource.getAllowedTypes($scope.currentNode.id).then(function(data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); }); + + if ($scope.currentNode.id > -1) { + authResource.getCurrentUser().then(function(currentUser) { + if (currentUser.allowedSections.indexOf("settings") > -1) { + $scope.hasSettingsAccess = true; + mediaResource.getById($scope.currentNode.id).then(function (data) { + $scope.mediaTypeId = data.contentType.id; + }); + } + }); + } } $scope.createMediaItem = function(docType) { @@ -21,6 +32,10 @@ function mediaCreateController($scope, $routeParams, $location, mediaTypeResourc }; $scope.close = function() { + navigationService.hideMenu(); + }; + + $scope.closeDialog = function () { const showMenu = true; navigationService.hideDialog(showMenu); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index 5f8000569d..02266bf318 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -284,7 +284,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, /** Callback for when user clicks the back-icon */ $scope.onBack = function() { if ($scope.page.listViewPath) { - $location.path($scope.page.listViewPath); + $location.path($scope.page.listViewPath.split("?")[0]); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js index a0da3d0ea8..0acb87f42e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js @@ -12,7 +12,7 @@ function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, - $filter, $q, localizationService, overlayHelper, eventsService, angularHelper) { + $q, localizationService, overlayHelper, eventsService, angularHelper) { var vm = this; var evts = []; @@ -36,7 +36,7 @@ function onInit() { // get init values from model when in infinite mode - if(infiniteMode) { + if (infiniteMode) { mediaTypeId = $scope.model.id; create = $scope.model.create; vm.saveButtonKey = "buttons_saveAndClose"; @@ -81,8 +81,7 @@ "name": vm.labels.design, "alias": "design", "icon": "icon-document-dashed-line", - "view": "views/mediatypes/views/design/design.html", - "active": true + "view": "views/mediatypes/views/design/design.html" }, { "name": vm.labels.listview, @@ -153,8 +152,31 @@ ] } ]; + + initializeActiveNavigationPanel(); }); + function initializeActiveNavigationPanel() { + // Initialise first loaded page based on page route paramater + // i.e. ?view=design|listview|permissions + var initialViewSetFromRouteParams = false; + var view = $routeParams.view; + if (view) { + var viewPath = "views/mediatypes/views/" + view + "/" + view + ".html"; + for (var i = 0; i < vm.page.navigation.length; i++) { + if (vm.page.navigation[i].view === viewPath) { + vm.page.navigation[i].active = true; + initialViewSetFromRouteParams = true; + break; + } + } + } + + if (initialViewSetFromRouteParams === false) { + vm.page.navigation[0].active = true; + } + } + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { vm.page.modelsBuilder = result; if (result) { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.controller.js new file mode 100644 index 0000000000..dcb2c0e582 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.controller.js @@ -0,0 +1,54 @@ +function ContentTypePickerController($scope, contentTypeResource, editorService, angularHelper) { + var vm = this; + vm.loading = false; + vm.contentTypes = []; + vm.remove = remove; + vm.add = add; + + var allContentTypes = null; + + function init() { + vm.loading = true; + contentTypeResource.getAll().then(function (all) { + allContentTypes = all; + vm.loading = false; + // the model value is a comma separated list of content type aliases + var currentContentTypes = _.map(($scope.model.value || "").split(","), function (s) { return s.trim(); }); + vm.contentTypes = _.filter(allContentTypes, function (contentType) { + return currentContentTypes.indexOf(contentType.alias) >= 0; + }); + }); + } + + function add() { + editorService.contentTypePicker({ + multiPicker: true, + submit: function (model) { + var newContentTypes = _.map(model.selection, function (selected) { + return _.findWhere(allContentTypes, {udi: selected.udi}); + }); + vm.contentTypes = _.uniq(_.union(vm.contentTypes, newContentTypes)); + updateModel(); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }); + } + + function remove(contentType) { + vm.contentTypes = _.without(vm.contentTypes, contentType); + updateModel(); + } + + function updateModel() { + // the model value is a comma separated list of content type aliases + $scope.model.value = _.pluck(vm.contentTypes, "alias").join(); + angularHelper.getCurrentForm($scope).$setDirty(); + } + + init(); +} + +angular.module('umbraco').controller("Umbraco.PrevalueEditors.ContentTypePickerController", ContentTypePickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.html new file mode 100644 index 0000000000..cd89fe4cb5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.html @@ -0,0 +1,23 @@ +
+ + + +
+ + + + + Add + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 62099734fb..869ca71eee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -77,6 +77,11 @@ function dateTimePickerController($scope, notificationsService, assetsService, a setDate(date); setDatePickerVal(); }; + + $scope.inputChanged = function() { + setDate($scope.model.datetimePickerValue); + setDatePickerVal(); + } //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 0d3fad580e..99961110aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -16,6 +16,7 @@ id="{{model.alias}}" type="text" ng-model="model.datetimePickerValue" + ng-blur="inputChanged()" ng-required="model.validation.mandatory" val-server="value" class="datepickerinput"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html index 51be936015..28cb909cc0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html @@ -2,7 +2,7 @@
- - + +
+ -
- - - - -
+
+ + + +
+
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 92f02b9f5b..ff62629b1c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -4,19 +4,24 @@ class="umb-property-editor umb-textstring textstring" val-server="value" ng-required="model.validation.mandatory" + aria-required="model.validation.mandatory" + aria-invalid="False" ng-trim="false" ng-keyup="model.change()" /> - - - {{textboxFieldForm.textbox.errorMsg}} - Required - + +
+

{{model.label}} {{textboxFieldForm.textbox.errorMsg}}

+ +

Required

+
- %0% characters left. +

{{model.label}} %0% characters left.

+

%0% characters left.

- Maximum %0% characters, %1% too many. +

{{model.label}} Maximum %0% characters, %1% too many.

+
diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js index 278d52c9a2..5954a2f984 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js @@ -33,7 +33,10 @@ } - beforeEach(inject(function ($controller, $rootScope, $q, $location) { + beforeEach(inject(function ($controller, $rootScope, $q, $location, authMocks) { + + authMocks.register(); + contentTypeResource = { getAllowedTypes: function () { var def = $q.defer(); diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml index 31a3bd604d..af84a603db 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml @@ -1,4 +1,4 @@ -@using System.Web.Mvc.Html +@using System.Web.Mvc.Html @using ClientDependency.Core.Mvc @using Umbraco.Web @using Umbraco.Web.Models @@ -7,6 +7,7 @@ @{ var loginModel = new LoginModel(); + loginModel.RedirectUrl = HttpContext.Current.Request.Url.AbsolutePath; Html.EnableClientValidation(); Html.EnableUnobtrusiveJavaScript(); @@ -20,6 +21,7 @@ @using (Html.BeginUmbracoForm("HandleLogin")) { + @Html.HiddenFor(m => loginModel.RedirectUrl)
Login diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 9e6bdc5e57..c200f1f35e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -434,6 +434,7 @@ Vælg link Vælg makro Vælg indhold + Vælg indholdstype Vælg medie startnode Vælg medlem Vælg medlemsgruppe @@ -1278,17 +1279,17 @@ Mange hilsner fra Umbraco robotten Kompositioner Gruppe - Du har ikke tilføjet nogle grupper + Du har ikke tilføjet nogen grupper Tilføj gruppe Nedarvet fra Tilføj egenskab Påkrævet label - Aktiver listevisning - Konfigurer indholdet til at blive vist i en sorterbar og søgbar liste, dens børn vil ikke blive vist i træet + Aktivér listevisning + Konfigurér indholdet til at blive vist i en sortérbar og søgbar liste; undersider vil ikke blive vist i træet Tilladte skabeloner Vælg hvilke skabeloner der er tilladt at bruge på dette indhold Tillad på rodniveau - Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under inhold og mediearkiv + Kun dokumenttyper med denne indstilling aktiveret kan oprettes i rodniveau under indhold og mediearkiv Tilladte typer Tillad at oprette indhold af en specifik type under denne Vælg child node @@ -1325,20 +1326,20 @@ Mange hilsner fra Umbraco robotten Tilføj sprog Påkrævet sprog Egenskaber på dette sprog skal være udfyldt før noden kan blive udgivet. - Standard sprog - Et Umbraco site kan kun have et standard sprog. - Ved at skifte standardsprog kan resultere i standard indhold mangler. - Fallsback til - Ingen fallback sprog - For at tillade flersproget indhold til at falde tilbage på et andet sprog, hvis det ikke er tilgængelig i det anmodet sprog, vælg det her. - Fallback sprog + Standardsprog + Et Umbraco-site kan kun have ét standardsprog. + At skifte standardsprog kan resultere i at standardindhold mangler. + Fallback til + Intet fallback-sprog + For at tillade flersproget indhold, som ikke er tilgængeligt i det anmodede sprog, skal du her vælge et sprog at falde tilbage på. + Fallback-sprog Tilføj parameter - Rediger parameter + Redigér parameter Indtast makronavn Parametre - Definer de parametre, der skal være tilgængelige, når du bruger denne makro. + Definér de parametre der skal være tilgængelige, når du bruger denne makro. Vælg partial view makrofil @@ -1605,6 +1606,7 @@ Mange hilsner fra Umbraco robotten Published Cache Models Builder Health Check + Profiling Kom godt i gang Installer Umbraco Forms diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 189bd9f10b..175146aafd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -293,8 +293,8 @@ Ready to Publish? Ready to Save? Send for approval - Select the date and time to publish and/or unpublish the content item. - Create new + Select the date and time to publish and/or unpublish the content item. + Create new Paste from clipboard @@ -304,7 +304,7 @@ Content Template created A Content Template was created from '%0%' Another Content Template with the same name already exists - A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + A Content Template is predefined content that an editor can select to use as the basis for creating new content Click to upload @@ -329,8 +329,12 @@ Select the document type you want to make a content template for Enter a folder name Choose a type and a title - "document types".]]> - "media types".]]> + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type Document Type without a template New folder New data type @@ -447,6 +451,7 @@ Select link Select macro Select content + Select content type Select media start node Select member Select member group @@ -1594,7 +1599,7 @@ To manage your website, simply open the Umbraco back office and start adding con Element type Is an Element type An Element type is meant to be used for instance in Nested Content, and not in the tree - This is not applicable for an Element type + This is not applicable for an Element type You have made changes to this property. Are you sure you want to discard them? @@ -2116,6 +2121,7 @@ To manage your website, simply open the Umbraco back office and start adding con Published Status Models Builder Health Check + Profiling Getting Started Install Umbraco Forms > diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 6ce6f82ccc..6aa6054992 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -297,8 +297,8 @@ Ready to Publish? Ready to Save? Send for approval - Select the date and time to publish and/or unpublish the content item. - Create new + Select the date and time to publish and/or unpublish the content item. + Create new Paste from clipboard @@ -308,7 +308,7 @@ Content Template created A Content Template was created from '%0%' Another Content Template with the same name already exists - A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + A Content Template is predefined content that an editor can select to use as the basis for creating new content Click to upload @@ -334,9 +334,12 @@ Select the document type you want to make a content template for Enter a folder name Choose a type and a title - "document types".]]> - "media types".]]> - Document Type without a template + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type Document Type without a template New folder New data type New JavaScript file @@ -451,6 +454,7 @@ Select link Select macro Select content + Select content type Select media start node Select member Select member group @@ -2131,6 +2135,7 @@ To manage your website, simply open the Umbraco back office and start adding con Published Status Models Builder Health Check + Profiling Getting Started Install Umbraco Forms diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml index bd847a605a..9287856fb2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml @@ -12,6 +12,7 @@ Kopiera Skapa Skapa paket + Skapa innehållsmall Standardvärde Ta bort Avaktivera @@ -59,6 +60,42 @@ Visar för + Innehållet raderat + Innehållet avpublicerat + Innehållet avpublicerat för språk: %0% + Spara och publicera utförd av användaren + Innehåll publicerat för språk: %0% + Innehåll sparat + Innehåll sparat för språk: %0% + Innehåll flyttat + Innehåll kopierat + Innehållet rullades tillbaka + Innehåll skickat för publicering + Innehåll skickat för publicering för språk: %0% + Sortering av underliggande objekt utfört av användaren + Kopiera + Publicera + Publicera + Flytta + Spara + Spara + Ta bort + Avpublicera + Avpublicera + Rulla tillbaka + Skicka till publicering + Skicka till publicering + Sortera + Historik (alla varianter) + + + Skapa en ny innehållsmall för '%0%' + Tom + Välj en innehållsmall + Innehållsmall skapad + En innehållsmall skapades från '%0%' + En annan innehållsmall med samma namn finns redan + En innehållsmall är fördefinierat innehåll som en redaktör kan välja att använda som grund för att skapa nytt innehåll Fetstil @@ -77,6 +114,7 @@ Numrerad lista Infoga macro Infoga bild + Publicera och stäng Ändra relation Återvänd till lista Spara @@ -142,14 +180,18 @@ Medlemstyp Inget datum valt Sidnamn + Inget innehåll kan läggas till för det här objektet Ej medlem av grupp(er) Egenskaper Detta dokument är publicerat men syns inte eftersom den överordnade sidan %0% inte är publicerad Oops: detta dokument är publicerat men finns inte i cacheminnet (internt fel) Publicera + Publicerad Publiceringsstatus Publiceringsdatum Rensa datum + Välj datum och tid för att publicera och / eller avpublicera innehållsobjektet. + Välj datum Sorteringsordningen har uppdaterats För att sortera noderna, dra i dem eller klicka på någon av kolumnrubrikerna. Du kan markera flera noder samtidigt genom att hålla nere SHIFT eller CONTROL medan du klickar Statistik @@ -180,6 +222,14 @@ Välkommen Besök + + Komma igång + URL-omdirigeringshantering + Innehåll + Välkommen + Komma igång + Installera Umbraco Forms + Stay Discard changes @@ -187,27 +237,27 @@ Are you sure you want to navigate away from this page? - you have unsaved changes - Done - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + Klar + Tog bort %0% objekt + Tog bort %0% objekt + Tog bort %0% av %1% objekt + Tog bort %0% av %1% objekt + Publicerade %0% objekt + Publicerade %0% objekt + Publicerade %0% av %1% objekt + Publicerade %0% av %1% objekt + Avpublicerade %0% objekt + Avpublicerade %0% objekt + Avpublicerade %0% av %1% objekt + Avpublicerade %0% av %1% objekt + Flyttade %0% objekt + Flyttade %0% objekt + Flyttade %0% av %1% objekt + Flyttade %0% av %1% objekt + Kopierade %0% objekt + Kopierade %0% objekt + Kopierade %0% av %1% objekt + Kopierade %0% av %1% objekt Namn @@ -317,6 +367,7 @@ Kommentar Bekräfta Begränsa proportioner + Innehåll Fortsätt Kopiera Skapa @@ -337,8 +388,11 @@ Fel Hitta Mapp + Generella + Grupper Höjd Hjälp + Historik Ikon Importera Innermarginal @@ -347,6 +401,7 @@ Justera Språk Layout + Länkar Laddar Låst Logga in @@ -362,6 +417,7 @@ OK Öppna eller + Sortering Lösenord Sökväg Ett ögonblick... @@ -370,12 +426,15 @@ E-postadress för formulärsdata Papperskorg Återstående + Ta bort Döp om Förnya Obligatorisk Försök igen Rättigheter + Schemalagd publicering Sök + Tyvärr kan vi inte hitta det du söker. Sökresultat Server Visa @@ -508,6 +567,7 @@ Klicka för att ladda upp + eller klicka här för att välja filer Välj sida ovan... @@ -714,6 +774,7 @@ Stilar + Skapad Redigera sidmall Lägg in innehållsyta Lägg in platshållare för innehållsyta @@ -847,6 +908,7 @@ Du kan byta ditt lösenord för Umbraco Back Office genom att fylla i nedanstående formulär och klicka på knappen "Ändra lösenord". Bekräfta det nya lösenordet Innehållskanal + Skapa användare Fält för beskrivning Avaktivera användare Dokumenttyp @@ -873,6 +935,8 @@ Återställ lösenord Sök igenom alla undernoder Sessionen går ut + Namn (A-Z) + Namn (Z-A) Startnod i innehåll Användarens namn Användarrättigheter diff --git a/src/Umbraco.Web/Dashboards/ProfilerDashboard.cs b/src/Umbraco.Web/Dashboards/ProfilerDashboard.cs new file mode 100644 index 0000000000..a4f51398e8 --- /dev/null +++ b/src/Umbraco.Web/Dashboards/ProfilerDashboard.cs @@ -0,0 +1,18 @@ +using System; +using Umbraco.Core.Composing; +using Umbraco.Core.Dashboards; + +namespace Umbraco.Web.Dashboards +{ + [Weight(60)] + public class ProfilerDashboardDashboard : IDashboard + { + public string Alias => "settingsProfiler"; + + public string[] Sections => new [] { "settings" }; + + public string View => "views/dashboard/settings/profiler.html"; + + public IAccessRule[] AccessRules => Array.Empty(); + } +} diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 4966328782..6b6cfacacc 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -20,6 +20,7 @@ using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.Profiling; using Umbraco.Web.PropertyEditors; using Umbraco.Web.Trees; using Constants = Umbraco.Core.Constants; @@ -308,6 +309,10 @@ namespace Umbraco.Web.Editors { "logViewerApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetNumberOfErrors(null, null)) + }, + { + "webProfilingBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetStatus()) } } }, diff --git a/src/Umbraco.Web/Editors/MacroRenderingController.cs b/src/Umbraco.Web/Editors/MacroRenderingController.cs index d182a32ade..efad07ce89 100644 --- a/src/Umbraco.Web/Editors/MacroRenderingController.cs +++ b/src/Umbraco.Web/Editors/MacroRenderingController.cs @@ -108,7 +108,7 @@ namespace Umbraco.Web.Editors //if it isn't supposed to be rendered in the editor then return an empty string //currently we cannot render a macro if the page doesn't yet exist - if (pageId == -1 || publishedContent == null || !m.UseInEditor) + if (pageId == -1 || publishedContent == null || m.DontRender) { var response = Request.CreateResponse(); //need to create a specific content result formatted as HTML since this controller has been configured diff --git a/src/Umbraco.Web/Editors/UpdateCheckController.cs b/src/Umbraco.Web/Editors/UpdateCheckController.cs index cd11382d13..132526576b 100644 --- a/src/Umbraco.Web/Editors/UpdateCheckController.cs +++ b/src/Umbraco.Web/Editors/UpdateCheckController.cs @@ -24,7 +24,8 @@ namespace Umbraco.Web.Editors { try { - var check = new org.umbraco.update.CheckForUpgrade(); + var check = new org.umbraco.update.CheckForUpgrade { Timeout = 2000 }; + var result = check.CheckUpgrade(UmbracoVersion.Current.Major, UmbracoVersion.Current.Minor, UmbracoVersion.Current.Build, @@ -37,6 +38,11 @@ namespace Umbraco.Web.Editors //this occurs if the server is down or cannot be reached return null; } + catch (System.Web.Services.Protocols.SoapException) + { + //this occurs if the server has a timeout + return null; + } } return null; } diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 99adf71742..274cc06dd2 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -223,6 +223,13 @@ namespace Umbraco.Web _method = method; _controllerName = controllerName; _encryptedString = UrlHelperRenderExtensions.CreateEncryptedRouteString(controllerName, controllerAction, area, additionalRouteVals); + + //For UmbracoForm's we want to add our routing string to the httpcontext items in the case where anti-forgery tokens are used. + //In which case our custom UmbracoAntiForgeryAdditionalDataProvider will kick in and validate the values in the request against + //the values that will be appended to the token. This essentially means that when anti-forgery tokens are used with UmbracoForm's forms, + //that each token is unique to the controller/action/area instead of the default ASP.Net implementation which is that the token is unique + //per user. + _viewContext.HttpContext.Items["ufprt"] = _encryptedString; } private readonly ViewContext _viewContext; diff --git a/src/Umbraco.Web/Logging/WebProfiler.cs b/src/Umbraco.Web/Logging/WebProfiler.cs index 14c1bb065f..512edb2296 100755 --- a/src/Umbraco.Web/Logging/WebProfiler.cs +++ b/src/Umbraco.Web/Logging/WebProfiler.cs @@ -68,6 +68,7 @@ namespace Umbraco.Web.Logging if (request.Result.Url.IsClientSideRequest()) return false; if (bool.TryParse(request.Result.QueryString["umbDebug"], out var umbDebug)) return umbDebug; if (bool.TryParse(request.Result.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Result.Cookies["UMB-DEBUG"]?.Value, out var cUmbDebug)) return cUmbDebug; return false; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs index dc72b0a81f..d4156d6db5 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemBasic.cs @@ -38,6 +38,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "updater")] public UserProfile Updater { get; set; } + public int ContentTypeId { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] [Required(AllowEmptyStrings = false)] public string ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 80358bfc7a..b6a90a93c3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -108,6 +108,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "treeNodeUrl")] public string TreeNodeUrl { get; set; } + [DataMember(Name = "contentTypeId")] + public int ContentTypeId { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] [Required(AllowEmptyStrings = false)] public string ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 2b30b0ac5a..dc0df4ca96 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -73,6 +73,7 @@ namespace Umbraco.Web.Models.Mapping target.AllowedActions = GetActions(source); target.AllowedTemplates = GetAllowedTemplates(source); target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(source.ContentType.Name); target.DocumentType = _commonMapper.GetContentType(source, context); @@ -117,6 +118,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Alias private void Map(IContent source, ContentItemBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Edited = source.Edited; diff --git a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs index 3da61bc9c0..05c006ec41 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs @@ -50,6 +50,7 @@ namespace Umbraco.Web.Models.Mapping { target.ContentApps = _commonMapper.GetContentApps(source); target.ContentType = _commonMapper.GetContentType(source, context); + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; target.CreateDate = source.CreateDate; @@ -75,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Edited -Updater -Alias private void Map(IMedia source, ContentItemBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Icon = source.ContentType.Icon; diff --git a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs index b230dcfe15..1c781255e7 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberMapDefinition.cs @@ -76,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Trashed -IsContainer -VariesByCulture private void Map(IMember source, MemberDisplay target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = source.ContentType.Name; target.CreateDate = source.CreateDate; @@ -101,6 +102,7 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -Trashed -Edited -Updater -Alias -VariesByCulture private void Map(IMember source, MemberBasic target, MapperContext context) { + target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; target.CreateDate = source.CreateDate; target.Email = source.Email; @@ -121,7 +123,7 @@ namespace Umbraco.Web.Models.Mapping //TODO: SD: I can't remember why this mapping is here? // Umbraco.Code.MapAll -Udi -Properties -ParentId -Path -SortOrder -Edited -Updater - // Umbraco.Code.MapAll -Trashed -Alias -ContentTypeAlias -VariesByCulture + // Umbraco.Code.MapAll -Trashed -Alias -ContentTypeId -ContentTypeAlias -VariesByCulture private void Map(MembershipUser source, MemberBasic target, MapperContext context) { target.CreateDate = source.CreationDate; diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 54215c2e8c..aa2e5f18a7 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Mvc public class RenderRouteHandler : IRouteHandler { // Define reserved dictionary keys for controller, action and area specified in route additional values data - private static class ReservedAdditionalKeys + internal static class ReservedAdditionalKeys { internal const string Controller = "c"; internal const string Action = "a"; @@ -134,36 +134,7 @@ namespace Umbraco.Web.Mvc return null; } - - string decryptedString; - try - { - decryptedString = encodedVal.DecryptWithMachineKey(); - } - catch (FormatException) - { - Current.Logger.Warn("A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); - return null; - } - - var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); - var decodedParts = new Dictionary(); - - foreach (var key in parsedQueryString.AllKeys) - { - decodedParts[key] = parsedQueryString[key]; - } - - //validate all required keys exist - - //the controller - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Controller)) - return null; - //the action - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Action)) - return null; - //the area - if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Area)) + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(encodedVal, out var decodedParts)) return null; foreach (var item in decodedParts.Where(x => new[] { @@ -417,7 +388,7 @@ namespace Umbraco.Web.Mvc return new UmbracoMvcHandler(requestContext); } - + private SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext, string controllerName) { return _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); diff --git a/src/Umbraco.Web/Profiling/WebProfilingController.cs b/src/Umbraco.Web/Profiling/WebProfilingController.cs new file mode 100644 index 0000000000..b3d580bc38 --- /dev/null +++ b/src/Umbraco.Web/Profiling/WebProfilingController.cs @@ -0,0 +1,19 @@ +using Umbraco.Web.Editors; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Profiling +{ + /// + /// The API controller used to display the state of the web profiler + /// + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + public class WebProfilingController : UmbracoAuthorizedJsonController + { + public object GetStatus() + { + return new + { + Enabled = Core.Configuration.GlobalSettings.DebugMode + }; + } + }} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 81239caec0..942f53b561 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -99,7 +99,7 @@ namespace Umbraco.Web.PropertyEditors { // process the file // no file, invalid file, reject change - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + if (UploadFileTypeValidator.IsValidFileExtension(file.FileName) == false) return null; // get the filepath diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 7bea542521..a828f3a58f 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -142,7 +142,7 @@ namespace Umbraco.Web.PropertyEditors { // process the file // no file, invalid file, reject change - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + if (UploadFileTypeValidator.IsValidFileExtension(file.FileName) == false) return null; // get the filepath diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs index b6333c3140..279872e2d2 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNode", "Node type", "treesource")] public MultiNodePickerConfigurationTreeSource TreeSource { get; set; } - [ConfigurationField("filter", "Allow items of type", "textstring", Description = "Separate with comma")] + [ConfigurationField("filter", "Allow items of type", "contenttypepicker", Description = "Select the applicable content types")] public string Filter { get; set; } [ConfigurationField("minNumber", "Minimum number of items", "number")] diff --git a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs index 5394aca5ba..6855ab3bb8 100644 --- a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Services; -using Umbraco.Core.Configuration; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.Composing; @@ -16,13 +16,27 @@ namespace Umbraco.Web.PropertyEditors { public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) { - if (!(value is JObject jobject) || jobject["selectedFiles"] == null) yield break; - - var fileNames = jobject["selectedFiles"].ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - foreach (var fileName in fileNames) + string selectedFiles = null; + if (value is JObject jobject && jobject["selectedFiles"] is JToken jToken) { - if (ValidateFileExtension(fileName) == false) + selectedFiles = jToken.ToString(); + } + else if (valueType?.InvariantEquals(ValueTypes.String) == true) + { + selectedFiles = value as string; + + if (string.IsNullOrWhiteSpace(selectedFiles)) + yield break; + } + + var fileNames = selectedFiles?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + + if (fileNames == null || !fileNames.Any()) + yield break; + + foreach (string filename in fileNames) + { + if (IsValidFileExtension(filename) == false) { //we only store a single value for this editor so the 'member' or 'field' // we'll associate this error with will simply be called 'value' @@ -30,11 +44,11 @@ namespace Umbraco.Web.PropertyEditors } } } - - internal static bool ValidateFileExtension(string fileName) + + internal static bool IsValidFileExtension(string fileName) { if (fileName.IndexOf('.') <= 0) return false; - var extension = Path.GetExtension(fileName).TrimStart("."); + var extension = new FileInfo(fileName).Extension.TrimStart("."); return Current.Configs.Settings().Content.IsFileAllowedForUpload(extension); } } diff --git a/src/Umbraco.Web/Runtime/WebFinalComponent.cs b/src/Umbraco.Web/Runtime/WebFinalComponent.cs index 42ff0ee5e6..ba606e8d5e 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComponent.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComponent.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Web.Helpers; using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; @@ -8,6 +9,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Web.Install; using Umbraco.Web.Mvc; +using Umbraco.Web.Security; using Umbraco.Web.WebApi; namespace Umbraco.Web.Runtime @@ -34,6 +36,8 @@ namespace Umbraco.Web.Runtime // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); + + AntiForgeryConfig.AdditionalDataProvider = new UmbracoAntiForgeryAdditionalDataProvider(AntiForgeryConfig.AdditionalDataProvider); } public void Terminate() diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..c6ad4c6901 --- /dev/null +++ b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,92 @@ +using System; +using Umbraco.Web.Mvc; +using Umbraco.Core; +using System.Web.Helpers; +using System.Web; +using Newtonsoft.Json; +using Umbraco.Web.Composing; + +namespace Umbraco.Web.Security +{ + /// + /// A custom to create a unique antiforgery token/validator per form created with BeginUmbracoForm + /// + public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider + { + private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; + + /// + /// Constructor, allows wrapping a default provider + /// + /// + public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) + { + _defaultProvider = defaultProvider; + } + + public string GetAdditionalData(HttpContextBase context) + { + return JsonConvert.SerializeObject(new AdditionalData + { + Stamp = DateTime.UtcNow.Ticks, + //this value will be here if this is a BeginUmbracoForms form + Ufprt = context.Items["ufprt"]?.ToString(), + //if there was a wrapped provider, add it's value to the json, else just a static value + WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" + }); + } + + public bool ValidateAdditionalData(HttpContextBase context, string additionalData) + { + if (!additionalData.DetectIsJson()) + return false; //must be json + + AdditionalData json; + try + { + json = JsonConvert.DeserializeObject(additionalData); + } + catch + { + return false; //couldn't parse + } + + if (json.Stamp == default) return false; + + //if there was a wrapped provider, validate it, else validate the static value + var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; + if (!validateWrapped) + return false; + + var ufprtRequest = context.Request["ufprt"]?.ToString(); + + //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate + if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) + return true; + + //if one or the other is null then something is wrong + if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; + if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) + return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) + return false; + + //ensure they all match + return additionalDataParts.Count == requestParts.Count + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; + } + + internal class AdditionalData + { + public string Ufprt { get; set; } + public long Stamp { get; set; } + public string WrappedValue { get; set; } + } + + } +} diff --git a/src/Umbraco.Web/Services/DashboardService.cs b/src/Umbraco.Web/Services/DashboardService.cs index 71969df475..794c6fa671 100644 --- a/src/Umbraco.Web/Services/DashboardService.cs +++ b/src/Umbraco.Web/Services/DashboardService.cs @@ -104,8 +104,9 @@ namespace Umbraco.Web.Services } } - if (!hasAccess || denyRules.Length == 0) - return true; + // No need to check denyRules if there aren't any, just return current state + if (denyRules.Length == 0) + return hasAccess; // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will // be denied to see it no matter what diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8465a2f016..0a5fcafb8d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1273,4 +1273,1284 @@ + + + + + v4.7.2 + false + false + {651E1350-91B6-44B7-BD60-7207006D7003} + Library + Umbraco.Web + Umbraco.Web + ..\ + true + Off + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + latest + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\Umbraco.Web.xml + false + latest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.7.0.100 + + + + + + + + + + + + + + + + + + + + + 1.0.5 + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + Umbraco.Examine + {07FBC26B-2927-4A22-8D96-D644C667FECC} + + + + + + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Reference.map + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ASPXCodeBehind + + + + + + + + + + + + + + True + True + Strings.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + Code + + + True + True + Settings.settings + + + + + + + True + True + Reference.map + + + + + + Component + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ResXFileCodeGenerator + Strings.Designer.cs + + + + + + + + + + + + + + + + + + MSDiscoCodeGenerator + Reference.cs + Designer + + + Mvc\web.config + + + MSDiscoCodeGenerator + Reference.cs + + + + + Reference.map + + + Reference.map + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + Dynamic + Web References\org.umbraco.our\ + http://our.umbraco.org/umbraco/webservices/api/repository.asmx + + + + + Settings + umbraco_org_umbraco_our_Repository + + + Dynamic + Web References\org.umbraco.update\ + http://update.umbraco.org/checkforupgrade.asmx + + + + + Settings + umbraco_org_umbraco_update_CheckForUpgrade + + + + + + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index a886bf421b..c0306e0a81 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -171,7 +171,8 @@ namespace Umbraco.Web return GlobalSettings.DebugMode && request != null && (string.IsNullOrEmpty(request["umbdebugshowtrace"]) == false - || string.IsNullOrEmpty(request["umbdebug"]) == false); + || string.IsNullOrEmpty(request["umbdebug"]) == false + || string.IsNullOrEmpty(request.Cookies["UMB-DEBUG"]?.Value) == false); } } diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 6e7b05691e..a330f66c43 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -8,7 +8,10 @@ using Umbraco.Core.Dictionary; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; +using Umbraco.Web.Composing; +using Umbraco.Web.Mvc; using Umbraco.Web.Security; +using Constants = Umbraco.Core.Constants; namespace Umbraco.Web { @@ -228,7 +231,7 @@ namespace Umbraco.Web #endregion - + #region Member/Content/Media from Udi @@ -495,7 +498,7 @@ namespace Umbraco.Web /// The existing contents corresponding to the identifiers. /// If an identifier does not match an existing content, it will be missing in the returned value. public IEnumerable Content(IEnumerable ids) - { + { return ids.Select(id => ContentQuery.Content(id)).WhereNotNull(); } @@ -809,5 +812,37 @@ namespace Umbraco.Web } #endregion + internal static bool DecryptAndValidateEncryptedRouteString(string ufprt, out IDictionary parts) + { + string decryptedString; + try + { + decryptedString = ufprt.DecryptWithMachineKey(); + } + catch (FormatException) + { + Current.Logger.Warn(typeof(UmbracoHelper), "A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); + parts = null; + return false; + } + var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); + parts = new Dictionary(); + foreach (var key in parsedQueryString.AllKeys) + { + parts[key] = parsedQueryString[key]; + } + //validate all required keys exist + //the controller + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Controller)) + return false; + //the action + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Action)) + return false; + //the area + if (parts.All(x => x.Key != RenderRouteHandler.ReservedAdditionalKeys.Area)) + return false; + + return true; + } } }