From b94b94ea069ced4b36f143a682c84dfb1bae1df0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Feb 2019 09:37:22 +0100 Subject: [PATCH 001/201] Add serverside validation of assigned hostnames --- .../src/views/content/assigndomain.html | 7 +++++++ .../content/content.assigndomain.controller.js | 1 + src/Umbraco.Web/Editors/ContentController.cs | 14 ++++++++++++++ src/Umbraco.Web/Routing/DomainAndUri.cs | 9 ++------- src/Umbraco.Web/Routing/DomainHelper.cs | 10 ++++++++++ 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html index 89381e1be4..492c85c0dd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html @@ -16,6 +16,13 @@ +
+
+
{{vm.error.errorMsg}}
+
{{vm.error.data.Message}}
+
+
+
Domains
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js index 0f27f3046c..627519a207 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js @@ -131,6 +131,7 @@ } }, function (e) { + vm.error = e; vm.submitButtonState = "error"; }); } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 12047b628b..37be426d42 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -37,6 +37,7 @@ using Umbraco.Web.Editors.Filters; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence; using Umbraco.Core.Security; +using Umbraco.Web.Routing; namespace Umbraco.Web.Editors { @@ -1588,6 +1589,19 @@ namespace Umbraco.Web.Editors [HttpPost] public DomainSave PostSaveLanguageAndDomains(DomainSave model) { + foreach(var domain in model.Domains) + { + try + { + var uri = DomainHelper.ParseUriFromDomainName(domain.Name, Request.RequestUri); + } + catch (UriFormatException) + { + var response = Request.CreateValidationErrorResponse("One or more domains are not valid"); + throw new HttpResponseException(response); + } + } + var node = Services.ContentService.GetById(model.NodeId); if (node == null) diff --git a/src/Umbraco.Web/Routing/DomainAndUri.cs b/src/Umbraco.Web/Routing/DomainAndUri.cs index 7ba0d8ca9e..23115cad0d 100644 --- a/src/Umbraco.Web/Routing/DomainAndUri.cs +++ b/src/Umbraco.Web/Routing/DomainAndUri.cs @@ -22,16 +22,11 @@ namespace Umbraco.Web.Routing { try { - // turn "/en" into "http://whatever.com/en" so it becomes a parseable uri - var name = Name.StartsWith("/") && currentUri != null - ? currentUri.GetLeftPart(UriPartial.Authority) + Name - : Name; - var scheme = currentUri?.Scheme ?? Uri.UriSchemeHttp; - Uri = new Uri(UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(name, scheme))); + Uri = DomainHelper.ParseUriFromDomainName(Name, currentUri); } catch (UriFormatException) { - throw new ArgumentException($"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{Name.ToCSharpString()}\"." + throw new ArgumentException($"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{domain.Name.ToCSharpString()}\"." + " Hostname should be a valid uri.", nameof(domain)); } } diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 88f2b17814..d7f8348247 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -251,6 +251,16 @@ namespace Umbraco.Web.Routing .OrderByDescending(d => d.Uri.ToString()); } + internal static Uri ParseUriFromDomainName(string Name, Uri currentUri) + { + // turn "/en" into "http://whatever.com/en" so it becomes a parseable uri + var name = Name.StartsWith("/") && currentUri != null + ? currentUri.GetLeftPart(UriPartial.Authority) + Name + : Name; + var scheme = currentUri?.Scheme ?? Uri.UriSchemeHttp; + return new Uri(UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(name, scheme))); + } + #endregion #region Utilities From 214457fc0437864a2b71aafed1719bf2b4da581c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Feb 2019 09:50:13 +0100 Subject: [PATCH 002/201] Add translation --- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 7969655264..a454597054 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -86,7 +86,7 @@ Tilføj nyt domæne fjern Ugyldig node. - Ugyldigt domæne-format. + Et eller flere domæner har et ugyldigt format. Domæne er allerede blevet tildelt. Sprog Domæne diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d2499a6df3..fd0b8b2545 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -87,7 +87,7 @@ Add new Domain remove Invalid node. - Invalid domain format. + One or more domains have an invalid format. Domain has already been assigned. Language Domain 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 fdd118dff9..7be291d828 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -87,7 +87,7 @@ Add new Domain remove Invalid node. - Invalid domain format. + One or more domains have an invalid format. Domain has already been assigned. Language Domain diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 37be426d42..e8ad9a61f7 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1596,8 +1596,8 @@ namespace Umbraco.Web.Editors var uri = DomainHelper.ParseUriFromDomainName(domain.Name, Request.RequestUri); } catch (UriFormatException) - { - var response = Request.CreateValidationErrorResponse("One or more domains are not valid"); + { + var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain/invalidDomain")); throw new HttpResponseException(response); } } From 7ca6739cee9fa1bd4b706e18aa052604a451a160 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Feb 2019 15:01:47 +0100 Subject: [PATCH 003/201] Clear error before (re)submitting --- .../src/views/content/content.assigndomain.controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js index 627519a207..78cb0fe16c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js @@ -89,6 +89,7 @@ function save() { + vm.error = null; vm.submitButtonState = "busy"; if (vm.domainForm.$valid) { From 32a622a6ea5162e7fb43b653888298b041476455 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Feb 2019 15:10:49 +0100 Subject: [PATCH 004/201] Add missing documentation + a bit of renaming --- src/Umbraco.Web/Routing/DomainAndUri.cs | 2 +- src/Umbraco.Web/Routing/DomainHelper.cs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Routing/DomainAndUri.cs b/src/Umbraco.Web/Routing/DomainAndUri.cs index 23115cad0d..1151055621 100644 --- a/src/Umbraco.Web/Routing/DomainAndUri.cs +++ b/src/Umbraco.Web/Routing/DomainAndUri.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Routing } catch (UriFormatException) { - throw new ArgumentException($"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{domain.Name.ToCSharpString()}\"." + throw new ArgumentException($"Failed to parse invalid domain: node id={domain.ContentId}, hostname=\"{Name.ToCSharpString()}\"." + " Hostname should be a valid uri.", nameof(domain)); } } diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index d7f8348247..95d97653a0 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -251,12 +251,18 @@ namespace Umbraco.Web.Routing .OrderByDescending(d => d.Uri.ToString()); } - internal static Uri ParseUriFromDomainName(string Name, Uri currentUri) + /// + /// Parses a domain name into a URI. + /// + /// The domain name to parse + /// The currently requested URI. If the domain name is relative, the authority of URI will be used. + /// The domain name as a URI + internal static Uri ParseUriFromDomainName(string domainName, Uri currentUri) { // turn "/en" into "http://whatever.com/en" so it becomes a parseable uri - var name = Name.StartsWith("/") && currentUri != null - ? currentUri.GetLeftPart(UriPartial.Authority) + Name - : Name; + var name = domainName.StartsWith("/") && currentUri != null + ? currentUri.GetLeftPart(UriPartial.Authority) + domainName + : domainName; var scheme = currentUri?.Scheme ?? Uri.UriSchemeHttp; return new Uri(UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(name, scheme))); } From b9d1df0844eca118a8de51c070b77b471b84b24e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Feb 2019 15:28:04 +0100 Subject: [PATCH 005/201] Return folder creation to non-active state in media picker --- .../infiniteeditors/mediapicker/mediapicker.controller.js | 8 ++++---- .../common/infiniteeditors/mediapicker/mediapicker.html | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 2d6a2be471..76cd816f14 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -117,7 +117,7 @@ angular.module("umbraco") $scope.submitFolder = function() { if ($scope.model.newFolderName) { - $scope.creatingFolder = true; + $scope.model.creatingFolder = true; mediaResource .addFolder($scope.model.newFolderName, $scope.currentFolder.id) .then(function(data) { @@ -126,13 +126,13 @@ angular.module("umbraco") cacheKey: "__media", //this is the main media tree cache key childrenOf: data.parentId //clear the children of the parent }); - $scope.creatingFolder = false; + $scope.model.creatingFolder = false; $scope.gotoFolder(data); - $scope.showFolderInput = false; + $scope.model.showFolderInput = false; $scope.model.newFolderName = ""; }); } else { - $scope.showFolderInput = false; + $scope.model.showFolderInput = false; } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index da88b9321e..6eee269cee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -64,15 +64,15 @@
  • - + - +
  • -
    +
    Date: Wed, 27 Feb 2019 19:13:51 +0100 Subject: [PATCH 006/201] Handle mixed variance when creating content templates from content --- src/Umbraco.Core/Services/Implement/ContentService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 5bdc0959da..3406eece3d 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2875,7 +2875,8 @@ namespace Umbraco.Core.Services.Implement { foreach (var property in blueprint.Properties) { - content.SetValue(property.Alias, property.GetValue(culture), culture); + var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null; + content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture); } content.Name = blueprint.Name; From 28d4f1cc44dc9a13ccfe7242a66a5f9e5785edfc Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Sun, 3 Mar 2019 20:18:41 +0100 Subject: [PATCH 007/201] Fix issues where table cells collapse when sorting doctypes in Nested Content config --- .../nestedcontent/nestedcontent.controller.js | 30 ++++++++++++++++++- .../nestedcontent.doctypepicker.html | 3 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 0d24f9d1cc..b79591c852 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -23,7 +23,35 @@ $scope.sortableOptions = { axis: 'y', cursor: "move", - handle: ".icon-navigation" + handle: ".handle", + placeholder: 'sortable-placeholder', + forcePlaceholderSize: true, + helper: function (e, ui) { + // When sorting table rows, the cells collapse. This helper fixes that: https://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ + ui.children().each(function () { + $(this).width($(this).width()); + }); + return ui; + }, + start: function (e, ui) { + + var cellHeight = ui.item.height(); + + // Build a placeholder cell that spans all the cells in the row: https://stackoverflow.com/questions/25845310/jquery-ui-sortable-and-table-cell-size + var cellCount = 0; + $('td, th', ui.helper).each(function () { + // For each td or th try and get it's colspan attribute, and add that or 1 to the total + var colspan = 1; + var colspanAttr = $(this).attr('colspan'); + if (colspanAttr > 1) { + colspan = colspanAttr; + } + cellCount += colspan; + }); + + // Add the placeholder UI - note that this is the item's content, so td rather than tr - and set height of tr + ui.placeholder.html('').height(cellHeight); + } }; $scope.docTypeTabs = {}; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html index e0a16d8687..48e75cbeda 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -18,7 +18,8 @@ - + + Pixels + \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 5f3022b2a9..3d1e9d321c 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,1219 +1,1227 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Publishing; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Binders; -using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.Events; -using Constants = Umbraco.Core.Constants; -using umbraco.cms.businesslogic; -using System.Collections; -using umbraco; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for editing content - /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [ContentControllerConfiguration] - public class ContentController : ContentControllerBase - { - /// - /// Constructor - /// - public ContentController() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Configures this controller with a custom action selector - /// - private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) - )); - } - } - - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); - } - - /// - /// Updates the permissions for a content item for a particular user group - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to update - /// - [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] - public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) - { - if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be alowed to set granular permissions? - - var content = Services.ContentService.GetById(saveModel.ContentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //current permissions explicitly assigned to this content item - var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) - .ToDictionary(x => x.UserGroupId, x => x); - - var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); - - //loop through each user group - foreach (var userGroup in allUserGroups) - { - //check if there's a permission set posted up for this user group - IEnumerable groupPermissions; - if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) - { - //create a string collection of the assigned letters - var groupPermissionCodes = groupPermissions.ToArray(); - - //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions - //for this group/node which will go back to the defaults - if (groupPermissionCodes.Length == 0) - { - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored - else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) - { - //only remove them if they are actually currently assigned - if (contentPermissions.ContainsKey(userGroup.Id)) - { - //remove these permissions from this node for this group since the ones being assigned are the same as the defaults - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - } - //if they are different we need to update, otherwise there's nothing to update - else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) - { - - Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); - } - } - } - - return GetDetailedPermissions(content, allUserGroups); - } - - /// - /// Returns the user group permissions for user groups assigned to this node - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to view - /// - [EnsureUserPermissionForContent("contentId", 'R')] - public IEnumerable GetDetailedPermissions(int contentId) - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be able to see detailed permissions? - - var allUserGroups = Services.UserService.GetAllUserGroups(); - - return GetDetailedPermissions(content, allUserGroups); - } - - private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) - { - //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. - //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. - - var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); - - var defaultPermissionsAsDictionary = defaultPermissionsByGroup - .ToDictionary(x => Convert.ToInt32(x.Id), x => x); - - //get the actual assigned permissions - var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); - - //iterate over assigned and update the defaults with the real values - foreach (var assignedGroupPermission in assignedPermissionsByGroup) - { - var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; - - //clone the default permissions model to the assigned ones - defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - } - - return defaultPermissionsByGroup; - } - - /// - /// Returns an item to be used to display the recycle bin for content - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinContent, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinContent - }; - - TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); - - return display; - } - - public ContentItemDisplay GetBlueprintById(int id) - { - var foundContent = Services.ContentService.GetBlueprintById(id); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - SetupBlueprint(content, foundContent); - - return content; - } - - private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) - { - content.AllowPreview = false; - - //set a custom path since the tree that renders this has the content type id as the parent - content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); - - content.AllowedActions = new[] { "A" }; - content.IsBlueprint = true; - - var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; - var propsTab = content.Tabs.Last(); - propsTab.Properties = propsTab.Properties - .Where(p => excludeProps.Contains(p.Alias) == false); - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Guid id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetById(guidUdi.Guid); - } - - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetWithTreeDefinition(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - /// If this is a container type, we'll remove the umbContainerView tab for a new item since - /// it cannot actually list children if it doesn't exist yet. - /// - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); - // translate the content type name if applicable - mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); - // if your user type doesn't have access to the Settings section it would not get this property mapped - if(mapped.DocumentType != null) - mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(int blueprintId, int parentId) - { - var blueprint = Services.ContentService.GetBlueprintById(blueprintId); - if (blueprint == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - blueprint.Id = 0; - blueprint.Name = string.Empty; - blueprint.ParentId = parentId; - - var mapped = Mapper.Map(blueprint); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(int id) - { - var url = Umbraco.NiceUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Guid id) - { - var url = Umbraco.UrlProvider.GetUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetNiceUrl(guidUdi.Guid); - } - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - string includeProperties, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - long totalChildren; - IContent[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.ContentService - .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemField, filter).ToArray(); - } - else - { - children = Services.ContentService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children.Select(content => - Mapper.Map>(content, - opts => - { - // if there's a list of property aliases to map - we will make sure to store this in the mapping context. - if (String.IsNullOrWhiteSpace(includeProperties) == false) - { - opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); - } - })); - - return pagedResult; - } - - [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] - public bool GetHasPermission(string permissionToCheck, int nodeId) - { - return HasPermission(permissionToCheck, nodeId); - } - - /// - /// Returns permissions for all nodes passed in for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - [HttpPost] - public Dictionary GetPermissions(int[] nodeIds) - { - var permissions = Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds); - - var permissionsDictionary = new Dictionary(); - foreach (var nodeId in nodeIds) - { - var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); - permissionsDictionary.Add(nodeId, aggregatePerms); - } - - return permissionsDictionary; - } - - /// - /// Checks a nodes permission for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - /// - [HttpGet] - public bool HasPermission(string permissionToCheck, int nodeId) - { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); - if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) - { - return true; - } - - return false; - } - - /// - /// Creates a blueprint from a content item - /// - /// The content id to copy - /// The name of the blueprint - /// - [HttpPost] - public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); - - var content = Services.ContentService.GetById(contentId); - if (content == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - EnsureUniqueName(name, content, "name"); - - var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); - - Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); - - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddSuccessNotification( - Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) - ); - - return notificationModel; - } - - private void EnsureUniqueName(string name, IContent content, string modelName) - { - var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); - if (existing.Any(x => x.Name == name && x.Id != content.Id)) - { - ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); - } - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - public ContentItemDisplay PostSaveBlueprint( - [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, - content => - { - EnsureUniqueName(content.Name, content, "Name"); - - Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants - return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); - }); - SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); - - return contentItemDisplay; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { - return PostSaveInternal(contentItem, - content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); - } - - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) - { - //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. - //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all - //uploaded files to being *only* the actual file name (as it should be). - if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) - { - foreach (var file in contentItem.UploadedFiles) - { - file.FileName = Path.GetFileName(file.FileName); - } - } - - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw a validation message - var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - - } - - //if the model state is not valid we cannot publish so change it to save - switch (contentItem.Action) - { - case ContentSaveAction.Publish: - contentItem.Action = ContentSaveAction.Save; - break; - case ContentSaveAction.PublishNew: - contentItem.Action = ContentSaveAction.SaveNew; - break; - } - } - - //initialize this to successful - var publishStatus = Attempt.Succeed(); - var wasCancelled = false; - - if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) - { - //save the item - var saveResult = saveMethod(contentItem.PersistedContent); - - wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; - } - else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) - { - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = sendResult == false; - } - else - { - //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; - } - - //return the updated model - var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - contentItem.ReleaseDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new [] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentSavedText") - ); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.SendPublish: - case ContentSaveAction.SendPublishNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); - break; - } - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (wasCancelled && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - - display.PersistedContent = contentItem.PersistedContent; - - return display; - } - - /// - /// Publishes a document with a given ID - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Publish access to this node. - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); - return Request.CreateValidationErrorResponse(notificationModel); - } - - //return ok - return Request.CreateResponse(HttpStatusCode.OK); - - } - - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteBlueprint(int id) - { - var found = Services.ContentService.GetBlueprintById(id); - - if (found == null) - { - return HandleContentNotFound(id, false); - } - - Services.ContentService.DeleteBlueprint(found); - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Delete access to this node. - /// - [EnsureUserPermissionForContent("id", 'D')] - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundContent.IsInRecycleBin() == false) - { - var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Empties the recycle bin - /// - /// - /// - /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin - /// - [HttpDelete] - [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] - public HttpResponseMessage EmptyRecycleBin() - { - Services.ContentService.EmptyRecycleBin(); - - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); - } - - /// - /// Change the sort order for content - /// - /// - /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - try - { - var contentService = Services.ContentService; - - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) - { - LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update content sort order", ex); - throw; - } - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Copies a content item and places the copy as a child of a given parent Id - /// - /// - /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) - { - var toCopy = ValidateMoveOrCopy(copy); - - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Unpublishes a node with a given Id and returns the unpublished entity - /// - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostUnPublish(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - if (unpublishResult == false) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); - return content; - } - } - - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - UpdateName(contentItem); - - //TODO: We need to support 'send to publish' - - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } - - base.MapPropertyValues(contentItem); - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IContent ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var contentService = Services.ContentService; - var toMove = contentService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); - } - } - else - { - var parent = contentService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); - } - } - - return toMove; - } - - private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) - { - switch (status.StatusType) - { - case PublishStatusType.Success: - case PublishStatusType.SuccessAlreadyPublished: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - expireDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentPublishedText") - ); - break; - case PublishStatusType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishStatusType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - }).Trim()); - break; - case PublishStatusType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! - break; - case PublishStatusType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - default: - throw new IndexOutOfRangeException(); - } - } - - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - /// Specifies the already resolved content item to check against - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - IEntityService entityService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) - { - if (storage == null) throw new ArgumentNullException("storage"); - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (contentService == null) throw new ArgumentNullException("contentService"); - if (entityService == null) throw new ArgumentNullException("entityService"); - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - contentItem = contentService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IContent).ToString()] = contentItem; - } - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasContentRootAccess(entityService) - : (nodeId == Constants.System.RecycleBinContent) - ? user.HasContentBinAccess(entityService) - : user.HasPathAccess(contentItem, entityService); - - if (hasPathAccess == false) - { - return false; - } - - if (permissionsToCheck == null || permissionsToCheck.Length == 0) - { - return true; - } - - //get the implicit/inherited permissions for the user for this path, - //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var path = contentItem != null ? contentItem.Path : nodeId.ToString(); - var permission = userService.GetPermissionsForPath(user, path); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - [EnsureUserPermissionForContent("contentId", 'F')] - public IEnumerable GetNotificationOptions(int contentId) - { - var notifications = new List(); - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var actionList = ActionsResolver.Current.Actions; - foreach (var a in actionList) - { - if (a.ShowInNotifier) - { - NotifySetting n = new NotifySetting - { - Name = ui.Text("actions", a.Alias), - Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), - NotifyCode = a.Letter.ToString() - }; - notifications.Add(n); - } - } - return notifications; - } - - public void PostNotificationOptions(int contentId, string notifyOptions = "") - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var node = new CMSNode(contentId); - - global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Binders; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Events; +using Constants = Umbraco.Core.Constants; +using umbraco.cms.businesslogic; +using System.Collections; +using umbraco; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for editing content + /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [ContentControllerConfiguration] + public class ContentController : ContentControllerBase + { + /// + /// Constructor + /// + public ContentController() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + public ContentController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Configures this controller with a custom action selector + /// + private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); + } + + /// + /// Updates the permissions for a content item for a particular user group + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to update + /// + [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] + public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) + { + if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be alowed to set granular permissions? + + var content = Services.ContentService.GetById(saveModel.ContentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //current permissions explicitly assigned to this content item + var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) + .ToDictionary(x => x.UserGroupId, x => x); + + var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); + + //loop through each user group + foreach (var userGroup in allUserGroups) + { + //check if there's a permission set posted up for this user group + IEnumerable groupPermissions; + if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) + { + //create a string collection of the assigned letters + var groupPermissionCodes = groupPermissions.ToArray(); + + //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions + //for this group/node which will go back to the defaults + if (groupPermissionCodes.Length == 0) + { + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored + else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) + { + //only remove them if they are actually currently assigned + if (contentPermissions.ContainsKey(userGroup.Id)) + { + //remove these permissions from this node for this group since the ones being assigned are the same as the defaults + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + } + //if they are different we need to update, otherwise there's nothing to update + else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) + { + + Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); + } + } + } + + return GetDetailedPermissions(content, allUserGroups); + } + + /// + /// Returns the user group permissions for user groups assigned to this node + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to view + /// + [EnsureUserPermissionForContent("contentId", 'R')] + public IEnumerable GetDetailedPermissions(int contentId) + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be able to see detailed permissions? + + var allUserGroups = Services.UserService.GetAllUserGroups(); + + return GetDetailedPermissions(content, allUserGroups); + } + + private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) + { + //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. + //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. + + var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); + + var defaultPermissionsAsDictionary = defaultPermissionsByGroup + .ToDictionary(x => Convert.ToInt32(x.Id), x => x); + + //get the actual assigned permissions + var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); + + //iterate over assigned and update the defaults with the real values + foreach (var assignedGroupPermission in assignedPermissionsByGroup) + { + var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; + + //clone the default permissions model to the assigned ones + defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); + + //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + //and we'll re-check it if it's one of the explicitly assigned ones + foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); + } + + } + + return defaultPermissionsByGroup; + } + + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + + public ContentItemDisplay GetBlueprintById(int id) + { + var foundContent = Services.ContentService.GetBlueprintById(id); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + SetupBlueprint(content, foundContent); + + return content; + } + + private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) + { + content.AllowPreview = false; + + //set a custom path since the tree that renders this has the content type id as the parent + content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); + + content.AllowedActions = new[] { "A" }; + content.IsBlueprint = true; + + var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; + var propsTab = content.Tabs.Last(); + propsTab.Properties = propsTab.Properties + .Where(p => excludeProps.Contains(p.Alias) == false); + } + + /// + /// Gets the content json for the content id + /// + /// + /// If set to true, user and group start node permissions will be ignored. + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id, [FromUri]bool ignoreUserStartNodes = false) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetWithTreeDefinition(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); + // translate the content type name if applicable + mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); + // if your user type doesn't have access to the Settings section it would not get this property mapped + if(mapped.DocumentType != null) + mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(int blueprintId, int parentId) + { + var blueprint = Services.ContentService.GetBlueprintById(blueprintId); + if (blueprint == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + blueprint.Id = 0; + blueprint.Name = string.Empty; + blueprint.ParentId = parentId; + + var mapped = Mapper.Map(blueprint); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(int id) + { + var url = Umbraco.NiceUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Guid id) + { + var url = Umbraco.UrlProvider.GetUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetNiceUrl(guidUdi.Guid); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + string includeProperties, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + long totalChildren; + IContent[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + } + else + { + children = Services.ContentService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children.Select(content => + Mapper.Map>(content, + opts => + { + // if there's a list of property aliases to map - we will make sure to store this in the mapping context. + if (String.IsNullOrWhiteSpace(includeProperties) == false) + { + opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); + } + })); + + return pagedResult; + } + + [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] + public bool GetHasPermission(string permissionToCheck, int nodeId) + { + return HasPermission(permissionToCheck, nodeId); + } + + /// + /// Returns permissions for all nodes passed in for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + var permissions = Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds); + + var permissionsDictionary = new Dictionary(); + foreach (var nodeId in nodeIds) + { + var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); + permissionsDictionary.Add(nodeId, aggregatePerms); + } + + return permissionsDictionary; + } + + /// + /// Checks a nodes permission for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + /// + [HttpGet] + public bool HasPermission(string permissionToCheck, int nodeId) + { + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); + if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + { + return true; + } + + return false; + } + + /// + /// Creates a blueprint from a content item + /// + /// The content id to copy + /// The name of the blueprint + /// + [HttpPost] + public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); + + var content = Services.ContentService.GetById(contentId); + if (content == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + EnsureUniqueName(name, content, "name"); + + var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); + + Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); + + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddSuccessNotification( + Services.TextService.Localize("blueprints/createdBlueprintHeading"), + Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + ); + + return notificationModel; + } + + private void EnsureUniqueName(string name, IContent content, string modelName) + { + var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); + if (existing.Any(x => x.Name == name && x.Id != content.Id)) + { + ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSaveBlueprint( + [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, + content => + { + EnsureUniqueName(content.Name, content, "Name"); + + Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants + return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); + }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostSave( + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { + return PostSaveInternal(contentItem, + content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + } + + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) + { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a validation message + var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + + } + + //if the model state is not valid we cannot publish so change it to save + switch (contentItem.Action) + { + case ContentSaveAction.Publish: + contentItem.Action = ContentSaveAction.Save; + break; + case ContentSaveAction.PublishNew: + contentItem.Action = ContentSaveAction.SaveNew; + break; + } + } + + //initialize this to successful + var publishStatus = Attempt.Succeed(); + var wasCancelled = false; + + if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) + { + //save the item + var saveResult = saveMethod(contentItem.PersistedContent); + + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; + } + else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) + { + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; + } + else + { + //publish the item and check if it worked, if not we will show a diff msg below + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; + } + + //return the updated model + var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + contentItem.ReleaseDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new [] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentSavedText") + ); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.SendPublish: + case ContentSaveAction.SendPublishNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); + break; + } + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (wasCancelled && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + + display.PersistedContent = contentItem.PersistedContent; + + return display; + } + + /// + /// Publishes a document with a given ID + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Publish access to this node. + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public HttpResponseMessage PostPublishById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); + return Request.CreateValidationErrorResponse(notificationModel); + } + + //return ok + return Request.CreateResponse(HttpStatusCode.OK); + + } + + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteBlueprint(int id) + { + var found = Services.ContentService.GetBlueprintById(id); + + if (found == null) + { + return HandleContentNotFound(id, false); + } + + Services.ContentService.DeleteBlueprint(found); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundContent.IsInRecycleBin() == false) + { + var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Empties the recycle bin + /// + /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// + [HttpDelete] + [HttpPost] + [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] + public HttpResponseMessage EmptyRecycleBin() + { + Services.ContentService.EmptyRecycleBin(); + + return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + } + + /// + /// Change the sort order for content + /// + /// + /// + [EnsureUserPermissionForContent("sorted.ParentId", 'S')] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + try + { + var contentService = Services.ContentService; + + // Save content with new sort order and update content xml in db accordingly + if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) + { + LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update content sort order", ex); + throw; + } + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("move.ParentId", 'M')] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Copies a content item and places the copy as a child of a given parent Id + /// + /// + /// + [EnsureUserPermissionForContent("copy.ParentId", 'C')] + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + var toCopy = ValidateMoveOrCopy(copy); + + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Unpublishes a node with a given Id and returns the unpublished entity + /// + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostUnPublish(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + HandleContentNotFound(id); + + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + if (unpublishResult == false) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; + } + } + + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + UpdateName(contentItem); + + //TODO: We need to support 'send to publish' + + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } + + base.MapPropertyValues(contentItem); + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IContent ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var contentService = Services.ContentService; + var toMove = contentService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + } + } + else + { + var parent = contentService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + } + } + + return toMove; + } + + private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) + { + switch (status.StatusType) + { + case PublishStatusType.Success: + case PublishStatusType.SuccessAlreadyPublished: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + expireDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentPublishedText") + ); + break; + case PublishStatusType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishStatusType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; + case PublishStatusType.FailedIsTrashed: + //TODO: We should add proper error messaging for this! + break; + case PublishStatusType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + default: + throw new IndexOutOfRangeException(); + } + } + + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// Specifies the already resolved content item to check against + /// If set to true, user and group start node permissions will be ignored. + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null, + bool ignoreUserStartNodes = false) + { + if (storage == null) throw new ArgumentNullException("storage"); + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + contentItem = contentService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IContent).ToString()] = contentItem; + } + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + if(ignoreUserStartNodes) + { + return true; + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? user.HasContentRootAccess(entityService) + : (nodeId == Constants.System.RecycleBinContent) + ? user.HasContentBinAccess(entityService) + : user.HasPathAccess(contentItem, entityService); + + if (hasPathAccess == false) + { + return false; + } + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + { + return true; + } + + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var path = contentItem != null ? contentItem.Path : nodeId.ToString(); + var permission = userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + [EnsureUserPermissionForContent("contentId", 'F')] + public IEnumerable GetNotificationOptions(int contentId) + { + var notifications = new List(); + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var actionList = ActionsResolver.Current.Actions; + foreach (var a in actionList) + { + if (a.ShowInNotifier) + { + NotifySetting n = new NotifySetting + { + Name = ui.Text("actions", a.Alias), + Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), + NotifyCode = a.Letter.ToString() + }; + notifications.Add(n); + } + } + return notifications; + } + + public void PostNotificationOptions(int contentId, string notifyOptions = "") + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var node = new CMSNode(contentId); + + global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); + } + } +} diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 8bde435ef6..045b4e6f0d 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -82,9 +82,10 @@ namespace Umbraco.Web.Editors /// /// A starting point for the search, generally a node id, but for members this is a member type alias /// + /// If set to true, user and group start node permissions will be ignored. /// [HttpGet] - public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null) + public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null, bool? ignoreUserStartNodes = false) { //TODO: Should we restrict search results based on what app the user has access to? // - Theoretically you shouldn't be able to see member data if you don't have access to members right? @@ -92,7 +93,7 @@ namespace Umbraco.Web.Editors if (string.IsNullOrEmpty(query)) return Enumerable.Empty(); - return ExamineSearch(query, type, searchFrom); + return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes != null && ignoreUserStartNodes.Value); } /// @@ -534,7 +535,8 @@ namespace Umbraco.Web.Editors int pageSize, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -562,7 +564,7 @@ namespace Umbraco.Web.Editors break; } - entities = aids == null || aids.Contains(Constants.System.Root) + entities = aids == null || aids.Contains(Constants.System.Root) || ignoreUserStartNodes ? Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed: false) : Services.EntityService.GetPagedDescendants(aids, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); } @@ -598,9 +600,9 @@ namespace Umbraco.Web.Editors } } - public IEnumerable GetAncestors(int id, UmbracoEntityTypes type) + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, bool ignoreUserStartNodes = false) { - return GetResultForAncestors(id, type); + return GetResultForAncestors(id, type, ignoreUserStartNodes); } public IEnumerable GetAll(UmbracoEntityTypes type, string postFilter, [FromUri]IDictionary postFilterParams) @@ -614,11 +616,12 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// - private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) + private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null, bool ignoreUserStartNodes = false) { long total; - return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom); + return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom, ignoreUserStartNodes); } @@ -645,7 +648,7 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType) + private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, bool ignoreUserStartNodes = false) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) @@ -654,35 +657,38 @@ namespace Umbraco.Web.Editors var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); - int[] aids = null; - switch (entityType) + if (ignoreUserStartNodes == false) { - case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - break; - case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - break; - } - - if (aids != null) - { - var lids = new List(); - var ok = false; - foreach (var i in ids) + int[] aids = null; + switch (entityType) { - if (ok) - { - lids.Add(i); - continue; - } - if (aids.Contains(i)) - { - lids.Add(i); - ok = true; - } + case UmbracoEntityTypes.Document: + aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + break; + case UmbracoEntityTypes.Media: + aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + break; + } + + if (aids != null) + { + var lids = new List(); + var ok = false; + foreach (var i in ids) + { + if (ok) + { + lids.Add(i); + continue; + } + if (aids.Contains(i)) + { + lids.Add(i); + ok = true; + } + } + ids = lids.ToArray(); } - ids = lids.ToArray(); } return ids.Length == 0 diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index e3f2365565..97e24ee6b8 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -264,11 +264,12 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { //if a request is made for the root node data but the user's start node is not the default, then // we need to return their start nodes - if (id == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + if (id == Constants.System.Root && UserStartNodes.Length > 0 && (UserStartNodes.Contains(Constants.System.Root) == false && ignoreUserStartNodes == false)) { if (pageNumber > 0) return new PagedResult>(0, 0, 0); @@ -321,6 +322,7 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Guid id, @@ -329,12 +331,13 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { var entity = Services.EntityService.GetByKey(id); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter, ignoreUserStartNodes); } throw new HttpResponseException(HttpStatusCode.NotFound); } @@ -349,6 +352,7 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Udi id, @@ -357,7 +361,8 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -365,7 +370,7 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.GetByKey(guidUdi.Guid); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter, ignoreUserStartNodes); } } @@ -381,7 +386,8 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, - string filter = "") + string filter = "", + bool ignoreUserStartNodes = false) { foreach (var type in new[] { typeof(int), typeof(Guid) }) { diff --git a/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs index 5b99264113..0039385e5f 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs @@ -15,10 +15,11 @@ namespace Umbraco.Web.PropertyEditors { InternalPreValues = new Dictionary { - {"startNodeId", "-1"}, + {"startNodeId", "-1"}, {"showOpenButton", "0"}, {"showEditButton", "0"}, {"showPathOnHover", "0"}, + {"ignoreUserStartNodes", "0"}, {"idType", "udi"} }; } @@ -39,7 +40,7 @@ namespace Umbraco.Web.PropertyEditors { public ContentPickerPreValueEditor() { - //create the fields + //create the fields Fields.Add(new PreValueField() { Key = "showOpenButton", @@ -48,6 +49,13 @@ namespace Umbraco.Web.PropertyEditors Description = "Opens the node in a dialog" }); Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Key = "startNodeId", View = "treepicker", @@ -60,4 +68,4 @@ namespace Umbraco.Web.PropertyEditors } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index af30b4ceeb..4a8803a099 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -136,6 +136,9 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] public string Rte { get; set; } + + [PreValueField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } #region Application event handler, used to bind to events on startup diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs index 2d9ee68b3b..6aa7ab4a54 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs @@ -57,6 +57,13 @@ namespace Umbraco.Web.PropertyEditors Description = "Do not allow folders to be picked." }); Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Key = "startNodeId", View = "mediapicker", @@ -69,4 +76,4 @@ namespace Umbraco.Web.PropertyEditors } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs index f43ecd48be..f57a9951b6 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.PropertyEditors {"showOpenButton", "0"}, {"showEditButton", "0"}, {"showPathOnHover", "0"}, + {"ignoreUserStartNodes", "0"}, {"idType", "udi"} }; } @@ -38,6 +39,13 @@ namespace Umbraco.Web.PropertyEditors { //create the fields Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Key = "startNode", View = "treesource", @@ -118,4 +126,4 @@ namespace Umbraco.Web.PropertyEditors } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index a058a16b8a..f3ae317efa 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -32,6 +32,13 @@ namespace Umbraco.Web.PropertyEditors { public MultiUrlPickerPreValueEditor() { + Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); Fields.Add(new PreValueField { Key = "minNumber", diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs index 541dccaa4e..a96c0724ff 100644 --- a/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.PropertyEditors { InternalPreValues = new Dictionary { + {"ignoreUserStartNodes", "0"}, {"idType", "udi"} }; } @@ -32,8 +33,11 @@ namespace Umbraco.Web.PropertyEditors internal class RelatedLinksPreValueEditor : PreValueEditor { + [PreValueField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } + [PreValueField("max", "Maximum number of links", "number", Description = "Enter the maximum amount of links to be added, enter 0 for unlimited")] - public int Maximum { get; set; } + public int Maximum { get; set; } } } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs index cd08a8ad45..69445bc304 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs @@ -23,6 +23,14 @@ namespace Umbraco.Web.PropertyEditors Key = "editor" }); + Fields.Add(new PreValueField() + { + Key = "ignoreUserStartNodes", + View = "boolean", + Name = "Ignore user start nodes", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to." + }); + Fields.Add(new PreValueField() { Name = "Hide Label", diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 38347de9ed..9714c6b946 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -27,13 +27,14 @@ namespace Umbraco.Web.Search /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// public IEnumerable ExamineSearch( UmbracoHelper umbracoHelper, string query, UmbracoEntityTypes entityType, int pageSize, - long pageIndex, out long totalFound, string searchFrom = null) + long pageIndex, out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) { var sb = new StringBuilder(); @@ -61,12 +62,12 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; var allMediaStartNodes = umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(appContext.Services.EntityService); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, appContext.Services.EntityService); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, appContext.Services.EntityService); break; case UmbracoEntityTypes.Document: type = "content"; var allContentStartNodes = umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(appContext.Services.EntityService); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, appContext.Services.EntityService); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, appContext.Services.EntityService); break; default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); @@ -203,7 +204,7 @@ namespace Umbraco.Web.Search } } - private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, IEntityService entityService) + private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, bool ignoreUserStartNodes, IEntityService entityService) { if (sb == null) throw new ArgumentNullException("sb"); if (entityService == null) throw new ArgumentNullException("entityService"); @@ -228,7 +229,7 @@ namespace Umbraco.Web.Search // make sure we don't find anything sb.Append("+__Path:none "); } - else if (startNodeIds.Contains(-1) == false) // -1 = no restriction + else if (startNodeIds.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction { var entityPaths = entityService.GetAllPaths(objectType, startNodeIds); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index af38e21546..2624c89b56 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -69,7 +69,7 @@ namespace Umbraco.Web.Trees { var node = base.CreateRootNode(queryStrings); - if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false) + if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false && IgnoreUserStartNodes(queryStrings) == false) { node.AdditionalData["noAccess"] = true; } @@ -91,7 +91,7 @@ namespace Umbraco.Web.Trees { bool hasPathAccess; var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out hasPathAccess); - if (entityIsAncestorOfStartNodes == false) + if (IgnoreUserStartNodes(queryStrings) == false && entityIsAncestorOfStartNodes == false) return null; var treeNode = GetSingleTreeNode(e, parentId, queryStrings); @@ -101,7 +101,7 @@ namespace Umbraco.Web.Trees //the node so we need to return null; return null; } - if (hasPathAccess == false) + if (IgnoreUserStartNodes(queryStrings) == false && hasPathAccess == false) { treeNode.AdditionalData["noAccess"] = true; } @@ -141,7 +141,7 @@ namespace Umbraco.Web.Trees // ensure that the user has access to that node, otherwise return the empty tree nodes collection // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access - if (HasPathAccess(id, queryStrings) == false) + if (IgnoreUserStartNodes(queryStrings) == false && HasPathAccess(id, queryStrings) == false) { LogHelper.Warn("User " + Security.CurrentUser.Username + " does not have access to node with id " + id); return nodes; @@ -158,7 +158,7 @@ namespace Umbraco.Web.Trees // get child entities - if id is root, but user's start nodes do not contain the // root node, this returns the start nodes instead of root's children - var entities = GetChildEntities(id).ToList(); + var entities = GetChildEntities(id, queryStrings).ToList(); nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); // if the user does not have access to the root node, what we have is the start nodes, @@ -190,7 +190,7 @@ namespace Umbraco.Web.Trees protected abstract UmbracoObjectTypes UmbracoObjectType { get; } - protected IEnumerable GetChildEntities(string id) + protected IEnumerable GetChildEntities(string id, FormDataCollection queryStrings) { // try to parse id as an integer else use GetEntityFromId // which will grok Guids, Udis, etc and let use obtain the id diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 98eb2ec8bc..2ea41ff128 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -349,6 +349,16 @@ namespace Umbraco.Web.Trees return queryStrings.GetValue(TreeQueryStringParameters.IsDialog); } + /// + /// If the request should allows a user to choose nodes that they normally don't have access to + /// + /// + /// + protected bool IgnoreUserStartNodes(FormDataCollection queryStrings) + { + return queryStrings.GetValue(TreeQueryStringParameters.IgnoreUserStartNodes); + } + /// /// An event that allows developers to modify the tree node collection that is being rendered /// diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 7f6fa28187..c79b9f8781 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -8,7 +8,8 @@ public const string IsDialog = "isDialog"; public const string Application = "application"; public const string StartNodeId = "startNodeId"; + public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; //public const string OnNodeClick = "OnNodeClick"; //public const string RenderParent = "RenderParent"; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index 18880b9f96..64fe9a4b65 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -52,7 +52,7 @@ namespace Umbraco.Web.WebApi.Filters _paramName = paramName; _permissionToCheck = ActionBrowse.Instance.Letter; - } + } public EnsureUserPermissionForContentAttribute(string paramName, char permissionToCheck) : this(paramName) @@ -72,6 +72,9 @@ namespace Umbraco.Web.WebApi.Filters //not logged in throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); } + + var ignoreUserStartNodes = actionContext.ActionArguments.ContainsKey("ignoreUserStartNodes") && + bool.Parse(actionContext.ActionArguments.GetValueAsString("ignoreUserStartNodes")); int nodeId; if (_nodeId.HasValue == false) @@ -124,9 +127,11 @@ namespace Umbraco.Web.WebApi.Filters actionContext.Request.Properties, UmbracoContext.Current.Security.CurrentUser, ApplicationContext.Current.Services.UserService, - ApplicationContext.Current.Services.ContentService, - ApplicationContext.Current.Services.EntityService, - nodeId, _permissionToCheck.HasValue ? new[]{_permissionToCheck.Value}: null)) + ApplicationContext.Current.Services.ContentService, + ApplicationContext.Current.Services.EntityService, + nodeId, + _permissionToCheck.HasValue ? new[]{_permissionToCheck.Value}: null, + ignoreUserStartNodes: ignoreUserStartNodes)) { base.OnActionExecuting(actionContext); } diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index beb67f3395..23d4fb871c 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -3,11 +3,13 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Web; using System.Web.Http.Filters; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Trees; namespace Umbraco.Web.WebApi.Filters { @@ -77,7 +79,12 @@ namespace Umbraco.Web.WebApi.Filters protected virtual void FilterItems(IUser user, IList items) { - FilterBasedOnStartNode(items, user); + bool.TryParse(HttpContext.Current.Request.QueryString.Get(TreeQueryStringParameters.IgnoreUserStartNodes), out var ignoreUserStartNodes); + + if (ignoreUserStartNodes == false) + { + FilterBasedOnStartNode(items, user); + } } internal void FilterBasedOnStartNode(IList items, IUser user) From d51daa2a8a8a4f372e91314b9b35ead6a0af666a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Mar 2019 13:25:55 +0100 Subject: [PATCH 011/201] Make sure the correct user is registered in the log when emptying the recycle bin --- src/Umbraco.Core/Services/ContentService.cs | 12 ++++++++++-- src/Umbraco.Core/Services/IContentService.cs | 8 ++++++++ src/Umbraco.Web/Editors/ContentController.cs | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index ba777ef59e..6e4fd54de5 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1741,7 +1741,15 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - public void EmptyRecycleBin() + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public void EmptyRecycleBin() => EmptyRecycleBin(0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + public void EmptyRecycleBin(int userId = 0) { using (new WriteLock(Locker)) { @@ -1771,7 +1779,7 @@ namespace Umbraco.Core.Services recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success; uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); - Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", userId, Constants.System.RecycleBinContent); uow.Commit(); } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6d0ca051a1..0587e70548 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -438,8 +438,16 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] void EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + void EmptyRecycleBin(int userId = 0); + /// /// Rollback an object to a previous version. /// This will create a new version, which is a copy of all the old data. diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 5f3022b2a9..beff2c2615 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -850,7 +850,7 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] public HttpResponseMessage EmptyRecycleBin() { - Services.ContentService.EmptyRecycleBin(); + Services.ContentService.EmptyRecycleBin(Security.CurrentUser.Id); return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); } From 011d406301a4e635cdba9c1bbc1440128c791bb9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 25 Mar 2019 11:59:41 +0100 Subject: [PATCH 012/201] https://github.com/umbraco/Umbraco-CMS/issues/5019 - Changed log level from error to debug. This will spam the log if we request with invalid parameters --- src/Umbraco.Core/UriExtensions.cs | 646 +++++++++++++++--------------- 1 file changed, 323 insertions(+), 323 deletions(-) diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index dd3ae8c10c..2d9d644514 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -1,323 +1,323 @@ -using System; -using System.IO; -using System.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; - -namespace Umbraco.Core -{ - /// - /// Provides extension methods to . - /// - public static class UriExtensions - { - /// - /// Checks if the current uri is a back office request - /// - /// - /// - /// The current application path or VirtualPath - /// - /// - /// - /// There are some special routes we need to check to properly determine this: - /// - /// If any route has an extension in the path like .aspx = back office - /// - /// These are def back office: - /// /Umbraco/RestServices = back office - /// /Umbraco/BackOffice = back office - /// If it's not any of the above, and there's no extension then we cannot determine if it's back office or front-end - /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the backoffice - /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. - /// - /// These are def front-end: - /// /Umbraco/Surface = front-end - /// /Umbraco/Api = front-end - /// But if we've got this far we'll just have to assume it's front-end anyways. - /// - /// - internal static bool IsBackOfficeRequest(this Uri url, string applicationPath) - { - applicationPath = applicationPath ?? string.Empty; - - var fullUrlPath = url.AbsolutePath.TrimStart(new[] {'/'}); - var appPath = applicationPath.TrimStart(new[] {'/'}); - var urlPath = fullUrlPath.TrimStart(appPath).EnsureStartsWith('/'); - - //check if this is in the umbraco back office - var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/').TrimStart(appPath.EnsureStartsWith('/')).EnsureStartsWith('/')); - //if not, then def not back office - if (isUmbracoPath == false) return false; - - //if its the normal /umbraco path - if (urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea) - || urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea + "/")) - { - return true; - } - - //check for a file extension - var extension = Path.GetExtension(url.LocalPath); - //has an extension, def back office - if (extension.IsNullOrWhiteSpace() == false) return true; - //check for special case asp.net calls like: - // /umbraco/webservices/legacyAjaxCalls.asmx/js which will return a null file extension but are still considered extension'd requests - if (urlPath.InvariantContains(".asmx/") - || urlPath.InvariantContains(".aspx/") - || urlPath.InvariantContains(".ashx/") - || urlPath.InvariantContains(".axd/") - || urlPath.InvariantContains(".svc/")) - { - return true; - } - - //check for special back office paths - if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/BackOffice/") - || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/RestServices/")) - { - return true; - } - - //check for special front-end paths - if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Surface/") - || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Api/")) - { - return false; - } - - //if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by - // checking how many parts the route has, for example, all PluginController routes will be routed like - // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} - // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a - // plugin controller for the front-end. - if (urlPath.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries).Length >= 3) - { - return false; - } - - //if its anything else we can assume it's back office - return true; - } - - /// - /// Checks if the current uri is an install request - /// - /// - /// - internal static bool IsInstallerRequest(this Uri url) - { - var authority = url.GetLeftPart(UriPartial.Authority); - var afterAuthority = url.GetLeftPart(UriPartial.Query) - .TrimStart(authority) - .TrimStart("/"); - - //check if this is in the umbraco back office - return afterAuthority.InvariantStartsWith(IOHelper.ResolveUrl("~/install").TrimStart("/")); - } - - /// - /// Checks if the uri is a request for the default back office page - /// - /// - /// - internal static bool IsDefaultBackOfficeRequest(this Uri url) - { - if (url.AbsolutePath.InvariantEquals(GlobalSettings.Path.TrimEnd("/")) - || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/')) - || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default") - || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default/")) - { - return true; - } - return false; - } - - /// - /// This is a performance tweak to check if this not an ASP.Net server file - /// .Net will pass these requests through to the module when in integrated mode. - /// We want to ignore all of these requests immediately. - /// - /// - /// - internal static bool IsClientSideRequest(this Uri url) - { - try - { - var ext = Path.GetExtension(url.LocalPath); - if (ext.IsNullOrWhiteSpace()) return false; - var toInclude = new[] {".aspx", ".ashx", ".asmx", ".axd", ".svc"}; - return toInclude.Any(ext.InvariantEquals) == false; - } - catch (ArgumentException ex) - { - LogHelper.Error(typeof(UriExtensions), "Failed to determine if request was client side", ex); - return false; - } - } - - /// - /// Rewrites the path of uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path) - { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) - : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); - } - - /// - /// Rewrites the path and query of a uri. - /// - /// The uri. - /// The new path, which must begin with a slash. - /// The new query, which must be empty or begin with a question mark. - /// The rewritten uri. - /// Everything else remains unchanged, except for the fragment which is removed. - public static Uri Rewrite(this Uri uri, string path, string query) - { - if (path.StartsWith("/") == false) - throw new ArgumentException("Path must start with a slash.", "path"); - if (query.Length > 0 && query.StartsWith("?") == false) - throw new ArgumentException("Query must start with a question mark.", "query"); - if (query == "?") - query = ""; - - return uri.IsAbsoluteUri - ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) - : new Uri(path + query, UriKind.Relative); - } - - /// - /// Gets the absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePath(this Uri uri) - { - if (uri.IsAbsoluteUri) - return uri.AbsolutePath; - - // cannot get .AbsolutePath on relative uri (InvalidOperation) - var s = uri.OriginalString; - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var pos = posq > 0 ? posq : (posf > 0 ? posf : 0); - var path = pos > 0 ? s.Substring(0, pos) : s; - return path; - } - - /// - /// Gets the decoded, absolute path of the uri. - /// - /// The uri. - /// The absolute path of the uri. - /// Only for absolute uris. - public static string GetAbsolutePathDecoded(this Uri uri) - { - return System.Web.HttpUtility.UrlDecode(uri.AbsolutePath); - } - - /// - /// Gets the decoded, absolute path of the uri, even if the uri is relative. - /// - /// The uri. - /// The absolute path of the uri. - /// Default uri.AbsolutePath does not support relative uris. - public static string GetSafeAbsolutePathDecoded(this Uri uri) - { - return System.Web.HttpUtility.UrlDecode(uri.GetSafeAbsolutePath()); - } - - /// - /// Rewrites the path of the uri so it ends with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri EndPathWithSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); - return uri; - } - - if (path != "/" && path.EndsWith("/") == false) - uri = new Uri(path + "/" + uri.Query, UriKind.Relative); - - return uri; - } - - /// - /// Rewrites the path of the uri so it does not end with a slash. - /// - /// The uri. - /// The rewritten uri. - /// Everything else remains unchanged. - public static Uri TrimPathEndSlash(this Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (uri.IsAbsoluteUri) - { - if (path != "/") - uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd('/') + uri.Query); - } - else - { - if (path != "/") - uri = new Uri(path.TrimEnd('/') + uri.Query, UriKind.Relative); - } - return uri; - } - - /// - /// Transforms a relative uri into an absolute uri. - /// - /// The relative uri. - /// The base absolute uri. - /// The absolute uri. - public static Uri MakeAbsolute(this Uri uri, Uri baseUri) - { - if (uri.IsAbsoluteUri) - throw new ArgumentException("Uri is already absolute.", "uri"); - - return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); - } - - static string GetSafeQuery(this Uri uri) - { - if (uri.IsAbsoluteUri) - return uri.Query; - - // cannot get .Query on relative uri (InvalidOperation) - var s = uri.OriginalString; - var posq = s.IndexOf("?", StringComparison.Ordinal); - var posf = s.IndexOf("#", StringComparison.Ordinal); - var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); - - return query; - } - - /// - /// Removes the port from the uri. - /// - /// The uri. - /// The same uri, without its port. - public static Uri WithoutPort(this Uri uri) - { - return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); - } - } -} +using System; +using System.IO; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; + +namespace Umbraco.Core +{ + /// + /// Provides extension methods to . + /// + public static class UriExtensions + { + /// + /// Checks if the current uri is a back office request + /// + /// + /// + /// The current application path or VirtualPath + /// + /// + /// + /// There are some special routes we need to check to properly determine this: + /// + /// If any route has an extension in the path like .aspx = back office + /// + /// These are def back office: + /// /Umbraco/RestServices = back office + /// /Umbraco/BackOffice = back office + /// If it's not any of the above, and there's no extension then we cannot determine if it's back office or front-end + /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the backoffice + /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. + /// + /// These are def front-end: + /// /Umbraco/Surface = front-end + /// /Umbraco/Api = front-end + /// But if we've got this far we'll just have to assume it's front-end anyways. + /// + /// + internal static bool IsBackOfficeRequest(this Uri url, string applicationPath) + { + applicationPath = applicationPath ?? string.Empty; + + var fullUrlPath = url.AbsolutePath.TrimStart(new[] {'/'}); + var appPath = applicationPath.TrimStart(new[] {'/'}); + var urlPath = fullUrlPath.TrimStart(appPath).EnsureStartsWith('/'); + + //check if this is in the umbraco back office + var isUmbracoPath = urlPath.InvariantStartsWith(GlobalSettings.Path.EnsureStartsWith('/').TrimStart(appPath.EnsureStartsWith('/')).EnsureStartsWith('/')); + //if not, then def not back office + if (isUmbracoPath == false) return false; + + //if its the normal /umbraco path + if (urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea) + || urlPath.InvariantEquals("/" + GlobalSettings.UmbracoMvcArea + "/")) + { + return true; + } + + //check for a file extension + var extension = Path.GetExtension(url.LocalPath); + //has an extension, def back office + if (extension.IsNullOrWhiteSpace() == false) return true; + //check for special case asp.net calls like: + // /umbraco/webservices/legacyAjaxCalls.asmx/js which will return a null file extension but are still considered extension'd requests + if (urlPath.InvariantContains(".asmx/") + || urlPath.InvariantContains(".aspx/") + || urlPath.InvariantContains(".ashx/") + || urlPath.InvariantContains(".axd/") + || urlPath.InvariantContains(".svc/")) + { + return true; + } + + //check for special back office paths + if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/BackOffice/") + || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/RestServices/")) + { + return true; + } + + //check for special front-end paths + if (urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Surface/") + || urlPath.InvariantStartsWith("/" + GlobalSettings.UmbracoMvcArea + "/Api/")) + { + return false; + } + + //if its none of the above, we will have to try to detect if it's a PluginController route, we can detect this by + // checking how many parts the route has, for example, all PluginController routes will be routed like + // Umbraco/MYPLUGINAREA/MYCONTROLLERNAME/{action}/{id} + // so if the path contains at a minimum 3 parts: Umbraco + MYPLUGINAREA + MYCONTROLLERNAME then we will have to assume it is a + // plugin controller for the front-end. + if (urlPath.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries).Length >= 3) + { + return false; + } + + //if its anything else we can assume it's back office + return true; + } + + /// + /// Checks if the current uri is an install request + /// + /// + /// + internal static bool IsInstallerRequest(this Uri url) + { + var authority = url.GetLeftPart(UriPartial.Authority); + var afterAuthority = url.GetLeftPart(UriPartial.Query) + .TrimStart(authority) + .TrimStart("/"); + + //check if this is in the umbraco back office + return afterAuthority.InvariantStartsWith(IOHelper.ResolveUrl("~/install").TrimStart("/")); + } + + /// + /// Checks if the uri is a request for the default back office page + /// + /// + /// + internal static bool IsDefaultBackOfficeRequest(this Uri url) + { + if (url.AbsolutePath.InvariantEquals(GlobalSettings.Path.TrimEnd("/")) + || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/')) + || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default") + || url.AbsolutePath.InvariantEquals(GlobalSettings.Path.EnsureEndsWith('/') + "Default/")) + { + return true; + } + return false; + } + + /// + /// This is a performance tweak to check if this not an ASP.Net server file + /// .Net will pass these requests through to the module when in integrated mode. + /// We want to ignore all of these requests immediately. + /// + /// + /// + internal static bool IsClientSideRequest(this Uri url) + { + try + { + var ext = Path.GetExtension(url.LocalPath); + if (ext.IsNullOrWhiteSpace()) return false; + var toInclude = new[] {".aspx", ".ashx", ".asmx", ".axd", ".svc"}; + return toInclude.Any(ext.InvariantEquals) == false; + } + catch (ArgumentException) + { + LogHelper.Debug(typeof(UriExtensions), "Failed to determine if request was client side. Due to invalid characters in path: {0}", () => url.LocalPath); + return false; + } + } + + /// + /// Rewrites the path of uri. + /// + /// The uri. + /// The new path, which must begin with a slash. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path) + { + if (path.StartsWith("/") == false) + throw new ArgumentException("Path must start with a slash.", "path"); + + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + uri.Query) + : new Uri(path + uri.GetSafeQuery(), UriKind.Relative); + } + + /// + /// Rewrites the path and query of a uri. + /// + /// The uri. + /// The new path, which must begin with a slash. + /// The new query, which must be empty or begin with a question mark. + /// The rewritten uri. + /// Everything else remains unchanged, except for the fragment which is removed. + public static Uri Rewrite(this Uri uri, string path, string query) + { + if (path.StartsWith("/") == false) + throw new ArgumentException("Path must start with a slash.", "path"); + if (query.Length > 0 && query.StartsWith("?") == false) + throw new ArgumentException("Query must start with a question mark.", "query"); + if (query == "?") + query = ""; + + return uri.IsAbsoluteUri + ? new Uri(uri.GetLeftPart(UriPartial.Authority) + path + query) + : new Uri(path + query, UriKind.Relative); + } + + /// + /// Gets the absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePath(this Uri uri) + { + if (uri.IsAbsoluteUri) + return uri.AbsolutePath; + + // cannot get .AbsolutePath on relative uri (InvalidOperation) + var s = uri.OriginalString; + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var pos = posq > 0 ? posq : (posf > 0 ? posf : 0); + var path = pos > 0 ? s.Substring(0, pos) : s; + return path; + } + + /// + /// Gets the decoded, absolute path of the uri. + /// + /// The uri. + /// The absolute path of the uri. + /// Only for absolute uris. + public static string GetAbsolutePathDecoded(this Uri uri) + { + return System.Web.HttpUtility.UrlDecode(uri.AbsolutePath); + } + + /// + /// Gets the decoded, absolute path of the uri, even if the uri is relative. + /// + /// The uri. + /// The absolute path of the uri. + /// Default uri.AbsolutePath does not support relative uris. + public static string GetSafeAbsolutePathDecoded(this Uri uri) + { + return System.Web.HttpUtility.UrlDecode(uri.GetSafeAbsolutePath()); + } + + /// + /// Rewrites the path of the uri so it ends with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri EndPathWithSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/" && path.EndsWith("/") == false) + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path + "/" + uri.Query); + return uri; + } + + if (path != "/" && path.EndsWith("/") == false) + uri = new Uri(path + "/" + uri.Query, UriKind.Relative); + + return uri; + } + + /// + /// Rewrites the path of the uri so it does not end with a slash. + /// + /// The uri. + /// The rewritten uri. + /// Everything else remains unchanged. + public static Uri TrimPathEndSlash(this Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (uri.IsAbsoluteUri) + { + if (path != "/") + uri = new Uri(uri.GetLeftPart(UriPartial.Authority) + path.TrimEnd('/') + uri.Query); + } + else + { + if (path != "/") + uri = new Uri(path.TrimEnd('/') + uri.Query, UriKind.Relative); + } + return uri; + } + + /// + /// Transforms a relative uri into an absolute uri. + /// + /// The relative uri. + /// The base absolute uri. + /// The absolute uri. + public static Uri MakeAbsolute(this Uri uri, Uri baseUri) + { + if (uri.IsAbsoluteUri) + throw new ArgumentException("Uri is already absolute.", "uri"); + + return new Uri(baseUri.GetLeftPart(UriPartial.Authority) + uri.GetSafeAbsolutePath() + uri.GetSafeQuery()); + } + + static string GetSafeQuery(this Uri uri) + { + if (uri.IsAbsoluteUri) + return uri.Query; + + // cannot get .Query on relative uri (InvalidOperation) + var s = uri.OriginalString; + var posq = s.IndexOf("?", StringComparison.Ordinal); + var posf = s.IndexOf("#", StringComparison.Ordinal); + var query = posq < 0 ? null : (posf < 0 ? s.Substring(posq) : s.Substring(posq, posf - posq)); + + return query; + } + + /// + /// Removes the port from the uri. + /// + /// The uri. + /// The same uri, without its port. + public static Uri WithoutPort(this Uri uri) + { + return new Uri(uri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped)); + } + } +} From 8a16c749e1bfdd64a2a91e69649e45599015f90c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2019 10:09:21 +0100 Subject: [PATCH 013/201] Mixin for limiting the width of a property-editor --- src/Umbraco.Web.UI.Client/src/less/mixins.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index 20d4e8d1f8..ce35097658 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -402,6 +402,11 @@ // COMPONENT MIXINS // -------------------------------------------------- +// Limit width of specific property editors +.umb-property-editor--limit-width { + max-width: 800px; +} + // Horizontal dividers // ------------------------- // Dividers (basically an hr) within dropdowns and nav lists From 5899f08d7fcf5346fb9f2c25b06edc87f7a30c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2019 10:09:43 +0100 Subject: [PATCH 014/201] removing the default width limitation. --- .../src/less/property-editors.less | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 279163d64b..c817c5ddd7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -4,35 +4,12 @@ // Container styles // -------------------------------------------------- .umb-property-editor { - @media (max-width: 800px) { - width: 100%; - } - @media (min-width: 800px) { - min-width:66.6%; - } - - &-pull { - float:left; - width:66.6%; - } - - &-push { - float:right; - } - - &--list{ - float: left; - } - - &__item{ - line-height: 1; - margin: 0 0 5px; - } + width: 100%; } .umb-property-editor-tiny { - width: 60px; - + width: 60px; + &.umb-editor-push { width:30%; min-width:0; @@ -87,6 +64,10 @@ // // Content picker // -------------------------------------------------- +.umb-contentpicker { + .umb-property-editor--limit-width(); +} + .umb-contentpicker li a:hover .hover-hide, .umb-contentpicker li a .hover-show{ display: none; } @@ -856,6 +837,8 @@ padding: 10px; font-size: 13px; text-shadow: none; + box-sizing: border-box; + .umb-property-editor--limit-width(); .tag { cursor: default; From 31fd8066a4bb3e8bb168f60470d6368b87eec3f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2019 10:10:13 +0100 Subject: [PATCH 015/201] fixing border color, which needs to be white. --- .../src/views/propertyeditors/grid/grid.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 068a60462c..cb39c51258 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -1,6 +1,6 @@
    - + Date: Tue, 26 Mar 2019 10:10:34 +0100 Subject: [PATCH 016/201] setting width limit of selected property editors --- src/Umbraco.Web.UI.Client/src/less/belle.less | 3 +++ .../src/less/components/umb-dropdown.less | 3 +++ .../src/less/components/umb-node-preview.less | 4 ++-- .../src/less/components/umb-textarea.less | 3 +++ .../src/less/components/umb-textstring.less | 3 +++ src/Umbraco.Web.UI.Client/src/less/listview.less | 2 -- src/Umbraco.Web.UI.Client/src/less/rte.less | 2 ++ 7 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umb-dropdown.less create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umb-textarea.less create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umb-textstring.less diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 8c0df988d1..e915106397 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -154,6 +154,9 @@ @import "components/umb-number-badge.less"; @import "components/umb-progress-circle.less"; @import "components/umb-stylesheet.less"; +@import "components/umb-textstring.less"; +@import "components/umb-textarea.less"; +@import "components/umb-dropdown.less"; @import "components/umb-range-slider.less"; @import "components/buttons/umb-button.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-dropdown.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-dropdown.less new file mode 100644 index 0000000000..3361329015 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-dropdown.less @@ -0,0 +1,3 @@ +.umb-dropdown { + .umb-property-editor--limit-width(); +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index d52cb918f6..1edaffe824 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -6,7 +6,7 @@ } .umb-editor-wrapper .umb-node-preview { - max-width: 66.6%; + .umb-property-editor--limit-width(); } .umb-node-preview:last-of-type { @@ -103,7 +103,7 @@ } .umb-editor-wrapper .umb-node-preview-add { - max-width: 66.6%; + .umb-property-editor--limit-width(); } .umb-overlay, diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-textarea.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-textarea.less new file mode 100644 index 0000000000..86df7c7dd5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-textarea.less @@ -0,0 +1,3 @@ +.umb-textarea { + .umb-property-editor--limit-width(); +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-textstring.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-textstring.less new file mode 100644 index 0000000000..bc4ba033aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-textstring.less @@ -0,0 +1,3 @@ +.umb-textstring { + .umb-property-editor--limit-width(); +} diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index f942987532..9bd582a8ad 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -1,8 +1,6 @@ // Listview // ------------------------- -.umb-listview{width: auto !important;} - .umb-listview table { border: 1px solid @gray-8; } diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index ba8d02c1e1..a4d6387012 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -3,6 +3,8 @@ .umb-rte { position: relative; + + .umb-property-editor--limit-width(); .-loading { position: absolute; From ff9179a15f10a8f2efe84fc325ee8834954ec222 Mon Sep 17 00:00:00 2001 From: Dave de Moel Date: Wed, 27 Mar 2019 10:33:21 +0100 Subject: [PATCH 017/201] Changed check to use localeCompare --- .../src/common/services/tree.service.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 6a15c0f553..a593e24bfe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -165,8 +165,9 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS Umbraco.Sys.ServerVariables.umbracoPlugins.trees && angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { - var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function(item) { - return item.alias === treeAlias; + var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { + // localeCompare returns 0 when strings are equal so we need to add one to make it true. + return item.alias.localeCompare(treeAlias, undefined, { ignorePunctuation: true }) + 1; }); return found ? found.packageFolder : undefined; From bb43fc310aaae9a42a4230c54f32f4ca9e09203c Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 27 Mar 2019 17:11:21 +0100 Subject: [PATCH 018/201] Add service-level loading of preview Xml document --- .../Repositories/ContentRepository.cs | 69 +++++++++++++++++-- .../Interfaces/IContentRepository.cs | 4 +- src/Umbraco.Core/Services/ContentService.cs | 11 +++ src/Umbraco.Core/Services/IContentService.cs | 2 + 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 4f19dc9241..be5cc20cb6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -199,7 +199,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } @@ -468,7 +468,7 @@ namespace Umbraco.Core.Persistence.Repositories entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set entity.Path = nodeDto.Path; entity.SortOrder = sortOrder; - entity.Level = level; + entity.Level = level; //Create the Content specific data - cmsContent var contentDto = dto.ContentVersionDto.ContentDto; @@ -644,7 +644,7 @@ namespace Umbraco.Core.Persistence.Repositories contentVersionDto.Id = contentVerDto.Id; Database.Update(contentVersionDto); } - + Database.Update(dto); } @@ -752,7 +752,7 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetBlueprints(IQuery query) { Func, Sql> translate = t => t.Translate(); - + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); var translatorFull = new SqlTranslator(sqlFull, query); var sqlIds = GetBaseQuery(BaseQueryType.Ids); @@ -835,6 +835,61 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", } + public XmlDocument BuildPreviewXmlCache() + { + var xmlDoc = new XmlDocument(); + var doctype = xmlDoc.CreateDocumentType("root", null, null, + ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); + xmlDoc.AppendChild(doctype); + var parent = xmlDoc.CreateElement("root"); + var pIdAtt = xmlDoc.CreateAttribute("id"); + pIdAtt.Value = "-1"; + parent.Attributes.Append(pIdAtt); + xmlDoc.AppendChild(parent); + + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode +inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type +inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); + + XmlElement last = null; + + //NOTE: Query creates a reader - does not load all into memory + foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + { + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) + { + parent = xmlDoc.GetElementById(parentId); + if (parent == null) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); + } + + return xmlDoc; + + } + public int CountPublished(string contentTypeAlias = null) { if (contentTypeAlias.IsNullOrWhiteSpace()) @@ -868,9 +923,9 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", ///
    /// /// - /// + /// public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) - { + { _permissionRepository.AssignEntityPermission(entity, permission, groupIds); } @@ -882,7 +937,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", public EntityPermissionCollection GetPermissionsForEntity(int entityId) { return _permissionRepository.GetPermissionsForEntity(entityId); - } + } /// /// Adds/updates content/published xml diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 5edd73f760..689c12efc6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -19,6 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories /// XmlDocument BuildXmlCache(); + XmlDocument BuildPreviewXmlCache(); + /// /// Get the count of published items /// @@ -96,4 +98,4 @@ namespace Umbraco.Core.Persistence.Repositories /// void AddOrUpdatePreviewXml(IContent content, Func xml); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index ba777ef59e..2fe1bb4d5b 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -2192,6 +2192,17 @@ namespace Umbraco.Core.Services } } + public XmlDocument BuildPreviewXmlCache() + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + var result = repository.BuildPreviewXmlCache(); + uow.Commit(); + return result; + } + } + /// /// Rebuilds all xml content in the cmsContentXml table for all documents /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6d0ca051a1..7512064ed3 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -124,6 +124,8 @@ namespace Umbraco.Core.Services /// XmlDocument BuildXmlCache(); + XmlDocument BuildPreviewXmlCache(); + /// /// Rebuilds all xml content in the cmsContentXml table for all documents /// From 0676726bfcf2343e729af869420975ab9242701b Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 28 Mar 2019 08:42:20 +0100 Subject: [PATCH 019/201] Refactor preview --- .../Cache/UnpublishedPageCacheRefresher.cs | 17 +- .../PublishedContentCache.cs | 2 + .../umbraco.presentation/content.cs | 207 ++++++++++++++++++ .../umbraco/preview/PreviewContent.cs | 76 ++++++- 4 files changed, 292 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index 6404d60b40..bf58454bfc 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Models; using System.Linq; using Newtonsoft.Json; +using umbraco.cms.businesslogic.web; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Sync; @@ -44,7 +45,7 @@ namespace Umbraco.Web.Cache return jsonObject; } - + internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] contentIds) { var items = contentIds.Select(x => new JsonPayload @@ -61,12 +62,12 @@ namespace Umbraco.Web.Cache #region Sub classes internal enum OperationType - { + { Deleted } internal class JsonPayload - { + { public int Id { get; set; } public OperationType Operation { get; set; } } @@ -79,6 +80,7 @@ namespace Umbraco.Web.Cache ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(); base.RefreshAll(); } @@ -87,6 +89,9 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(id); + var d = new Document(id); + content.Instance.UpdateDocumentCache(d); + content.Instance.UpdatePreviewXmlContent(d); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(id); } @@ -97,6 +102,7 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(id); base.Remove(id); } @@ -106,6 +112,9 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(instance); + var d = new Document(instance); + content.Instance.UpdateDocumentCache(d); + content.Instance.UpdatePreviewXmlContent(d); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(instance); } @@ -116,6 +125,7 @@ namespace Umbraco.Web.Cache ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + content.Instance.ClearPreviewXmlContent(instance.Id); base.Remove(instance); } @@ -132,6 +142,7 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.Services.IdkMap.ClearCache(payload.Id); ClearRepositoryCacheItemById(payload.Id); content.Instance.UpdateSortOrder(payload.Id); + content.Instance.ClearPreviewXmlContent(payload.Id); } DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 59772e8237..e9342b6136 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -523,6 +523,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (preview) { + if (PreviewContent.IsSinglePreview) + return content.Instance.PreviewXmlContent; var previewContent = PreviewContentCache.GetOrCreateValue(context); // will use the ctor with no parameters previewContent.EnsureInitialized(context.UmbracoUser, StateHelper.Cookies.Preview.GetValue(), true, () => { diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index ccd2e009a7..deb3f0be17 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; +using System.Threading; using System.Web; using System.Xml; using umbraco.BusinessLogic; @@ -13,6 +14,7 @@ using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.web; using umbraco.DataLayer; using umbraco.presentation.nodeFactory; +using umbraco.presentation.preview; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -275,6 +277,8 @@ namespace umbraco safeXml.AcceptChanges(); } + + ClearPreviewXmlContent(); } /// @@ -474,6 +478,8 @@ namespace umbraco safeXml.AcceptChanges(); } } + + ClearPreviewXmlContent(id); } /// @@ -668,6 +674,21 @@ namespace umbraco _persisterTask = _persisterTask.Touch(); // _persisterTask != null because SyncToXmlFile == true } + private static bool HasSchema(string contentTypeAlias, XmlDocument xml) + { + string subset = null; + + // get current doctype + var n = xml.FirstChild; + while (n.NodeType != XmlNodeType.DocumentType && n.NextSibling != null) + n = n.NextSibling; + if (n.NodeType == XmlNodeType.DocumentType) + subset = ((XmlDocumentType)n).InternalSubset; + + // ensure it contains the content type + return subset != null && subset.Contains(string.Format("", contentTypeAlias)); + } + private static XmlDocument EnsureSchema(string contentTypeAlias, XmlDocument xml) { string subset = null; @@ -1260,5 +1281,191 @@ namespace umbraco } #endregion + + #region Preview + + private const string PreviewCacheKey = "umbraco.content.preview"; + + internal void ClearPreviewXmlContent() + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + runtimeCache.ClearCacheItem(PreviewCacheKey); + } + + internal void ClearPreviewXmlContent(int id) + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + // Check if node present, before cloning + var x = xml.GetElementById(id.ToString()); + if (x == null) + return; + + // Find the document in the xml cache + // The document already exists in cache, so repopulate it + x.ParentNode.RemoveChild(x); + } + + internal void UpdatePreviewXmlContent(Document d) + { + if (PreviewContent.IsSinglePreview == false) return; + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + var pnode = GetPreviewOrPublishedNode(d, xml, true); + var pattr = ((XmlElement)pnode).GetAttributeNode("sortOrder"); + pattr.Value = d.sortOrder.ToString(); + AddOrUpdatePreviewXmlNode(d.Id, d.Level, d.Level == 1 ? -1 : d.ParentId, pnode); + } + + private void AddOrUpdatePreviewXmlNode(int id, int level, int parentId, XmlNode docNode) + { + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + // sanity checks + if (id != docNode.AttributeValue("id")) + throw new ArgumentException("Values of id and docNode/@id are different."); + if (parentId != docNode.AttributeValue("parentID")) + throw new ArgumentException("Values of parentId and docNode/@parentID are different."); + + // find the document in the cache + XmlNode currentNode = xml.GetElementById(id.ToInvariantString()); + + // if the document is not there already then it's a new document + // we must make sure that its document type exists in the schema + if (currentNode == null && UseLegacySchema == false) + { + if (HasSchema(docNode.Name, xml) == false) + { + runtimeCache.ClearCacheItem(PreviewCacheKey); + return; + } + } + + // find the parent + XmlNode parentNode = level == 1 + ? xml.DocumentElement + : xml.GetElementById(parentId.ToInvariantString()); + + // no parent = cannot do anything + if (parentNode == null) + return; + + // insert/move the node under the parent + if (currentNode == null) + { + // document not there, new node, append + currentNode = docNode; + parentNode.AppendChild(currentNode); + } + else + { + // document found... we could just copy the currentNode children nodes over under + // docNode, then remove currentNode and insert docNode... the code below tries to + // be clever and faster, though only benchmarking could tell whether it's worth the + // pain... + + // first copy current parent ID - so we can compare with target parent + var moving = currentNode.AttributeValue("parentID") != parentId; + + if (docNode.Name == currentNode.Name) + { + // name has not changed, safe to just update the current node + // by transfering values eg copying the attributes, and importing the data elements + TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode); + + // if moving, move the node to the new parent + // else it's already under the right parent + // (but maybe the sort order has been updated) + if (moving) + parentNode.AppendChild(currentNode); // remove then append to parentNode + } + else + { + // name has changed, must use docNode (with new name) + // move children nodes from currentNode to docNode (already has properties) + var children = currentNode.SelectNodes(ChildNodesXPath); + if (children == null) throw new Exception("oops"); + foreach (XmlNode child in children) + docNode.AppendChild(child); // remove then append to docNode + + // and put docNode in the right place - if parent has not changed, then + // just replace, else remove currentNode and insert docNode under the right parent + // (but maybe not at the right position due to sort order) + if (moving) + { + if (currentNode.ParentNode == null) throw new Exception("oops"); + currentNode.ParentNode.RemoveChild(currentNode); + parentNode.AppendChild(docNode); + } + else + { + // replacing might screw the sort order + parentNode.ReplaceChild(docNode, currentNode); + } + + currentNode = docNode; + } + } + + // if the nodes are not ordered, must sort + // (see U4-509 + has to work with ReplaceChild too) + //XmlHelper.SortNodesIfNeeded(parentNode, childNodesXPath, x => x.AttributeValue("sortOrder")); + + // but... + // if we assume that nodes are always correctly sorted + // then we just need to ensure that currentNode is at the right position. + // should be faster that moving all the nodes around. + XmlHelper.SortNode(parentNode, ChildNodesXPath, currentNode, x => x.AttributeValue("sortOrder")); + } + + // UpdateSortOrder is meant to update the Xml cache sort order on Save, 'cos that change + // should be applied immediately, even though the Xml cache is not updated on Saves - we + // don't have to do it for preview Xml since it is always fully updated - OTOH we have + // to ensure it *is* updated, in UnpublishedPageCacheRefresher + + private XmlDocument LoadPreviewXmlContent() + { + try + { + LogHelper.Info("Loading preview content from database..."); + var xml = ApplicationContext.Current.Services.ContentService.BuildPreviewXmlCache(); + LogHelper.Debug("Done loading preview content"); + return xml; + } + catch (Exception ee) + { + LogHelper.Error("Error loading preview content", ee); + } + + // An error of some sort must have stopped us from successfully generating + // the content tree, so lets return null signifying there is no content available + return null; + } + + public XmlDocument PreviewXmlContent + { + get + { + if (PreviewContent.IsSinglePreview == false) + throw new InvalidOperationException(); + + var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + return runtimeCache.GetCacheItem(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true, + removedCallback: (key, removed, reason) => LogHelper.Debug($"Removed preview xml from cache ({reason})")); + } + } + + #endregion } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 3346fa608c..815db997f0 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Globalization; using System.IO; using System.Xml; @@ -11,11 +12,57 @@ using Umbraco.Core.Logging; namespace umbraco.presentation.preview { - //TODO : Migrate this to a new API! + public enum PreviewMode + { + Unknown = 0, // default value + Vintage, + SinglePreview + } public class PreviewContent { - // zb-00004 #29956 : refactor cookies names & handling + private static PreviewMode _previewMode; + private const PreviewMode DefaultPreviewMode = PreviewMode.SinglePreview; + private static int _singlePreviewCacheDurationSeconds = -1; + private const int DefaultSinglePreviewCacheDurationSeconds = 60; + + public static PreviewMode PreviewMode + { + get + { + if (_previewMode != PreviewMode.Unknown) + return _previewMode; + + var appSettings = ConfigurationManager.AppSettings; + var setting = appSettings["Umbraco.Preview.Mode"]; + if (setting.IsNullOrWhiteSpace()) + return _previewMode = DefaultPreviewMode; + if (Enum.TryParse(setting, false, out _previewMode)) + return _previewMode; + throw new ConfigurationErrorsException($"Failed to parse Umbraco.Preview.Mode appSetting, {setting} is not a valid value. " + + "Valid values are: Vintage (default), SinglePreview."); + } + } + + public static int SinglePreviewCacheDurationSeconds + { + get + { + if (_singlePreviewCacheDurationSeconds >= 0) + return _singlePreviewCacheDurationSeconds; + + var appSettings = ConfigurationManager.AppSettings; + var setting = appSettings["Umbraco.Preview.SinglePreview.CacheDurationSeconds"]; + if (setting.IsNullOrWhiteSpace()) + return _singlePreviewCacheDurationSeconds = DefaultSinglePreviewCacheDurationSeconds; + if (int.TryParse(setting, out _singlePreviewCacheDurationSeconds)) + return _singlePreviewCacheDurationSeconds; + throw new ConfigurationErrorsException($"Failed to parse Umbraco.Preview.SinglePreview.CacheDurationSeconds appSetting, {setting} is not a valid value. " + + "Valid values are positive integers."); + } + } + + public static bool IsSinglePreview => PreviewMode == PreviewMode.SinglePreview; public XmlDocument XmlContent { get; set; } public Guid PreviewSet { get; set; } @@ -35,6 +82,8 @@ namespace umbraco.presentation.preview public void EnsureInitialized(User user, string previewSet, bool validate, Action initialize) { + if (IsSinglePreview) return; + lock (_initLock) { if (_initialized) return; @@ -53,17 +102,19 @@ namespace umbraco.presentation.preview public PreviewContent(Guid previewSet) { - ValidPreviewSet = UpdatePreviewPaths(previewSet, true); + ValidPreviewSet = IsSinglePreview || UpdatePreviewPaths(previewSet, true); } public PreviewContent(User user, Guid previewSet, bool validate) { _userId = user.Id; - ValidPreviewSet = UpdatePreviewPaths(previewSet, validate); + ValidPreviewSet = IsSinglePreview || UpdatePreviewPaths(previewSet, validate); } public void PrepareDocument(User user, Document documentObject, bool includeSubs) { + if (IsSinglePreview) return; + _userId = user.Id; // clone xml @@ -144,6 +195,8 @@ namespace umbraco.presentation.preview /// public bool ValidatePreviewPath() { + if (IsSinglePreview) return true; + if (!File.Exists(PreviewsetPath)) return false; @@ -154,12 +207,21 @@ namespace umbraco.presentation.preview public void LoadPreviewset() { - XmlContent = new XmlDocument(); - XmlContent.Load(PreviewsetPath); + if (IsSinglePreview) + { + XmlContent = content.Instance.PreviewXmlContent; + } + else + { + XmlContent = new XmlDocument(); + XmlContent.Load(PreviewsetPath); + } } public void SavePreviewSet() { + if (IsSinglePreview) return; + //make sure the preview folder exists first var dir = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Preview)); if (!dir.Exists) @@ -211,7 +273,7 @@ namespace umbraco.presentation.preview public static void ClearPreviewCookie() { // zb-00004 #29956 : refactor cookies names & handling - if (UmbracoContext.Current.UmbracoUser != null) + if (!IsSinglePreview && UmbracoContext.Current.UmbracoUser != null) { if (StateHelper.Cookies.Preview.HasValue) { From 514452ddb9cb954b7d32a5a391024f480dc1591d Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 18 Mar 2019 17:51:04 +0100 Subject: [PATCH 020/201] Better ServerMessenger configuration --- src/Umbraco.Core/CompositionExtensions.cs | 22 +++++ .../BatchedDatabaseServerMessenger.cs | 5 +- ...aseServerRegistrarAndMessengerComponent.cs | 80 ++++++++----------- 3 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index e1c7ad4467..828a577c34 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -203,6 +203,28 @@ namespace Umbraco.Core composition.RegisterUnique(_ => registrar); } + /// + /// Sets the database server messenger options. + /// + /// The composition. + /// A function creating the options. + /// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default. + public static void SetDatabaseServerMessengerOptions(this Composition composition, Func factory) + { + composition.RegisterUnique(factory); + } + + /// + /// Sets the database server messenger options. + /// + /// The composition. + /// Options. + /// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default. + public static void SetDatabaseServerMessengerOptions(this Composition composition, DatabaseServerMessengerOptions options) + { + composition.RegisterUnique(_ => options); + } + /// /// Sets the short string helper. /// diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 76d7565862..818e8ecf77 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -27,9 +27,8 @@ namespace Umbraco.Web private readonly IUmbracoDatabaseFactory _databaseFactory; public BatchedDatabaseServerMessenger( - IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, - bool enableDistCalls, DatabaseServerMessengerOptions options) - : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, enableDistCalls, options) + IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, DatabaseServerMessengerOptions options) + : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) { _databaseFactory = databaseFactory; } diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 086aa9b197..a68e137665 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -38,54 +38,44 @@ namespace Umbraco.Web.Compose public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer, ICoreComposer { + public static DatabaseServerMessengerOptions GetDefaultOptions(IFactory factory) + { + var logger = factory.GetInstance(); + var indexRebuilder = factory.GetInstance(); + + return new DatabaseServerMessengerOptions + { + //These callbacks will be executed if the server has not been synced + // (i.e. it is a new server or the lastsynced.txt file has been removed) + InitializingCallbacks = new Action[] + { + //rebuild the xml cache file if the server is not synced + () => + { + // rebuild the published snapshot caches entirely, if the server is not synced + // this is equivalent to DistributedCache RefreshAll... but local only + // (we really should have a way to reuse RefreshAll... locally) + // note: refresh all content & media caches does refresh content types too + var svc = Current.PublishedSnapshotService; + svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); + svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); + svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); + }, + + //rebuild indexes if the server is not synced + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. + () => { ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); } + } + }; + } + public override void Compose(Composition composition) { base.Compose(composition); - composition.SetServerMessenger(factory => - { - var runtime = factory.GetInstance(); - var databaseFactory = factory.GetInstance(); - var globalSettings = factory.GetInstance(); - var proflog = factory.GetInstance(); - var scopeProvider = factory.GetInstance(); - var sqlContext = factory.GetInstance(); - var logger = factory.GetInstance(); - var indexRebuilder = factory.GetInstance(); - - return new BatchedDatabaseServerMessenger( - runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings, - true, - //Default options for web including the required callbacks to build caches - new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] - { - //rebuild the xml cache file if the server is not synced - () => - { - // rebuild the published snapshot caches entirely, if the server is not synced - // this is equivalent to DistributedCache RefreshAll... but local only - // (we really should have a way to reuse RefreshAll... locally) - // note: refresh all content & media caches does refresh content types too - var svc = Current.PublishedSnapshotService; - svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); - svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); - svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); - }, - - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - () => - { - ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); - } - } - }); - }); + composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); + composition.SetServerMessenger(); } } @@ -128,7 +118,7 @@ namespace Umbraco.Web.Compose } public void Initialize() - { + { //We will start the whole process when a successful request is made if (_registrar != null || _messenger != null) UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; From 7dcf2b1abb168e98cd1c190b9d7794043fb39901 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 1 Apr 2019 17:05:13 +1100 Subject: [PATCH 021/201] infinity editing dim-layer for umb-modelcolumn --- src/Umbraco.Web.UI.Client/src/less/modals.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 771be1bc2a..8e88774613 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -55,6 +55,16 @@ position: absolute;; } +.--notInFront .umb-modalcolumn::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + background: rgba(0,0,0,.4); +} + /* re-align loader */ .umb-modal .umb-loader-wrapper, .umb-modalcolumn .umb-loader-wrapper, .umb-dialog .umb-loader-wrapper{ position:relative; From 9bcd70589c82ee1c880e91a42fd753d211c22865 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 1 Apr 2019 08:21:01 +0200 Subject: [PATCH 022/201] Alt. preview: thread safety --- .../umbraco.presentation/content.cs | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index deb3f0be17..484014656c 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1285,13 +1285,17 @@ namespace umbraco #region Preview private const string PreviewCacheKey = "umbraco.content.preview"; + private readonly object _previewLock = new object(); internal void ClearPreviewXmlContent() { if (PreviewContent.IsSinglePreview == false) return; var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - runtimeCache.ClearCacheItem(PreviewCacheKey); + lock (_previewLock) + { + runtimeCache.ClearCacheItem(PreviewCacheKey); + } } internal void ClearPreviewXmlContent(int id) @@ -1299,17 +1303,21 @@ namespace umbraco if (PreviewContent.IsSinglePreview == false) return; var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - var xml = runtimeCache.GetCacheItem(PreviewCacheKey); - if (xml == null) return; - // Check if node present, before cloning - var x = xml.GetElementById(id.ToString()); - if (x == null) - return; + lock (_previewLock) + { + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; - // Find the document in the xml cache - // The document already exists in cache, so repopulate it - x.ParentNode.RemoveChild(x); + // Check if node present, before cloning + var x = xml.GetElementById(id.ToString()); + if (x == null) + return; + + // Find the document in the xml cache + // The document already exists in cache, so repopulate it + x.ParentNode.RemoveChild(x); + } } internal void UpdatePreviewXmlContent(Document d) @@ -1317,13 +1325,17 @@ namespace umbraco if (PreviewContent.IsSinglePreview == false) return; var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - var xml = runtimeCache.GetCacheItem(PreviewCacheKey); - if (xml == null) return; - var pnode = GetPreviewOrPublishedNode(d, xml, true); - var pattr = ((XmlElement)pnode).GetAttributeNode("sortOrder"); - pattr.Value = d.sortOrder.ToString(); - AddOrUpdatePreviewXmlNode(d.Id, d.Level, d.Level == 1 ? -1 : d.ParentId, pnode); + lock (_previewLock) + { + var xml = runtimeCache.GetCacheItem(PreviewCacheKey); + if (xml == null) return; + + var pnode = GetPreviewOrPublishedNode(d, xml, true); + var pattr = ((XmlElement)pnode).GetAttributeNode("sortOrder"); + pattr.Value = d.sortOrder.ToString(); + AddOrUpdatePreviewXmlNode(d.Id, d.Level, d.Level == 1 ? -1 : d.ParentId, pnode); + } } private void AddOrUpdatePreviewXmlNode(int id, int level, int parentId, XmlNode docNode) @@ -1461,8 +1473,12 @@ namespace umbraco throw new InvalidOperationException(); var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - return runtimeCache.GetCacheItem(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true, - removedCallback: (key, removed, reason) => LogHelper.Debug($"Removed preview xml from cache ({reason})")); + + lock (_previewLock) + { + return runtimeCache.GetCacheItem(PreviewCacheKey, LoadPreviewXmlContent, TimeSpan.FromSeconds(PreviewContent.SinglePreviewCacheDurationSeconds), true, + removedCallback: (key, removed, reason) => LogHelper.Debug($"Removed preview xml from cache ({reason})")); + } } } From e6d75f27de0866f7188d58aefe3c8c005f8d8c86 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 1 Apr 2019 08:23:48 +0200 Subject: [PATCH 023/201] Alt. preview: rename modes --- .../umbraco/preview/PreviewContent.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index 815db997f0..c8e1ac145c 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -15,14 +15,14 @@ namespace umbraco.presentation.preview public enum PreviewMode { Unknown = 0, // default value - Vintage, - SinglePreview + Legacy, + Default } public class PreviewContent { private static PreviewMode _previewMode; - private const PreviewMode DefaultPreviewMode = PreviewMode.SinglePreview; + private const PreviewMode DefaultPreviewMode = PreviewMode.Default; private static int _singlePreviewCacheDurationSeconds = -1; private const int DefaultSinglePreviewCacheDurationSeconds = 60; @@ -62,7 +62,7 @@ namespace umbraco.presentation.preview } } - public static bool IsSinglePreview => PreviewMode == PreviewMode.SinglePreview; + public static bool IsSinglePreview => PreviewMode == PreviewMode.Default; public XmlDocument XmlContent { get; set; } public Guid PreviewSet { get; set; } From fb1f2c8af4f5d490d2f445e2fdd11b3f15c4efaf Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 2 Apr 2019 08:12:46 +0200 Subject: [PATCH 024/201] Add content type alias to content "general" info box --- .../src/views/components/content/umb-content-node-info.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 0ed3986fe3..b37743bf01 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -198,9 +198,10 @@ style="max-width: 100%; margin-bottom: 0;" icon="node.icon" name="node.contentTypeName" + alias="documentType.alias" allow-open="allowOpen" on-open="openDocumentType(documentType)" - open-url="previewOpenUrl"> + open-url="previewOpenUrl"> From 0997fe6732723eefe9872070fad9ee2f47f57eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 2 Apr 2019 10:12:21 +0200 Subject: [PATCH 025/201] checkmark column removed --- .../editor/subheader/umb-editor-sub-header.less | 1 + .../src/less/components/umb-checkmark.less | 10 ++++++---- .../src/views/users/views/users/users.html | 13 ++----------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 2e599252bb..169edbf7f4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -24,6 +24,7 @@ padding-left: 10px; padding-right: 10px; background-color: @pinkLight; + border-color: @pinkLight; border-radius: 3px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less index 75ac13bdd0..021fc8cc9b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less @@ -2,19 +2,21 @@ border: 2px solid @white; width: 25px; height: 25px; - background: @gray-7; - border-radius: 50%; + border: 1px solid @gray-7; + border-radius: 3px; box-sizing: border-box; display: flex; justify-content: center; align-items: center; - color: @white; + color: @gray-7; cursor: pointer; font-size: 15px; } .umb-checkmark--checked { - background: @green; + background: @ui-active; + border-color: @ui-active; + color: @white; } .umb-checkmark--xs { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 7fb036a57a..2991b7621b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -229,12 +229,11 @@
    -
    + -
    Name
    User group
    Last login
    @@ -246,15 +245,7 @@ ng-click="vm.selectUser(user, vm.selection, $event)" ng-class="{'-selected': user.selected, '-selectable': vm.isSelectable(user)}" class="umb-table-row umb-user-table-row"> -
    -
    - - -
    -
    -
    +
    Date: Wed, 3 Apr 2019 10:45:04 +1100 Subject: [PATCH 026/201] Reload listview after moving items --- .../src/views/propertyeditors/listview/listview.controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 44c0c4ae7b..e52edd6e92 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -583,6 +583,8 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs .then(function () { //executes if all is successful, let's sync the tree if (newPath) { + // reload the current view so the moved items are no longer shown + $scope.reloadView($scope.contentId); //we need to do a double sync here: first refresh the node where the content was moved, // then refresh the node where the content was moved from From eb529783c46475b720309291efb438ecb35e224a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 3 Apr 2019 16:22:13 +1100 Subject: [PATCH 027/201] Don't allow move in members listview --- .../src/views/propertyeditors/listview/listview.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index e52edd6e92..8e233646bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -159,7 +159,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, - allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkMove: $scope.entityType !== 'member' && $scope.model.config.bulkActionPermissions.allowBulkMove, allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete, cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture }; From 3d949c9a55a1a0bd0c38bd4d3177ce8057a23d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 10:37:31 +0200 Subject: [PATCH 028/201] changed user groups to use umb-table --- .../users/views/groups/groups.controller.js | 50 +++++++----- .../src/views/users/views/groups/groups.html | 80 +++++++++++-------- 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index 539a064060..20f1e406b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -1,7 +1,8 @@ (function () { "use strict"; - function UserGroupsController($scope, $timeout, $location, userService, userGroupsResource, formHelper, localizationService) { + function UserGroupsController($scope, $timeout, $location, userService, userGroupsResource, + formHelper, localizationService, listViewHelper) { var vm = this; @@ -27,7 +28,8 @@ // only allow editing and selection if user is member of the group or admin vm.userGroups = _.map(userGroups, function (ug) { - return { group: ug, hasAccess: user.userGroups.indexOf(ug.alias) !== -1 || user.userGroups.indexOf("admin") !== -1} + ug.hasAccess = user.userGroups.indexOf(ug.alias) !== -1 || user.userGroups.indexOf("admin") !== -1; + return ug; }); vm.loading = false; @@ -42,36 +44,42 @@ // go to create user group $location.path('users/users/group/-1').search("create", "true");; } - - function goToUserGroup(userGroup) { + + function goToUserGroup(userGroup, $event) { + // only allow editing if user is member of the group or admin - if (currentUser.userGroups.indexOf(userGroup.group.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { + if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { return; } - $location.path('users/users/group/' + userGroup.group.id).search("create", null); + $location.path(getEditPath(userGroup)).search("create", null); + } + + function getEditPath(userGroup) { + + // only allow editing if user is member of the group or admin + if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { + return ""; + } + + return 'users/users/group/' + userGroup.id; } - function selectUserGroup(userGroup, selection, event) { - + function selectUserGroup(userGroup, $index, $event) { + + console.log(userGroup, $index); + // Only allow selection if user is member of the group or admin - if (currentUser.userGroups.indexOf(userGroup.group.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { + if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { return; } // Disallow selection of the admin/translators group, the checkbox is not visible in the UI, but clicking(and thus selecting) is still possible. // Currently selection can only be used for deleting, and the Controller will also disallow deleting the admin group. - if (userGroup.group.alias === "admin" || userGroup.group.alias === "translator") + if (userGroup.alias === "admin" || userGroup.alias === "translator") return; - - if (userGroup.selected) { - var index = selection.indexOf(userGroup.group.id); - selection.splice(index, 1); - userGroup.selected = false; - } else { - userGroup.selected = true; - vm.selection.push(userGroup.group.id); - } - - if(event){ + + listViewHelper.selectHandler(userGroup, $index, vm.userGroups, vm.selection, $event); + + if(event) { event.stopPropagation(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 863d9658ae..ddf2e5a802 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -70,38 +70,54 @@
    - - - - - - - - - +
    -
    - - - -
    Group
    - - - - -
    +
    +
    +
    +
    Name
    +
    Sections
    +
    Content start node
    +
    Media start node
    +
    Permissions
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + {{ section.name }}, + All sections +
    +
    + No start node selected + {{ group.contentStartNode.name }} +
    +
    + No start node selected + {{ group.mediaStartNode.name }} +
    +
    + {{ permission.name }}, +
    +
    +
    +
    From 7f61a6fe9f37a01aac736891e62368dc4ccf1886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 10:51:01 +0200 Subject: [PATCH 029/201] ability to open as a new tab --- .../views/users/views/groups/groups.controller.js | 12 +++++++++--- .../src/views/users/views/groups/groups.html | 10 +++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index 20f1e406b8..ccc18ac4f0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -9,8 +9,8 @@ vm.userGroups = []; vm.selection = []; + vm.clickUserGroupName = clickUserGroupName; vm.createUserGroup = createUserGroup; - vm.goToUserGroup = goToUserGroup; vm.clearSelection = clearSelection; vm.selectUserGroup = selectUserGroup; vm.deleteUserGroups = deleteUserGroups; @@ -54,6 +54,14 @@ $location.path(getEditPath(userGroup)).search("create", null); } + function clickUserGroupName(item, $event) { + if(!($event.metaKey || $event.ctrlKey)) { + goToUserGroup(item, $event); + $event.preventDefault(); + } + $event.stopPropagation(); + }; + function getEditPath(userGroup) { // only allow editing if user is member of the group or admin @@ -66,8 +74,6 @@ function selectUserGroup(userGroup, $index, $event) { - console.log(userGroup, $index); - // Only allow selection if user is member of the group or admin if (currentUser.userGroups.indexOf(userGroup.alias) === -1 && currentUser.userGroups.indexOf("admin") === -1) { return; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index ddf2e5a802..5e77becdf1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -87,16 +87,16 @@
    + ng-class="{'-selected': group.selected, '-selectable': group.hasAccess && group.alias !== 'admin' && group.alias !== 'translator'}">
    -
    - + From 17e9487bf0fb5025cd687c34d4b32ebaef2ad43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 11:18:15 +0200 Subject: [PATCH 030/201] remove end of none existing anchor element --- src/Umbraco.Web.UI.Client/src/views/components/umb-table.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index befdd8ee78..b1ca6a91c5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -12,7 +12,6 @@
    Name -
    Status From d2ff96e7ea85e4c1785ca7fd687ccaf1bd542805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 11:18:37 +0200 Subject: [PATCH 031/201] show checkmark icon when checked --- .../src/views/users/views/groups/groups.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 5e77becdf1..00253906dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -90,7 +90,8 @@ ng-class="{'-selected': group.selected, '-selectable': group.hasAccess && group.alias !== 'admin' && group.alias !== 'translator'}">
    - + +
    - Name - + Name +
    Status From db0a069b02810a75e26bd3857dbb785b887cefe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 12:40:15 +0200 Subject: [PATCH 033/201] move highlight css to umb-table-body__link --- .../src/less/components/umb-table.less | 9 +++++++-- .../src/views/users/views/groups/groups.html | 4 ++-- .../src/views/users/views/users/users.html | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 6674e01475..73a758310d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -136,9 +136,14 @@ input.umb-table__input { } .umb-table-body__link { + + color: @ui-option-type; + font-size: 14px; + font-weight: bold; text-decoration: none; - - &:hover { + + &:hover, &:focus { + color: @ui-option-type-hover; text-decoration: underline; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 00253906dc..6ad06926fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -75,7 +75,7 @@
    -
    Name
    +
    Name
    Sections
    Content start node
    Media start node
    @@ -93,7 +93,7 @@
    -
    +
    - + {{user.name}} From df46593ef3713b8e9aa8d3eb1ade8a9f12b19faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 15:00:00 +0200 Subject: [PATCH 034/201] improved list spacing, better text-overflow & center aligned select-all-checkbox for users list --- .../src/less/components/users/umb-user-table.less | 11 ++++------- .../src/views/users/views/groups/groups.html | 6 ++++-- .../src/views/users/views/users/users.html | 12 +++++------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less index bc85ae90a9..0c61a5d113 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-table.less @@ -3,6 +3,10 @@ .umb-user-table-col-avatar { flex: 0 0 32px; padding: 15px 0; + + .umb-checkmark { + margin-left:5px; + } } .umb-table-cell a { @@ -14,15 +18,8 @@ } .umb-table-body .umb-table-cell.umb-table__name { - margin: 0; - padding: 0; a { display: flex; - padding: 6px 2px; - height: 42px; - span { - margin: auto 14px; - } } } .umb-table-cell.umb-table__name a { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 6ad06926fc..cf0a7fbd4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -103,8 +103,10 @@
    - {{ section.name }}, - All sections +
    + {{ section.name }}, + All sections +
    No start node selected diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 746747e9a8..f328cf052e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -229,7 +229,7 @@
    -
    +
    @@ -245,7 +245,7 @@ ng-click="vm.selectUser(user, vm.selection, $event)" ng-class="{'-selected': user.selected, '-selectable': vm.isSelectable(user)}" class="umb-table-row umb-user-table-row"> -
    + -
    {{ userGroup.name }},
    +
    {{ userGroup.name }},
    {{ user.formattedLastLogin }}
    Date: Wed, 3 Apr 2019 15:11:36 +0200 Subject: [PATCH 035/201] removed permissions column --- .../src/views/users/views/groups/groups.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index cf0a7fbd4b..5e3542a30e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -79,7 +79,6 @@
    Sections
    Content start node
    Media start node
    -
    Permissions
    @@ -116,9 +115,6 @@ No start node selected {{ group.mediaStartNode.name }}
    -
    - {{ permission.name }}, -
    From bc821db8eb3b6bbdc74b2514da69c1a43b8a5caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 3 Apr 2019 15:11:55 +0200 Subject: [PATCH 036/201] less space for content-node and media-node --- .../src/less/components/umb-table.less | 9 +++++++++ .../src/views/users/views/groups/groups.html | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 73a758310d..f387b6540b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -264,6 +264,15 @@ input.umb-table__input { flex: 0 0 auto !important; } +.umb-table-cell--small { + flex: .5 .5 1%; + max-width: 12.5%; +} +.umb-table-cell--large { + flex: 1 1 25%; + max-width: 25%; +} + .umb-table-cell--faded { opacity: 0.4; } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 5e3542a30e..ba1d6bc9eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -77,8 +77,8 @@
    Name
    Sections
    -
    Content start node
    -
    Media start node
    +
    Content start node
    +
    Media start node
    @@ -107,11 +107,11 @@ All sections
    -
    +
    No start node selected {{ group.contentStartNode.name }}
    -
    +
    No start node selected {{ group.mediaStartNode.name }}
    From 63fb8f59330c5a34f62eb871c512acb24af2def5 Mon Sep 17 00:00:00 2001 From: Andrew Deans Date: Tue, 2 Apr 2019 23:00:08 -0400 Subject: [PATCH 037/201] Fix case of query parameter --- .../Repositories/Implement/ExternalLoginRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 7cb78f4c9f..0fa48e5521 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IIdentityUserLogin PerformGet(int id) { var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.Where(GetBaseWhereClause(), new { id = id }); var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (dto == null) From f985c437b5a55c614490eebf8323411b87c52558 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 29 Mar 2019 08:26:36 +0100 Subject: [PATCH 038/201] Add content.SetValue overload for posted files --- src/Umbraco.Core/ContentExtensions.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index f802c2d0d9..a7d40b0b7d 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -7,6 +7,7 @@ using System.Web; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco.Expressions; using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -52,8 +53,8 @@ namespace Umbraco.Core return ContentStatus.Unpublished; } - - + + #endregion /// @@ -134,9 +135,14 @@ namespace Umbraco.Core /// /// Sets the posted file value of a property. /// - /// This really is for FileUpload fields only, and should be obsoleted. For anything else, - /// you need to store the file by yourself using Store and then figure out - /// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself. + public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, HttpPostedFileBase postedFile, string culture = null, string segment = null) + { + content.SetValue(contentTypeBaseServiceProvider, propertyTypeAlias, postedFile.FileName, postedFile.InputStream, culture, segment); + } + + /// + /// Sets the posted file value of a property. + /// public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) { if (filename == null || filestream == null) return; From 30da32ee9407cd324342aa81d243e8f5eb6e9631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 4 Apr 2019 14:15:57 +0200 Subject: [PATCH 039/201] Visually more catching focus highlighting --- .../src/less/sections.less | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 6388369b51..f7e6e5ec79 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -11,7 +11,6 @@ ul.sections>li { display: flex; justify-content: center; align-items: center; - padding: 0 20px; position: relative; } @@ -22,28 +21,33 @@ ul.sections>li>a { align-items: center; justify-content: center; position: relative; + padding: 0 10px; text-decoration: none; outline: none; cursor: pointer; } ul.sections>li>a .section__name { + border-radius: 3px; + margin-top:1px; + padding: 3px 10px 4px 10px; opacity: 0.8; - transition: opacity .1s linear; + transition: opacity .1s linear, box-shadow .1s; } ul.sections>li>a::after { content: ""; + left: 10px; + right: 10px; height: 4px; - width: 100%; + bottom: 0; + transform: translateY(4px); background-color: @pinkLight; position: absolute; - left: 0; - bottom: -4px; border-radius: 3px 3px 0 0; opacity: 0; padding: 0 2px; - transition: all .2s linear; + transition: transform 240ms ease-in-out; } ul.sections>li.current>a { @@ -51,16 +55,23 @@ ul.sections>li.current>a { } ul.sections>li.current>a::after { opacity: 1; - bottom: 0; + transform: translateY(0px); } - ul.sections>li.current>a .section__name, -ul.sections>li>a:hover .section__name, +ul.sections>li>a:hover .section__name, ul.sections>li>a:focus .section__name { opacity: 1; -webkit-font-smoothing: subpixel-antialiased; } +ul.sections>li>a:focus .section__name { + box-shadow: 0 0 2px @pinkLight, inset 0 0 2px 1px @pinkLight; +} +ul.sections>li>a:hover .section__name, +ul.sections>li.current>a:focus .section__name { + box-shadow: none; +} + /* Sections tray */ From 75bba068a4a2acb53d2cbb83d08a0ed4f5b7174b Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Thu, 4 Apr 2019 16:39:53 +0200 Subject: [PATCH 040/201] Fix the colour of the error message in the domains overlay --- src/Umbraco.Web.UI.Client/src/less/forms.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 45188ca02e..1947270c9f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -533,6 +533,10 @@ div.help { } +table.domains .help-inline{ + color:@red; +} + // INPUT GROUPS // ------------ From 6e1a40e2ee8131a48431e469df2da3b51d9c505e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 4 Apr 2019 19:41:39 +0200 Subject: [PATCH 041/201] When creating partial views from templates, also replace (Model.Content) with (Model) - building upon the logic in e2d41106 --- src/Umbraco.Core/Services/Implement/FileService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/FileService.cs b/src/Umbraco.Core/Services/Implement/FileService.cs index 596d033ae8..79d5b35775 100644 --- a/src/Umbraco.Core/Services/Implement/FileService.cs +++ b/src/Umbraco.Core/Services/Implement/FileService.cs @@ -1034,10 +1034,12 @@ namespace Umbraco.Core.Services.Implement //strip the @inherits if it's there snippetContent = StripPartialViewHeader(snippetContent); - //Update Model.Content. to be Model. when used as PartialView + //Update Model.Content to be Model when used as PartialView if (partialViewType == PartialViewType.PartialView) { - snippetContent = snippetContent.Replace("Model.Content.", "Model."); + snippetContent = snippetContent + .Replace("Model.Content.", "Model.") + .Replace("(Model.Content)", "(Model)"); } var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}"; From b5796ad23763f4a333c90dce8a9348f6baf5214d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Apr 2019 10:44:36 +1100 Subject: [PATCH 042/201] Enable configuration of NuCache BTree block size #5114 --- .../NuCache/DataSource/BTree.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs index 1b17e0c124..910c0ca737 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs @@ -1,4 +1,5 @@ -using CSharpTest.Net.Collections; +using System.Configuration; +using CSharpTest.Net.Collections; using CSharpTest.Net.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource @@ -14,6 +15,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource CreateFile = exists ? CreatePolicy.IfNeeded : CreatePolicy.Always, FileName = filepath, + // read or write but do *not* keep in memory + CachePolicy = CachePolicy.None, + + // default is 4096, min 2^9 = 512, max 2^16 = 64K + FileBlockSize = GetBlockSize(), + // other options? }; @@ -25,6 +32,28 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return tree; } + private static int GetBlockSize() + { + var blockSize = 4096; + + var appSetting = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.BTree.BlockSize"]; + if (appSetting == null) + return blockSize; + + if (!int.TryParse(appSetting, out blockSize)) + throw new ConfigurationErrorsException($"Invalid block size value \"{appSetting}\": not a number."); + + var bit = 0; + for (var i = blockSize; i != 1; i >>= 1) + bit++; + if (1 << bit != blockSize) + throw new ConfigurationErrorsException($"Invalid block size value \"{blockSize}\": must be a power of two."); + if (blockSize < 512 || blockSize > 65536) + throw new ConfigurationErrorsException($"Invalid block size value \"{blockSize}\": must be >= 512 and <= 65536."); + + return blockSize; + } + /* class ListOfIntSerializer : ISerializer> { From e38095b72796221741e8d0d1c6269bfe216d66b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Apr 2019 10:49:19 +1100 Subject: [PATCH 043/201] NuCache: fix loading the media cache from local files, was missing media --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 2 ++ .../PublishedCache/NuCache/PublishedSnapshotService.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 7ab4a64f31..331ec37248 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -503,6 +503,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // IMPORTANT kits must be sorted out by LEVEL public void SetAll(IEnumerable kits) { var lockInfo = new WriteLockInfo(); @@ -533,6 +534,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // IMPORTANT kits must be sorted out by LEVEL public void SetBranch(int rootContentId, IEnumerable kits) { var lockInfo = new WriteLockInfo(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 9c5587fbd5..7fb51463bf 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -356,6 +356,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading content from database..."); var sw = Stopwatch.StartNew(); + // IMPORTANT GetAllContentSources sorts kits by level var kits = _dataSource.GetAllContentSources(scope); _contentStore.SetAll(kits); sw.Stop(); @@ -370,7 +371,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading content from local db..."); var sw = Stopwatch.StartNew(); - var kits = _localContentDb.Select(x => x.Value).OrderBy(x => x.Node.Level); + var kits = _localContentDb.Select(x => x.Value) + .OrderBy(x => x.Node.Level); // IMPORTANT sort by level _contentStore.SetAll(kits); sw.Stop(); _logger.Debug("Loaded content from local db ({Duration}ms)", sw.ElapsedMilliseconds); @@ -422,6 +424,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading media from database..."); var sw = Stopwatch.StartNew(); + // IMPORTANT GetAllMediaSources sorts kits by level var kits = _dataSource.GetAllMediaSources(scope); _mediaStore.SetAll(kits); sw.Stop(); @@ -436,7 +439,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading media from local db..."); var sw = Stopwatch.StartNew(); - var kits = _localMediaDb.Select(x => x.Value); + var kits = _localMediaDb.Select(x => x.Value) + .OrderBy(x => x.Node.Level); // IMPORTANT sort by level _mediaStore.SetAll(kits); sw.Stop(); _logger.Debug("Loaded media from local db ({Duration}ms)", sw.ElapsedMilliseconds); @@ -647,6 +651,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? + // IMPORTANT GetbranchContentSources sorts kits by level var kits = _dataSource.GetBranchContentSources(scope, capture.Id); _contentStore.SetBranch(capture.Id, kits); } @@ -738,6 +743,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (capture.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch)) { // ?? should we do some RV check here? + // IMPORTANT GetbranchContentSources sorts kits by level var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); _mediaStore.SetBranch(capture.Id, kits); } From c87a9d6bfaae936d8faca4f6d4d24a9dce270daf Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 5 Apr 2019 19:00:34 +0200 Subject: [PATCH 044/201] Fix composers ordering --- src/Umbraco.Core/Composing/Composers.cs | 134 +++++++++++++----- .../Components/ComponentTests.cs | 28 ++++ src/Umbraco.Web/Runtime/WebFinalComposer.cs | 2 + 3 files changed, 126 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs index 14cb0dce8e..b21879b055 100644 --- a/src/Umbraco.Core/Composing/Composers.cs +++ b/src/Umbraco.Core/Composing/Composers.cs @@ -70,7 +70,24 @@ namespace Umbraco.Core.Composing } } - private IEnumerable PrepareComposerTypes() + internal IEnumerable PrepareComposerTypes() + { + var requirements = GetRequirements(); + + // only for debugging, this is verbose + //_logger.Debug(GetComposersReport(requirements)); + Console.WriteLine(GetComposersReport(requirements)); + + var sortedComposerTypes = SortComposers(requirements); + + // bit verbose but should help for troubleshooting + //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; + _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + + return sortedComposerTypes; + } + + internal Dictionary> GetRequirements(bool throwOnMissing = true) { // create a list, remove those that cannot be enabled due to runtime level var composerTypeList = _composerTypes @@ -89,25 +106,69 @@ namespace Umbraco.Core.Composing // enable or disable composers EnableDisableComposers(composerTypeList); - // sort the composers according to their dependencies - var requirements = new Dictionary>(); - foreach (var type in composerTypeList) requirements[type] = null; - foreach (var type in composerTypeList) + void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute { - GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements); - GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements); + foreach (var attribute in type.GetCustomAttributes()) + { + var typeInAttribute = getTypeInAttribute(attribute); + if (typeInAttribute != null && // if the attribute references a type ... + typeInAttribute.IsInterface && // ... which is an interface ... + typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... + !iset.Contains(typeInAttribute)) // ... which is not already in the list + { + // add it to the new list + iset.Add(typeInAttribute); + set2.Add(typeInAttribute); + + // add all its interfaces implementing IComposer + foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) + { + iset.Add(i); + set2.Add(i); + } + } + } } - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); + // gather interfaces too + var interfaces = new HashSet(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); + composerTypeList.AddRange(interfaces); + var list1 = composerTypeList; + while (list1.Count > 0) + { + var list2 = new List(); + foreach (var t in list1) + { + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, list2); + } + composerTypeList.AddRange(list2); + list1 = list2; + } + // sort the composers according to their dependencies + var requirements = new Dictionary>(); + foreach (var type in composerTypeList) + requirements[type] = null; + foreach (var type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); + } + + return requirements; + } + + internal IEnumerable SortComposers(Dictionary> requirements) + { // sort composers var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value); graph.AddItems(requirements); List sortedComposerTypes; try { - sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList(); + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); } catch (Exception e) { @@ -117,40 +178,37 @@ namespace Umbraco.Core.Composing throw; } - // bit verbose but should help for troubleshooting - //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; - _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); - return sortedComposerTypes; } - private static string GetComposersReport(Dictionary> requirements) + internal static string GetComposersReport(Dictionary> requirements) { var text = new StringBuilder(); text.AppendLine("Composers & Dependencies:"); + text.AppendLine(" < compose before"); + text.AppendLine(" > compose after"); + text.AppendLine(" : implements"); + text.AppendLine(" = depends"); text.AppendLine(); + bool HasReq(IEnumerable types, Type type) + => types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); + foreach (var kvp in requirements) { var type = kvp.Key; text.AppendLine(type.FullName); foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); - foreach (var i in type.GetInterfaces()) { - text.AppendLine(" : " + i.FullName); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); + var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); + text.AppendLine(" > " + attribute.RequiredType + + (weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); } + foreach (var attribute in type.GetCustomAttributes()) + text.AppendLine(" < " + attribute.RequiringType); + foreach (var i in type.GetInterfaces()) + text.AppendLine(" : " + i.FullName); if (kvp.Value != null) foreach (var t in kvp.Value) text.AppendLine(" = " + t); @@ -221,16 +279,16 @@ namespace Umbraco.Core.Composing types.Remove(kvp.Key); } - private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary> requirements, bool throwOnMissing = true) { // get 'require' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requireAttributes = type + var afterAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. - foreach (var attr in requireAttributes) + foreach (var attr in afterAttributes) { if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) @@ -238,13 +296,13 @@ namespace Umbraco.Core.Composing // unless strong, and then require at least one enabled composer implementing that interface if (attr.RequiredType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList(); if (implems.Count > 0) { if (requirements[type] == null) requirements[type] = new List(); requirements[type].AddRange(implems); } - else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak + else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } // requiring a class = require that the composer is enabled @@ -256,28 +314,28 @@ namespace Umbraco.Core.Composing if (requirements[type] == null) requirements[type] = new List(); requirements[type].Add(attr.RequiredType); } - else if (attr.Weak != true) // if not explicitly set to weak, is strong + else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } } } - private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary> requirements) { // get 'required' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requiredAttributes = type + var beforeAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer - foreach (var attr in requiredAttributes) + foreach (var attr in beforeAttributes) { if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) // required by an interface = by any enabled composer implementing this that interface if (attr.RequiringType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); foreach (var implem in implems) { if (requirements[implem] == null) requirements[implem] = new List(); diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index c026e5a157..2ba94d8c78 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Compose; using Umbraco.Core.Composing; using Umbraco.Core.IO; @@ -299,11 +300,19 @@ namespace Umbraco.Tests.Components composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer2) }; composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer12) }; composers = new Composers(composition, types, Mock.Of()); @@ -349,6 +358,25 @@ namespace Umbraco.Tests.Components Assert.AreEqual(typeof(Composer27), Composed[1]); } + [Test] + public void AllComposers() + { + var typeLoader = new TypeLoader(AppCaches.Disabled.RuntimeCache, IOHelper.MapPath("~/App_Data/TEMP"), Mock.Of()); + + var register = MockRegister(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); + + var types = typeLoader.GetTypes().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web")); + var composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(); + var report = Composers.GetComposersReport(requirements); + Console.WriteLine(report); + var composerTypes = composers.SortComposers(requirements); + + foreach (var type in composerTypes) + Console.WriteLine(type); + } + #region Compothings public class TestComposerBase : IComposer diff --git a/src/Umbraco.Web/Runtime/WebFinalComposer.cs b/src/Umbraco.Web/Runtime/WebFinalComposer.cs index 64d7725848..c69ae1af1a 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComposer.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComposer.cs @@ -3,7 +3,9 @@ namespace Umbraco.Web.Runtime { // web's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] public class WebFinalComposer : ComponentComposer { } } From 2ee8f9dea2c3cb9ddc25db36094b03acdfd7e013 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 6 Apr 2019 08:00:44 +0200 Subject: [PATCH 045/201] Use the content type model when creating a "matching template" --- .../common/resources/contenttype.resource.js | 6 ++++ .../views/templates/templates.controller.js | 33 +++++++------------ .../Editors/ContentTypeController.cs | 17 ++++++++++ 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 86867ccff9..4e4c8d2eb5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -344,6 +344,12 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "Import", { file: file })), "Failed to import document type " + file ); + }, + + createDefaultTemplate: function (id) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateDefaultTemplate", { id: id })), + 'Failed to create default template for content type with id ' + id); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js index 1d825fdf26..a147da3afb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js @@ -9,7 +9,7 @@ (function () { 'use strict'; - function TemplatesController($scope, entityResource, contentTypeHelper, templateResource, $routeParams) { + function TemplatesController($scope, entityResource, contentTypeHelper, templateResource, contentTypeResource, $routeParams) { /* ----------- SCOPE VARIABLES ----------- */ @@ -48,34 +48,25 @@ vm.createTemplateButtonState = "busy"; - templateResource.getScaffold(-1).then(function (template) { - - template.alias = $scope.model.alias; - template.name = $scope.model.name; - - templateResource.save(template).then(function (savedTemplate) { - - // add icon - savedTemplate.icon = "icon-layout"; + contentTypeResource.createDefaultTemplate($scope.model.id).then(function (savedTemplate) { + // add icon + savedTemplate.icon = "icon-layout"; - vm.availableTemplates.push(savedTemplate); - vm.canCreateTemplate = false; + vm.availableTemplates.push(savedTemplate); + vm.canCreateTemplate = false; - $scope.model.allowedTemplates.push(savedTemplate); + $scope.model.allowedTemplates.push(savedTemplate); - if ($scope.model.defaultTemplate === null) { - $scope.model.defaultTemplate = savedTemplate; - } + if ($scope.model.defaultTemplate === null) { + $scope.model.defaultTemplate = savedTemplate; + } - vm.createTemplateButtonState = "success"; - - }, function() { - vm.createTemplateButtonState = "error"; - }); + vm.createTemplateButtonState = "success"; }, function() { vm.createTemplateButtonState = "error"; }); + }; function checkIfTemplateExists() { diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index ede480e29a..f95a4da30e 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -322,6 +322,23 @@ namespace Umbraco.Web.Editors return display; } + public TemplateDisplay PostCreateDefaultTemplate(int id) + { + var contentType = Services.ContentTypeService.Get(id); + if (contentType == null) + { + throw new NullReferenceException("No content type found with id " + id); + } + + var template = CreateTemplateForContentType(contentType.Alias, contentType.Name); + if (template == null) + { + throw new NullReferenceException("Could not create default template for content type with id " + id); + } + + return Mapper.Map(template); + } + private ITemplate CreateTemplateForContentType(string contentTypeAlias, string contentTypeName) { var template = Services.FileService.GetTemplate(contentTypeAlias); From 1c9b449c46d3327f7be8d128cb2026ba2cb9e494 Mon Sep 17 00:00:00 2001 From: Stephan Date: Sun, 7 Apr 2019 16:24:35 +0200 Subject: [PATCH 046/201] Log more details about boot environment --- src/Umbraco.Core/Runtime/CoreRuntime.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index f00365496a..9c33f1fcfe 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Web; +using System.Web.Hosting; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; @@ -70,10 +71,15 @@ namespace Umbraco.Core.Runtime // objects. using (var timer = profilingLogger.TraceDuration( - $"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()} on {NetworkHelper.MachineName}.", + $"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()}.", "Booted.", "Boot failed.")) { + logger.Info("Booting site '{HostingSiteName}', app '{HostingApplicationID}', path '{HostingPhysicalPath}', server '{MachineName}'.", + HostingEnvironment.SiteName, + HostingEnvironment.ApplicationID, + HostingEnvironment.ApplicationPhysicalPath, + NetworkHelper.MachineName); logger.Debug("Runtime: {Runtime}", GetType().FullName); // application environment From 1aec0e1d9c19ec21a18e99cff863ab7368c33a58 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 8 Apr 2019 07:24:56 +0200 Subject: [PATCH 047/201] Fix exception types as per review --- src/Umbraco.Web/Editors/ContentTypeController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index f95a4da30e..e2a1e54571 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -327,13 +327,13 @@ namespace Umbraco.Web.Editors var contentType = Services.ContentTypeService.Get(id); if (contentType == null) { - throw new NullReferenceException("No content type found with id " + id); + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound, "No content type found with id " + id)); } var template = CreateTemplateForContentType(contentType.Alias, contentType.Name); if (template == null) { - throw new NullReferenceException("Could not create default template for content type with id " + id); + throw new InvalidOperationException("Could not create default template for content type with id " + id); } return Mapper.Map(template); From 7e1ee26dee591d1ea9faf6166e8cbdab694c2cad Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 9 Apr 2019 10:34:59 +1000 Subject: [PATCH 048/201] Fix spelling mistake --- src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 93c2ce05ed..4767f6fdcc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -250,7 +250,7 @@
    Compatibility
    -
    This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100%
    +
    This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100%
    {{compatibility.version}} diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 1840883c34..5e0d959b61 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1164,7 +1164,7 @@ To manage your website, simply open the Umbraco back office and start adding con Downloads Likes Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% External sources Author Documentation 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 8700daff03..92780b304e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1163,7 +1163,7 @@ To manage your website, simply open the Umbraco back office and start adding con Downloads Likes Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be gauranteed for versions reported below 100% + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% External sources Author Documentation From 699c34263ed0024ed48d9133077ca3e3cebaab30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 9 Apr 2019 16:42:24 +0200 Subject: [PATCH 049/201] bigger area for add-property + only hover-state if you can edit property --- .../less/components/umb-group-builder.less | 47 +++++++++++++++++-- .../views/components/umb-groups-builder.html | 10 ++-- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index d8c7224d57..dc862d7bc3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -135,6 +135,28 @@ input.umb-group-builder__group-title-input:disabled:hover { margin-left: auto; } +.umb-group-builder__group-add-property { + min-height: 46px; + margin-right: 30px; + margin-left: 270px; + border-radius: 3px; + + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + border: 1px dashed @gray-7; + background-color: transparent; + color: @ui-action-type; + font-weight: bold; + position: relative; + &:hover { + color:@ui-action-type-hover; + text-decoration: none; + border-color: @ui-active-type-hover; + } +} + /* ---------- PROPERTIES ---------- */ .umb-group-builder__properties { @@ -270,7 +292,7 @@ input.umb-group-builder__group-title-input:disabled:hover { } } -.umb-group-builder__property-preview:hover { +.umb-group-builder__property-preview:not(.-not-clickable):hover { &::after { opacity: .8; } @@ -301,14 +323,29 @@ input.umb-group-builder__group-title-input:disabled:hover { opacity: 0.8 } + +.umb-group-builder__open-settings { + position: absolute; + z-index:1; + top: 0; + bottom:0; + left: 0; + width: 100%; + background-color: transparent; + border: none; + &:focus { + outline:0; + } +} + .umb-group-builder__property-actions { - flex: 0 0 40px; - text-align: center; - margin: 15px 0 0 15px; + flex: 0 0 30px; + text-align: right; + margin-top: 9px; } .umb-group-builder__property-action { - margin: 0 0 10px 0; + margin: 10px 0 10px 0; display: block; font-size: 18px; position: relative; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 4d649a7fab..eefeb73084 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -116,7 +116,7 @@ -
    +
    @@ -234,7 +234,7 @@
    - + + +
    -
    +
    From e540a4ac2309bfb669ca5982f2ac0ce0e0d19423 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 9 Apr 2019 21:24:55 +0200 Subject: [PATCH 050/201] IsVisible moves from publishde content to element --- src/Umbraco.Web/PublishedContentExtensions.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index bba58dfae5..54afb7abbd 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -298,23 +298,6 @@ namespace Umbraco.Web #region IsSomething: misc. - /// - /// Gets a value indicating whether the content is visible. - /// - /// The content. - /// A value indicating whether the content is visible. - /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, - /// the content is visible. - public static bool IsVisible(this IPublishedContent content) - { - // note: would be better to ensure we have an IPropertyEditorValueConverter for booleans - // and then treat the umbracoNaviHide property as a boolean - vs. the hard-coded "1". - - // rely on the property converter - will return default bool value, ie false, if property - // is not defined, or has no value, else will return its value. - return content.Value(Constants.Conventions.Content.NaviHide) == false; - } - /// /// Determines whether the specified content is a specified content type. /// From d2e04614c6b60b91df24b9b15565a86aa705725e Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 5 Apr 2019 20:55:38 +0200 Subject: [PATCH 051/201] Added UDI support for GetById and fixed bug not using specified preview mode --- .../ContextualPublishedCache.cs | 166 +++++++++++++----- 1 file changed, 119 insertions(+), 47 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs index 429e2a2f9e..15a931957c 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Xml; @@ -13,14 +12,10 @@ namespace Umbraco.Web.PublishedCache ///
    public abstract class ContextualPublishedCache { - //TODO: We need to add: - //* GetById(Guid contentId) - //* GetById(UDI contentId) - protected readonly UmbracoContext UmbracoContext; /// - /// Initializes a new instance of the with a context. + /// Initializes a new instance of the with a context. /// /// The context. protected ContextualPublishedCache(UmbracoContext umbracoContext) @@ -32,8 +27,12 @@ namespace Umbraco.Web.PublishedCache /// Gets a content identified by its unique identifier. ///
    /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on context. + /// + /// The content, or null. + /// + /// + /// Considers published or unpublished content depending on context. + /// public IPublishedContent GetById(int contentId) { return GetById(UmbracoContext.InPreviewMode, contentId); @@ -43,35 +42,84 @@ namespace Umbraco.Web.PublishedCache /// Gets a content identified by its unique identifier. ///
    /// The content unique identifier. - /// The content, or null. - /// Considers published or unpublished content depending on context. + /// + /// The content, or null. + /// + /// + /// Considers published or unpublished content depending on context. + /// public IPublishedContent GetById(Guid contentId) { return GetById(UmbracoContext.InPreviewMode, contentId); } + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// + /// The content, or null. + /// + /// + /// Considers published or unpublished content depending on context. + /// + public IPublishedContent GetById(Udi contentId) + { + return GetById(UmbracoContext.InPreviewMode, contentId); + } + /// /// Gets a content identified by its unique identifier. /// /// A value indicating whether to consider unpublished content. /// The content unique identifier. - /// The content, or null. + /// + /// The content, or null. + /// public abstract IPublishedContent GetById(bool preview, int contentId); - // same with Guid - // cannot make this public nor abstract without breaking backward compatibility + /// + /// Gets a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content key. + /// + /// The content, or null. + /// public virtual IPublishedContent GetById(bool preview, Guid contentKey) { - // original implementation - override in concrete classes - var intId = UmbracoContext.Application.Services.EntityService.GetIdForKey(contentKey, UmbracoObjectTypes.Document); - return GetById(intId.Success ? intId.Result : -1); + var contentIdAttempt = UmbracoContext.Application.Services.EntityService.GetIdForKey(contentKey, UmbracoObjectTypes.Document); + + return GetById(preview, contentIdAttempt.Success ? contentIdAttempt.Result : -1); + } + + /// + /// Gets a content identified by its unique identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content identifier. + /// + /// The content, or null. + /// + /// UDIs for content items must be + public virtual IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("UDIs for content items must be " + typeof(GuidUdi)); + + return GetById(preview, guidUdi.Guid); } /// /// Gets content at root. /// - /// The contents. - /// Considers published or unpublished content depending on context. + /// + /// The contents. + /// + /// + /// Considers published or unpublished content depending on context. + /// public IEnumerable GetAtRoot() { return GetAtRoot(UmbracoContext.InPreviewMode); @@ -81,7 +129,9 @@ namespace Umbraco.Web.PublishedCache /// Gets contents at root. ///
    /// A value indicating whether to consider unpublished content. - /// The contents. + /// + /// The contents. + /// public abstract IEnumerable GetAtRoot(bool preview); /// @@ -89,10 +139,11 @@ namespace Umbraco.Web.PublishedCache /// /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// Considers published or unpublished content depending on context. /// @@ -106,10 +157,11 @@ namespace Umbraco.Web.PublishedCache ///
    /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// Considers published or unpublished content depending on context. /// @@ -124,10 +176,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IPublishedContent GetSingleByXPath(bool preview, string xpath, params XPathVariable[] vars); @@ -138,10 +191,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The content, or null. + /// + /// The content, or null. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); @@ -151,7 +205,9 @@ namespace Umbraco.Web.PublishedCache /// /// The XPath query. /// Optional XPath variables. - /// The contents. + /// + /// The contents. + /// /// /// If is null, or is empty, or contains only one single /// value which itself is null, then variables are ignored. @@ -186,10 +242,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The contents. + /// + /// The contents. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IEnumerable GetByXPath(bool preview, string xpath, params XPathVariable[] vars); @@ -200,10 +257,11 @@ namespace Umbraco.Web.PublishedCache /// A value indicating whether to consider unpublished content. /// The XPath query. /// Optional XPath variables. - /// The contents. + /// + /// The contents. + /// /// - /// If is null, or is empty, or contains only one single - /// value which itself is null, then variables are ignored. + /// If is null, or is empty, or contains only one single value which itself is null, then variables are ignored. /// The XPath expression should reference variables as $var. /// public abstract IEnumerable GetByXPath(bool preview, XPathExpression xpath, params XPathVariable[] vars); @@ -211,8 +269,12 @@ namespace Umbraco.Web.PublishedCache /// /// Gets an XPath navigator that can be used to navigate content. /// - /// The XPath navigator. - /// Considers published or unpublished content depending on context. + /// + /// The XPath navigator. + /// + /// + /// Considers published or unpublished content depending on context. + /// public XPathNavigator GetXPathNavigator() { return GetXPathNavigator(UmbracoContext.InPreviewMode); @@ -222,20 +284,28 @@ namespace Umbraco.Web.PublishedCache /// Gets an XPath navigator that can be used to navigate content. /// /// A value indicating whether to consider unpublished content. - /// The XPath navigator. + /// + /// The XPath navigator. + /// public abstract XPathNavigator GetXPathNavigator(bool preview); /// - /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator - /// and that navigator is a NavigableNavigator. + /// Gets a value indicating whether GetXPathNavigator returns an XPathNavigator and that navigator is a NavigableNavigator. /// + /// + /// true if the XPathNavigator is navigable; otherwise, false. + /// public abstract bool XPathNavigatorIsNavigable { get; } /// /// Gets a value indicating whether the underlying non-contextual cache contains content. /// - /// A value indicating whether the underlying non-contextual cache contains content. - /// Considers published or unpublished content depending on context. + /// + /// A value indicating whether the underlying non-contextual cache contains content. + /// + /// + /// Considers published or unpublished content depending on context. + /// public bool HasContent() { return HasContent(UmbracoContext.InPreviewMode); @@ -245,7 +315,9 @@ namespace Umbraco.Web.PublishedCache /// Gets a value indicating whether the underlying non-contextual cache contains content. /// /// A value indicating whether to consider unpublished content. - /// A value indicating whether the underlying non-contextual cache contains content. + /// + /// A value indicating whether the underlying non-contextual cache contains content. + /// public abstract bool HasContent(bool preview); } } From be7f42c454adf437692071c6bb990ed882b08bb9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 10 Apr 2019 09:52:06 +1000 Subject: [PATCH 052/201] Don't show the recycle bin in tree pickers/dialogs --- src/Umbraco.Web/Trees/TreeControllerBase.cs | 2 +- src/Umbraco.Web/Trees/TreeQueryStringParameters.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 4e43e6c093..a815364982 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -366,7 +366,7 @@ namespace Umbraco.Web.Trees /// protected bool IsDialog(FormDataCollection queryStrings) { - return queryStrings.GetValue(TreeQueryStringParameters.IsDialog); + return queryStrings.GetValue(TreeQueryStringParameters.Use) == "dialog"; } /// diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 205871ca20..466aff5a1f 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -5,7 +5,7 @@ /// internal struct TreeQueryStringParameters { - public const string IsDialog = "isDialog"; + public const string Use = "use"; public const string Application = "application"; public const string StartNodeId = "startNodeId"; //public const string OnNodeClick = "OnNodeClick"; From 5fbd455fd8a90f33ee004d480001226e007895b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 10 Apr 2019 10:41:58 +0200 Subject: [PATCH 053/201] align description with title --- src/Umbraco.Web.UI.Client/src/less/components/editor.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index aa3b83ed6c..9d0e0d93a1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -90,6 +90,9 @@ .umb-editor-header__name-and-description { margin-right: 20px; + .umb-panel-header-description { + padding: 0 10px; + } } .-split-view-active .umb-editor-header__name-and-description { From 1fcce288da300b532369fc7cec34c287a7c2acc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 10 Apr 2019 10:42:21 +0200 Subject: [PATCH 054/201] changed action into buttons for keyboard focus --- .../views/components/umb-groups-builder.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index eefeb73084..37b09ca005 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -234,12 +234,12 @@ - - - + + + @@ -252,13 +252,13 @@
    -
    - +
    +
    - + Date: Wed, 10 Apr 2019 10:42:29 +0200 Subject: [PATCH 055/201] style updates --- .../less/components/umb-group-builder.less | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index dc862d7bc3..e2a35c5fb5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -75,7 +75,7 @@ display: flex; align-items: center; border-bottom: 1px solid @gray-9; - padding: 10px 15px; + padding: 10px 15px 10px 10px; } .umb-group-builder__group-title { @@ -112,7 +112,7 @@ input.umb-group-builder__group-title-input { } input.umb-group-builder__group-title-input:disabled:hover { - border: none; + border-color: transparent; } .umb-group-builder__group-title-input:hover { @@ -163,6 +163,7 @@ input.umb-group-builder__group-title-input:disabled:hover { list-style: none; margin: 0; padding: 15px; + padding-right: 5px; min-height: 35px; // the height of a sortable property } @@ -250,6 +251,9 @@ input.umb-group-builder__group-title-input:disabled:hover { overflow: hidden; border-color: transparent; background: transparent; + &:focus { + border-color: @inputBorderFocus; + } } .umb-group-builder__property-meta-label textarea.ng-invalid { @@ -269,6 +273,9 @@ input.umb-group-builder__group-title-input:disabled:hover { overflow: hidden; border-color: transparent; background: transparent; + &:focus { + border-color: @inputBorderFocus; + } } .umb-group-builder__property-preview { @@ -287,21 +294,17 @@ input.umb-group-builder__group-title-input:disabled:hover { bottom: 0; left: 0; right: 0; - background: rgba(0,0,0,0.1); + background: rgba(225,225,225,.5); transition: opacity 120ms; } } .umb-group-builder__property-preview:not(.-not-clickable):hover { &::after { - opacity: .8; + opacity: .66; } } -.umb-group-builder__property-preview:focus { - outline: none; -} - .umb-group-builder__property-preview.-not-clickable { cursor: auto; } @@ -335,26 +338,42 @@ input.umb-group-builder__group-title-input:disabled:hover { border: none; &:focus { outline:0; + border: 1px solid @inputBorderFocus; } } .umb-group-builder__property-actions { - flex: 0 0 30px; + flex: 0 0 44px; text-align: right; - margin-top: 9px; + margin-top: 7px; } .umb-group-builder__property-action { - margin: 10px 0 10px 0; - display: block; - font-size: 18px; - position: relative; - cursor: pointer; - color: @ui-icon; -} - -.umb-group-builder__property-action:hover { - color: @ui-icon-hover; + + position: relative; + margin: 5px 0; + + > button { + border: none; + + font-size: 18px; + position: relative; + cursor: pointer; + color: @ui-icon; + + margin: 0; + padding: 5px 10px; + width: auto; + overflow: visible; + background: transparent; + line-height: normal; + outline: 0; + -webkit-appearance: none; + + &:hover, &:focus { + color: @ui-icon-hover; + } + } } .umb-group-builder__property-tags { @@ -476,7 +495,7 @@ input.umb-group-builder__group-sort-value { font-size: 14px; color: @ui-action-type; - &:hover { + &:hover{ text-decoration: none; color:@ui-action-type-hover; border-color:@ui-action-border-hover; From 43759fbef34a278598f76b14aaf7e794da4e73cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 10 Apr 2019 12:10:01 +0200 Subject: [PATCH 056/201] applied icons again --- .../less/components/buttons/umb-toggle.less | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) 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 5f9e5b58ee..6f23677a1c 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 @@ -14,9 +14,9 @@ cursor: pointer; align-items: center; display: flex; - width: 48px; - height: 24px; - border-radius: 3px; + width: 38px; + height: 18px; + border-radius: 10px; border: 1px solid @inputBorder; background-color: @inputBorder; position: relative; @@ -49,14 +49,14 @@ top: 1px; left: 1px; display: block; - width: 22px; - height: 22px; + width: 16px; + height: 16px; background-color: @white; - border-radius: 2px; + border-radius: 8px; transition: transform 120ms ease-in-out, background-color 120ms; .umb-toggle.umb-toggle--checked & { - transform: translateX(24px); + transform: translateX(20px); background-color: white; } @@ -67,20 +67,22 @@ .umb-toggle__icon { position: absolute; + font-size: 12px; line-height: 1em; text-decoration: none; transition: all 0.2s ease; } .umb-toggle__icon--left { - left: 6px; - color: @ui-btn; - transition: color 120ms; + left: 5px; + color: white; + transition: opacity 120ms; + opacity: 0; // .umb-toggle:hover & { // color: @ui-btn-hover; // } .umb-toggle--checked & { - color: white; + opacity: 1; } .umb-toggle.umb-toggle--checked:hover & { color: white; @@ -90,6 +92,10 @@ .umb-toggle__icon--right { right: 5px; color: @ui-btn; + transition: opacity 120ms; + .umb-toggle--checked & { + opacity: 0; + } .umb-toggle:hover & { color: @ui-btn-hover; } From ed8ba3ddcf78369056f469d0ef163441a251ec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 10 Apr 2019 13:40:06 +0200 Subject: [PATCH 057/201] changed inline-styling into a appearance property --- .../editor/subheader/umbeditorsubheader.directive.js | 3 +++ .../components/editor/subheader/umb-editor-sub-header.less | 4 ++++ .../components/editor/subheader/umb-editor-sub-header.html | 2 +- .../src/views/propertyeditors/grid/grid.html | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheader.directive.js index 241bcc26ea..95a1416498 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheader.directive.js @@ -47,6 +47,9 @@ The sub header is sticky and will follow along down the page when scrolling. transclude: true, restrict: 'E', replace: true, + scope: { + "appearance": "@?" + }, templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html' }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 2e599252bb..515efdfa00 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -19,6 +19,10 @@ background: @brownGrayLight; } } +.umb-editor-sub-header--white { + background-color: white; + border-color: white; +} .umb-editor-sub-header.--state-selection { padding-left: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html index 5a8e38c47c..c385223baf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html @@ -1,5 +1,5 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index ca22e052a7..3afeeef27d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -1,6 +1,6 @@
    - + Date: Wed, 10 Apr 2019 15:31:55 +0200 Subject: [PATCH 058/201] V8: Fix the layout for editing datatypes in infinite edit mode (#5194) ### Prerequisites - [x] I have added steps to test this contribution in the description below If there's an existing issue for this PR then this fixes https://github.com/umbraco/Umbraco-CMS/issues/5154 ### Description This PR fixes the layout of the datatype editor when editing datatypes in infinite editing mode (see #5154 for details). When applied the editor behaves like this: ![dataype-infinite-editing](https://user-images.githubusercontent.com/7405322/55711636-5df57780-59ed-11e9-958d-0deb05cb17bf.gif) --- .../infiniteeditors/datatypesettings/datatypesettings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index 620f9f1731..3faf74fdef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -11,7 +11,7 @@ hide-description="true"> - + From 179afe7e0d1e716db47ec3b50471c7206eb967b1 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 21 Mar 2019 15:01:46 +0100 Subject: [PATCH 059/201] Add IsVisible extension on IPublishedElement --- src/Umbraco.Web/PublishedElementExtensions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index f2a49f7f60..de7c72d21a 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -170,5 +170,23 @@ namespace Umbraco.Web } #endregion + + #region IsSomething + + /// + /// Gets a value indicating whether the content is visible. + /// + /// The content. + /// A value indicating whether the content is visible. + /// A content is not visible if it has an umbracoNaviHide property with a value of "1". Otherwise, + /// the content is visible. + public static bool IsVisible(this IPublishedElement content) + { + // rely on the property converter - will return default bool value, ie false, if property + // is not defined, or has no value, else will return its value. + return content.Value(Constants.Conventions.Content.NaviHide) == false; + } + + #endregion } } From 8b509656cd7afdfd5eb963b002ea2a763b3b0123 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 11 Apr 2019 18:55:40 +0200 Subject: [PATCH 060/201] NuCache: fix vanishing content when refreshing content types --- .../PublishedCache/NuCache/ContentStore.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 331ec37248..48c68ab9bf 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -313,6 +313,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var removedContentTypeNodes = new List(); var refreshedContentTypeNodes = new List(); + // find all the nodes that are either refreshed or removed, + // because of their content type being either refreshed or removed foreach (var link in _contentNodes.Values) { var node = link.Value; @@ -322,39 +324,49 @@ namespace Umbraco.Web.PublishedCache.NuCache if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); } - // all content should have been deleted - but + // perform deletion of content with removed content type + // removing content types should have removed their content already + // but just to be 100% sure, clear again here foreach (var node in removedContentTypeNodes) ClearBranchLocked(node); + // perform deletion of removed content types foreach (var id in removedIdsA) { - if (_contentTypesById.TryGetValue(id, out LinkedNode link) == false || link.Value == null) + if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) continue; SetValueLocked(_contentTypesById, id, null); SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); } + // perform update of refreshed content types foreach (var type in refreshedTypesA) { SetValueLocked(_contentTypesById, type.Id, type); SetValueLocked(_contentTypesByAlias, type.Alias, type); } - // skip missing type - // skip missing parents & unbuildable kits - what else could we do? + // perform update of content with refreshed content type - from the kits + // skip missing type, skip missing parents & unbuildable kits - what else could we do? + // kits are ordered by level, so ParentExits is ok here var visited = new List(); foreach (var kit in kits.Where(x => refreshedIdsA.Contains(x.ContentTypeId) && ParentExistsLocked(x) && BuildKit(x))) { + // replacing the node: must preserve the parents + var node = GetHead(_contentNodes, kit.Node.Id)?.Value; + if (node != null) + kit.Node.ChildContentIds = node.ChildContentIds; + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + visited.Add(kit.Node.Id); if (_localDb != null) RegisterChange(kit.Node.Id, kit); } // all content should have been refreshed - but... - var orphans = refreshedContentTypeNodes.Except(visited); foreach (var id in orphans) ClearBranchLocked(id); From ebe4c1b3c3dc8351cda279ec92d4d4bfc93faed3 Mon Sep 17 00:00:00 2001 From: elitsa Date: Fri, 12 Apr 2019 08:56:18 +0200 Subject: [PATCH 061/201] Fixing formatting --- src/Umbraco.Web.UI.Client/src/less/forms.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 1947270c9f..dc63baf335 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -533,7 +533,7 @@ div.help { } -table.domains .help-inline{ +table.domains .help-inline { color:@red; } From 14f9445ffb1c0ec8950b67ad2ff7d80f4d78e292 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 12 Apr 2019 15:34:06 +0200 Subject: [PATCH 062/201] Ensure entities type in content caches GetById(Udi) --- .../PublishedCache/ContextualPublishedCache.cs | 10 +++++++++- .../PublishedCache/ContextualPublishedContentCache.cs | 3 +++ .../PublishedCache/ContextualPublishedMediaCache.cs | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs index 15a931957c..8cbb0bb0fc 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs @@ -106,11 +106,19 @@ namespace Umbraco.Web.PublishedCache { var guidUdi = contentId as GuidUdi; if (guidUdi == null) - throw new InvalidOperationException("UDIs for content items must be " + typeof(GuidUdi)); + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != UdiEntityType) + throw new ArgumentException($"Udi entity type must be \"{UdiEntityType}\".", nameof(contentId)); return GetById(preview, guidUdi.Guid); } + /// + /// Gets the entity type. + /// + protected abstract string UdiEntityType { get; } + /// /// Gets content at root. /// diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs index c927644817..4475f6893e 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.PublishedCache.XmlPublishedCache; @@ -21,6 +22,8 @@ namespace Umbraco.Web.PublishedCache : base(umbracoContext, cache) { } + protected override string UdiEntityType => Constants.UdiEntityType.Document; + public override IPublishedContent GetById(bool preview, Guid contentKey) { if (InnerCache is PublishedContentCache cc) diff --git a/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs index 0b4ccc18c4..6ab50f14fa 100644 --- a/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.PublishedCache.XmlPublishedCache; @@ -21,6 +22,8 @@ namespace Umbraco.Web.PublishedCache : base(umbracoContext, cache) { } + protected override string UdiEntityType => Constants.UdiEntityType.Media; + public override IPublishedContent GetById(bool preview, Guid contentKey) { if (InnerCache is PublishedMediaCache cc) From d4ba1298d59673f4c93c1f615db47262c541a829 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 12 Apr 2019 16:05:43 +0200 Subject: [PATCH 063/201] Add GetById(Udi) to published caches --- .../PublishedContentCache.cs | 3 +++ .../PublishedMediaCache.cs | 3 +++ .../PublishedContent/SolidPublishedSnapshot.cs | 3 +++ .../PublishedCache/IPublishedCache.cs | 18 ++++++++++++++++++ .../PublishedCache/NuCache/ContentCache.cs | 12 ++++++++++++ .../PublishedCache/NuCache/MediaCache.cs | 15 +++++++++++++++ .../PublishedCache/PublishedCacheBase.cs | 18 +++++++++--------- 7 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs index 5fc0d628c9..2a144f3aaa 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs @@ -381,6 +381,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache } } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return GetXml(preview).CreateNavigator().MoveToId(contentId.ToString(CultureInfo.InvariantCulture)); diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs index 71490465d0..0c7ee98c6d 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs @@ -97,6 +97,9 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache throw new NotImplementedException(); } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return GetUmbracoMedia(contentId) != null; diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 86017be820..9828a14597 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -92,6 +92,9 @@ namespace Umbraco.Tests.PublishedContent throw new NotImplementedException(); } + public override IPublishedContent GetById(bool preview, Udi nodeId) + => throw new NotSupportedException(); + public override bool HasById(bool preview, int contentId) { return _content.ContainsKey(contentId); diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index ff459a2d9b..3cd7b924fb 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -29,6 +30,15 @@ namespace Umbraco.Web.PublishedCache /// The value of overrides defaults. IPublishedContent GetById(bool preview, Guid contentId); + /// + /// Gets a content identified by its Udi identifier. + /// + /// A value indicating whether to consider unpublished content. + /// The content Udi identifier. + /// The content, or null. + /// The value of overrides defaults. + IPublishedContent GetById(bool preview, Udi contentId); + /// /// Gets a content identified by its unique identifier. /// @@ -45,6 +55,14 @@ namespace Umbraco.Web.PublishedCache /// Considers published or unpublished content depending on defaults. IPublishedContent GetById(Guid contentId); + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + IPublishedContent GetById(Udi contentId); + /// /// Gets a value indicating whether the cache contains a specified content. /// diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 0e74ea919f..4bd3fcf247 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -238,6 +238,18 @@ namespace Umbraco.Web.PublishedCache.NuCache return GetNodePublishedContent(node, preview); } + public override IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != Constants.UdiEntityType.Document) + throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Document}\".", nameof(contentId)); + + return GetById(preview, guidUdi.Guid); + } + public override bool HasById(bool preview, int contentId) { var n = _snapshot.Get(contentId); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs index f7bdb4400f..112ccd9931 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -44,6 +45,20 @@ namespace Umbraco.Web.PublishedCache.NuCache return n?.PublishedModel; } + public override IPublishedContent GetById(bool preview, Udi contentId) + { + var guidUdi = contentId as GuidUdi; + if (guidUdi == null) + throw new ArgumentException($"Udi must be of type {typeof(GuidUdi).Name}.", nameof(contentId)); + + if (guidUdi.EntityType != Constants.UdiEntityType.Media) + throw new ArgumentException($"Udi entity type must be \"{Constants.UdiEntityType.Media}\".", nameof(contentId)); + + // ignore preview, there's only draft for media + var n = _snapshot.Get(guidUdi.Guid); + return n?.PublishedModel; + } + public override bool HasById(bool preview, int contentId) { var n = _snapshot.Get(contentId); diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs index b0fe1a4240..b88ae26704 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Xml; @@ -19,23 +20,22 @@ namespace Umbraco.Web.PublishedCache public abstract IPublishedContent GetById(bool preview, int contentId); public IPublishedContent GetById(int contentId) - { - return GetById(PreviewDefault, contentId); - } + => GetById(PreviewDefault, contentId); public abstract IPublishedContent GetById(bool preview, Guid contentId); public IPublishedContent GetById(Guid contentId) - { - return GetById(PreviewDefault, contentId); - } + => GetById(PreviewDefault, contentId); + + public abstract IPublishedContent GetById(bool preview, Udi contentId); + + public IPublishedContent GetById(Udi contentId) + => GetById(PreviewDefault, contentId); public abstract bool HasById(bool preview, int contentId); public bool HasById(int contentId) - { - return HasById(PreviewDefault, contentId); - } + => HasById(PreviewDefault, contentId); public abstract IEnumerable GetAtRoot(bool preview); From aa75fcfdb35f2b8e2a6f3deba5c2dd8535133428 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 12 Apr 2019 17:40:10 +0200 Subject: [PATCH 064/201] Fix bad local temp path --- src/Umbraco.Core/Configuration/GlobalSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 94deb28d87..49f4481a59 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -315,7 +315,7 @@ namespace Umbraco.Core.Configuration var hash = hashString.GenerateHash(); var siteTemp = System.IO.Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", hash); - return _localTempPath = System.IO.Path.Combine(siteTemp, "umbraco.config"); + return _localTempPath = siteTemp; //case LocalTempStorage.Default: //case LocalTempStorage.Unknown: From 9953aea156098e098d325b437deebd63365c4283 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 14 Apr 2019 21:03:24 +0200 Subject: [PATCH 065/201] Make tree item annotation "has pending changes" take precendence over "is listview" --- .../src/less/components/tree/umb-tree.less | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index b66ab40335..9aaf5f6f3a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -236,13 +236,6 @@ body.touch .umb-tree { } } -.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { - content: "\e25a"; - color: @green; - font-size: 20px; - margin-left: -25px; -} - .is-container > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e04e"; color: @blue; @@ -250,6 +243,13 @@ body.touch .umb-tree { margin-left: -20px; } +.has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { + content: "\e25a"; + color: @green; + font-size: 20px; + margin-left: -25px; +} + .protected > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e256"; color: @red; From 576c10cd50029577ddbe2683c6ef05fd029f6763 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Apr 2019 12:56:45 +1000 Subject: [PATCH 066/201] NuCache: fix vanishing content when refreshing content types --- .../PublishedCache/NuCache/ContentStore.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 331ec37248..48c68ab9bf 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -313,6 +313,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var removedContentTypeNodes = new List(); var refreshedContentTypeNodes = new List(); + // find all the nodes that are either refreshed or removed, + // because of their content type being either refreshed or removed foreach (var link in _contentNodes.Values) { var node = link.Value; @@ -322,39 +324,49 @@ namespace Umbraco.Web.PublishedCache.NuCache if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); } - // all content should have been deleted - but + // perform deletion of content with removed content type + // removing content types should have removed their content already + // but just to be 100% sure, clear again here foreach (var node in removedContentTypeNodes) ClearBranchLocked(node); + // perform deletion of removed content types foreach (var id in removedIdsA) { - if (_contentTypesById.TryGetValue(id, out LinkedNode link) == false || link.Value == null) + if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) continue; SetValueLocked(_contentTypesById, id, null); SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); } + // perform update of refreshed content types foreach (var type in refreshedTypesA) { SetValueLocked(_contentTypesById, type.Id, type); SetValueLocked(_contentTypesByAlias, type.Alias, type); } - // skip missing type - // skip missing parents & unbuildable kits - what else could we do? + // perform update of content with refreshed content type - from the kits + // skip missing type, skip missing parents & unbuildable kits - what else could we do? + // kits are ordered by level, so ParentExits is ok here var visited = new List(); foreach (var kit in kits.Where(x => refreshedIdsA.Contains(x.ContentTypeId) && ParentExistsLocked(x) && BuildKit(x))) { + // replacing the node: must preserve the parents + var node = GetHead(_contentNodes, kit.Node.Id)?.Value; + if (node != null) + kit.Node.ChildContentIds = node.ChildContentIds; + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + visited.Add(kit.Node.Id); if (_localDb != null) RegisterChange(kit.Node.Id, kit); } // all content should have been refreshed - but... - var orphans = refreshedContentTypeNodes.Except(visited); foreach (var id in orphans) ClearBranchLocked(id); From 7e64f455b254f40ca3e01611e563723a6836af49 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 15 Apr 2019 08:30:04 +0200 Subject: [PATCH 067/201] Remove leaked Console.WriteLine --- src/Umbraco.Core/Composing/Composers.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs index b21879b055..0510740e42 100644 --- a/src/Umbraco.Core/Composing/Composers.cs +++ b/src/Umbraco.Core/Composing/Composers.cs @@ -76,7 +76,6 @@ namespace Umbraco.Core.Composing // only for debugging, this is verbose //_logger.Debug(GetComposersReport(requirements)); - Console.WriteLine(GetComposersReport(requirements)); var sortedComposerTypes = SortComposers(requirements); From fa655b812ce958fb25a283fcf27534cbd1dbfec6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Apr 2019 17:55:22 +1000 Subject: [PATCH 068/201] Fix composers ordering --- src/Umbraco.Core/Composing/Composers.cs | 133 +++++++++++++----- .../Components/ComponentTests.cs | 28 ++++ src/Umbraco.Web/Runtime/WebFinalComposer.cs | 2 + 3 files changed, 125 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs index 14cb0dce8e..0510740e42 100644 --- a/src/Umbraco.Core/Composing/Composers.cs +++ b/src/Umbraco.Core/Composing/Composers.cs @@ -70,7 +70,23 @@ namespace Umbraco.Core.Composing } } - private IEnumerable PrepareComposerTypes() + internal IEnumerable PrepareComposerTypes() + { + var requirements = GetRequirements(); + + // only for debugging, this is verbose + //_logger.Debug(GetComposersReport(requirements)); + + var sortedComposerTypes = SortComposers(requirements); + + // bit verbose but should help for troubleshooting + //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; + _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); + + return sortedComposerTypes; + } + + internal Dictionary> GetRequirements(bool throwOnMissing = true) { // create a list, remove those that cannot be enabled due to runtime level var composerTypeList = _composerTypes @@ -89,25 +105,69 @@ namespace Umbraco.Core.Composing // enable or disable composers EnableDisableComposers(composerTypeList); - // sort the composers according to their dependencies - var requirements = new Dictionary>(); - foreach (var type in composerTypeList) requirements[type] = null; - foreach (var type in composerTypeList) + void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute { - GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements); - GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements); + foreach (var attribute in type.GetCustomAttributes()) + { + var typeInAttribute = getTypeInAttribute(attribute); + if (typeInAttribute != null && // if the attribute references a type ... + typeInAttribute.IsInterface && // ... which is an interface ... + typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... + !iset.Contains(typeInAttribute)) // ... which is not already in the list + { + // add it to the new list + iset.Add(typeInAttribute); + set2.Add(typeInAttribute); + + // add all its interfaces implementing IComposer + foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) + { + iset.Add(i); + set2.Add(i); + } + } + } } - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); + // gather interfaces too + var interfaces = new HashSet(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); + composerTypeList.AddRange(interfaces); + var list1 = composerTypeList; + while (list1.Count > 0) + { + var list2 = new List(); + foreach (var t in list1) + { + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, list2); + } + composerTypeList.AddRange(list2); + list1 = list2; + } + // sort the composers according to their dependencies + var requirements = new Dictionary>(); + foreach (var type in composerTypeList) + requirements[type] = null; + foreach (var type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); + } + + return requirements; + } + + internal IEnumerable SortComposers(Dictionary> requirements) + { // sort composers var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value); graph.AddItems(requirements); List sortedComposerTypes; try { - sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList(); + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); } catch (Exception e) { @@ -117,40 +177,37 @@ namespace Umbraco.Core.Composing throw; } - // bit verbose but should help for troubleshooting - //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine; - _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes); - return sortedComposerTypes; } - private static string GetComposersReport(Dictionary> requirements) + internal static string GetComposersReport(Dictionary> requirements) { var text = new StringBuilder(); text.AppendLine("Composers & Dependencies:"); + text.AppendLine(" < compose before"); + text.AppendLine(" > compose after"); + text.AppendLine(" : implements"); + text.AppendLine(" = depends"); text.AppendLine(); + bool HasReq(IEnumerable types, Type type) + => types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); + foreach (var kvp in requirements) { var type = kvp.Key; text.AppendLine(type.FullName); foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in type.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); - foreach (var i in type.GetInterfaces()) { - text.AppendLine(" : " + i.FullName); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue - ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")")) - : "")); - foreach (var attribute in i.GetCustomAttributes()) - text.AppendLine(" -< " + attribute.RequiringType); + var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); + text.AppendLine(" > " + attribute.RequiredType + + (weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); } + foreach (var attribute in type.GetCustomAttributes()) + text.AppendLine(" < " + attribute.RequiringType); + foreach (var i in type.GetInterfaces()) + text.AppendLine(" : " + i.FullName); if (kvp.Value != null) foreach (var t in kvp.Value) text.AppendLine(" = " + t); @@ -221,16 +278,16 @@ namespace Umbraco.Core.Composing types.Remove(kvp.Key); } - private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary> requirements, bool throwOnMissing = true) { // get 'require' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requireAttributes = type + var afterAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer // what happens in case of conflicting attributes (different strong/weak for same type) is not specified. - foreach (var attr in requireAttributes) + foreach (var attr in afterAttributes) { if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) @@ -238,13 +295,13 @@ namespace Umbraco.Core.Composing // unless strong, and then require at least one enabled composer implementing that interface if (attr.RequiredType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList(); if (implems.Count > 0) { if (requirements[type] == null) requirements[type] = new List(); requirements[type].AddRange(implems); } - else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak + else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } // requiring a class = require that the composer is enabled @@ -256,28 +313,28 @@ namespace Umbraco.Core.Composing if (requirements[type] == null) requirements[type] = new List(); requirements[type].Add(attr.RequiredType); } - else if (attr.Weak != true) // if not explicitly set to weak, is strong + else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}."); } } } - private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection types, IDictionary> requirements) { // get 'required' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requiredAttributes = type + var beforeAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer - foreach (var attr in requiredAttributes) + foreach (var attr in beforeAttributes) { if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below) // required by an interface = by any enabled composer implementing this that interface if (attr.RequiringType.IsInterface) { - var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList(); + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); foreach (var implem in implems) { if (requirements[implem] == null) requirements[implem] = new List(); diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index c026e5a157..2ba94d8c78 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Compose; using Umbraco.Core.Composing; using Umbraco.Core.IO; @@ -299,11 +300,19 @@ namespace Umbraco.Tests.Components composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer2) }; composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer12) }; composers = new Composers(composition, types, Mock.Of()); @@ -349,6 +358,25 @@ namespace Umbraco.Tests.Components Assert.AreEqual(typeof(Composer27), Composed[1]); } + [Test] + public void AllComposers() + { + var typeLoader = new TypeLoader(AppCaches.Disabled.RuntimeCache, IOHelper.MapPath("~/App_Data/TEMP"), Mock.Of()); + + var register = MockRegister(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); + + var types = typeLoader.GetTypes().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web")); + var composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(); + var report = Composers.GetComposersReport(requirements); + Console.WriteLine(report); + var composerTypes = composers.SortComposers(requirements); + + foreach (var type in composerTypes) + Console.WriteLine(type); + } + #region Compothings public class TestComposerBase : IComposer diff --git a/src/Umbraco.Web/Runtime/WebFinalComposer.cs b/src/Umbraco.Web/Runtime/WebFinalComposer.cs index 64d7725848..c69ae1af1a 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComposer.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComposer.cs @@ -3,7 +3,9 @@ namespace Umbraco.Web.Runtime { // web's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] public class WebFinalComposer : ComponentComposer { } } From 723d9eddd139a2a11cab9fc0f40abe586a6ef280 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Apr 2019 18:15:52 +1000 Subject: [PATCH 069/201] unbreaks a ctor change preventing 8.0.2 from being dropped into the bin --- src/Umbraco.Web/BatchedDatabaseServerMessenger.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 818e8ecf77..f6d0bfd36a 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Scoping; using Umbraco.Web.Composing; +using System.ComponentModel; namespace Umbraco.Web { @@ -26,6 +27,14 @@ namespace Umbraco.Web { private readonly IUmbracoDatabaseFactory _databaseFactory; + [Obsolete("This overload should not be used, enableDistCalls has no effect")] + [EditorBrowsable(EditorBrowsableState.Never)] + public BatchedDatabaseServerMessenger( + IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, bool enableDistCalls, DatabaseServerMessengerOptions options) + : this(runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings, options) + { + } + public BatchedDatabaseServerMessenger( IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, DatabaseServerMessengerOptions options) : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) From 6cb55789bd6fe0f1a1300f660d9977cc39395efa Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 15 Apr 2019 10:25:29 +0200 Subject: [PATCH 070/201] Fix mapper concurrency issue --- src/Umbraco.Core/Mapping/UmbracoMapper.cs | 14 ++-- src/Umbraco.Tests/Mapping/MappingTests.cs | 83 ++++++++++++++++++++++- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index 0831edab4e..a2ddebe431 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -29,11 +30,16 @@ namespace Umbraco.Core.Mapping /// public class UmbracoMapper { - private readonly Dictionary>> _ctors - = new Dictionary>>(); + // note + // + // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary + // the inner dictionaries are never modified and therefore can be simple Dictionary - private readonly Dictionary>> _maps - = new Dictionary>>(); + private readonly ConcurrentDictionary>> _ctors + = new ConcurrentDictionary>>(); + + private readonly ConcurrentDictionary>> _maps + = new ConcurrentDictionary>>(); /// /// Initializes a new instance of the class. diff --git a/src/Umbraco.Tests/Mapping/MappingTests.cs b/src/Umbraco.Tests/Mapping/MappingTests.cs index 3435050cc5..47be99bb0e 100644 --- a/src/Umbraco.Tests/Mapping/MappingTests.cs +++ b/src/Umbraco.Tests/Mapping/MappingTests.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; using NUnit.Framework; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -108,6 +110,69 @@ namespace Umbraco.Tests.Mapping var target = mapper.Map>(source); } + [Test] + [Explicit] + public void ConcurrentMap() + { + var definitions = new MapDefinitionCollection(new IMapDefinition[] + { + new MapperDefinition1(), + new MapperDefinition3(), + }); + var mapper = new UmbracoMapper(definitions); + + // the mapper currently has a map from Thing1 to Thing2 + // because Thing3 inherits from Thing1, it will map a Thing3 instance, + // and register a new map from Thing3 to Thing2, + // thus modifying its internal dictionaries + + // if timing is good, and mapper does have non-concurrent dictionaries, it fails + // practically, to reproduce, one needs to add a 1s sleep in the mapper's loop + // hence, this test is explicit + + var thing3 = new Thing3 { Value = "value" }; + var thing4 = new Thing4(); + Exception caught = null; + + void ThreadLoop() + { + for (var i = 0; i < 10; i++) + { + try + { + mapper.Map(thing4); + } + catch (Exception e) + { + caught = e; + Console.WriteLine($"{e.GetType().Name} {e.Message}"); + } + } + + Console.WriteLine("done"); + } + + var thread = new Thread(ThreadLoop); + thread.Start(); + Thread.Sleep(1000); + + try + { + Console.WriteLine($"{DateTime.Now:O} mapping"); + var thing2 = mapper.Map(thing3); + Console.WriteLine($"{DateTime.Now:O} mapped"); + + Assert.IsNotNull(thing2); + Assert.AreEqual("value", thing2.Value); + } + finally + { + thread.Join(); + } + + Assert.IsNull(caught); + } + private class Thing1 { public string Value { get; set; } @@ -121,6 +186,9 @@ namespace Umbraco.Tests.Mapping public string Value { get; set; } } + private class Thing4 + { } + private class MapperDefinition1 : IMapDefinition { public void DefineMaps(UmbracoMapper mapper) @@ -144,5 +212,18 @@ namespace Umbraco.Tests.Mapping private static void Map(Property source, ContentPropertyDto target, MapperContext context) { } } + + private class MapperDefinition3 : IMapDefinition + { + public void DefineMaps(UmbracoMapper mapper) + { + // just some random things so that the mapper contains things + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + mapper.Define(); + } + } } } From 3f5ccb302d9514bbc6e76e8c60ba5d1aff3e3e16 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 15 Apr 2019 21:10:09 +0200 Subject: [PATCH 071/201] Make the tree annotations easier to see --- .../src/less/components/tree/umb-tree.less | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index b66ab40335..bff9c1a53e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -228,40 +228,60 @@ body.touch .umb-tree { } } -.umb-tree-item__annotation { - &::before { - font-family: 'icomoon'; - position: absolute; - bottom: 0; +.has-unpublished-version, .is-container, .protected { + > .umb-tree-item__inner { + > .umb-tree-item__annotation { + background-color: @white; + border-radius: 50%; + width: 12px; + height: 12px; + position: absolute; + margin-left: 12px; + top: 17px; + + &::before { + font-family: 'icomoon'; + position: absolute; + top: -4px; + } + } + } + + &.current > .umb-tree-item__inner > .umb-tree-item__annotation { + background-color: @pinkLight; } } .has-unpublished-version > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e25a"; color: @green; - font-size: 20px; - margin-left: -25px; + font-size: 23px; + margin-left: 16px; + left: -21px; } .is-container > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e04e"; color: @blue; font-size: 9px; - margin-left: -20px; + margin-left: 2px; + left: 0px; } .protected > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e256"; color: @red; - font-size: 20px; - margin-left: -25px; + font-size: 23px; + margin-left: -3px; + left: -2px; } .locked > .umb-tree-item__inner > .umb-tree-item__annotation::before { content: "\e0a7"; color: @red; font-size: 9px; - margin-left: -20px; + margin-left: 2px; + left: 0px; } .no-access { From 52bc4b04b35a964e1a4a675aa449873248638655 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 15 Apr 2019 21:16:59 +0200 Subject: [PATCH 072/201] Fix annotation hover background color --- .../src/less/components/tree/umb-tree.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index bff9c1a53e..95c2ac68d7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -245,6 +245,10 @@ body.touch .umb-tree { top: -4px; } } + + &:hover > .umb-tree-item__annotation { + background-color: @ui-option-hover; + } } &.current > .umb-tree-item__inner > .umb-tree-item__annotation { From 02ddf80014093fa4dc4b60c8dd01f954519c9b61 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Apr 2019 15:39:28 +1000 Subject: [PATCH 073/201] Adds a unit test --- .../Repositories/MemberTypeRepositoryTest.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 0c2314fd47..561822bbbe 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -232,6 +232,32 @@ namespace Umbraco.Tests.Persistence.Repositories } } + /// + /// This demonstates an issue found: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698 + /// + [Test] + public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception() + { + //TODO: Fix this bug and then change this test + + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + repository.Save(memberType); + + foreach(var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs()) + { + var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key); + prop.Alias = prop.Alias + "__0000"; + } + + Assert.Throws(() => repository.Save(memberType)); + } + } + [Test] public void Built_In_Member_Type_Properties_Are_Automatically_Added_When_Creating() { From 6f44e4fd09775c594f0d5ef051a84e77e5e19573 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Apr 2019 17:37:42 +1000 Subject: [PATCH 074/201] Fixes INotifyCollectionChanged collections to raise the correct events, ensures that no duplicate property type aliases can be added/updated to --- src/Umbraco.Core/EnumerableExtensions.cs | 17 +++++++ src/Umbraco.Core/Models/ContentTypeBase.cs | 12 +++++ src/Umbraco.Core/Models/PropertyCollection.cs | 13 ++--- .../Models/PropertyGroupCollection.cs | 17 +++---- .../Models/PropertyTypeCollection.cs | 37 ++++++++++---- src/Umbraco.Tests/Models/ContentTypeTests.cs | 50 +++++++++++++++---- .../Repositories/MemberTypeRepositoryTest.cs | 2 +- 7 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index d71ccb04b9..00bb783486 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -10,6 +10,23 @@ namespace Umbraco.Core /// public static class EnumerableExtensions { + internal static bool HasDuplicates(this IEnumerable items, bool includeNull) + { + var hs = new HashSet(); + foreach (var item in items) + { + if (item != null || includeNull) + { + if (!hs.Add(item)) + { + return true; + } + } + } + return false; + } + + /// /// Wraps this object instance into an IEnumerable{T} consisting of a single item. /// diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index ed8a098299..04fac1a855 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -95,6 +95,18 @@ namespace Umbraco.Core.Models protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { + //detect if there are any duplicate aliases - this cannot be allowed + if (e.Action == NotifyCollectionChangedAction.Add + || e.Action == NotifyCollectionChangedAction.Replace) + { + var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); + if (allAliases.HasDuplicates(false)) + { + var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); + throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); + } + } + OnPropertyChanged(nameof(PropertyTypes)); } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index 977600a2f7..c587a45424 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Models public class PropertyCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable { private readonly object _addLocker = new object(); - internal Action OnAdd; + internal Func AdditionValidator { get; set; } /// @@ -49,10 +49,12 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable properties) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var property in properties) Add(property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// @@ -60,8 +62,9 @@ namespace Umbraco.Core.Models /// protected override void SetItem(int index, Property property) { + var oldItem = index >= 0 ? this[index] : property; base.SetItem(index, property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property, index)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem)); } /// @@ -120,10 +123,8 @@ namespace Umbraco.Core.Models } } + //collection events will be raised in InsertItem with Add base.Add(property); - - OnAdd?.Invoke(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property)); } } diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index 26e0fef178..5422dfb792 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -19,9 +19,6 @@ namespace Umbraco.Core.Models { private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); - // TODO: this doesn't seem to be used anywhere - internal Action OnAdd; - internal PropertyGroupCollection() { } @@ -37,16 +34,19 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable groups) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var group in groups) Add(group); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } protected override void SetItem(int index, PropertyGroup item) { + var oldItem = index >= 0 ? this[index] : item; base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); } protected override void RemoveItem(int index) @@ -84,6 +84,7 @@ namespace Umbraco.Core.Models if (keyExists) throw new Exception($"Naming conflict: Changing the name of PropertyGroup '{item.Name}' would result in duplicates"); + //collection events will be raised in SetItem SetItem(IndexOfKey(item.Id), item); return; } @@ -96,16 +97,14 @@ namespace Umbraco.Core.Models var exists = Contains(key); if (exists) { + //collection events will be raised in SetItem SetItem(IndexOfKey(key), item); return; } } } - + //collection events will be raised in InsertItem base.Add(item); - OnAdd?.Invoke(); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); } finally { diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs index 6181ee078b..6e41f0d12b 100644 --- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs +++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs @@ -19,9 +19,6 @@ namespace Umbraco.Core.Models [IgnoreDataMember] private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim(); - // TODO: This doesn't seem to be used - [IgnoreDataMember] - internal Action OnAdd; internal PropertyTypeCollection(bool supportsPublishing) { @@ -43,36 +40,44 @@ namespace Umbraco.Core.Models /// internal void Reset(IEnumerable properties) { + //collection events will be raised in each of these calls Clear(); + + //collection events will be raised in each of these calls foreach (var property in properties) - Add(property); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + Add(property); } protected override void SetItem(int index, PropertyType item) { item.SupportsPublishing = SupportsPublishing; - base.SetItem(index, item); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + var oldItem = index >= 0 ? this[index] : item; + base.SetItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem)); + item.PropertyChanged += Item_PropertyChanged; } protected override void RemoveItem(int index) { var removed = this[index]; base.RemoveItem(index); + removed.PropertyChanged -= Item_PropertyChanged; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); } protected override void InsertItem(int index, PropertyType item) { item.SupportsPublishing = SupportsPublishing; - base.InsertItem(index, item); + base.InsertItem(index, item); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + item.PropertyChanged += Item_PropertyChanged; } protected override void ClearItems() { base.ClearItems(); + foreach (var item in this) + item.PropertyChanged -= Item_PropertyChanged; OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } @@ -91,6 +96,7 @@ namespace Umbraco.Core.Models var exists = Contains(key); if (exists) { + //collection events will be raised in SetItem SetItem(IndexOfKey(key), item); return; } @@ -103,10 +109,8 @@ namespace Umbraco.Core.Models item.SortOrder = this.Max(x => x.SortOrder) + 1; } + //collection events will be raised in InsertItem base.Add(item); - OnAdd?.Invoke(); - - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); } finally { @@ -115,6 +119,17 @@ namespace Umbraco.Core.Models } } + /// + /// Occurs when a property changes on a PropertyType that exists in this collection + /// + /// + /// + private void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + var propType = (PropertyType)sender; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType)); + } + /// /// Determines whether this collection contains a whose alias matches the specified PropertyType. /// diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index d9e65ba6c6..3b35bb2dfc 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -15,13 +15,45 @@ namespace Umbraco.Tests.Models [TestFixture] public class ContentTypeTests : UmbracoTestBase { + [Test] + public void Cannot_Add_Duplicate_Property_Aliases() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[] + { + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" } + }))); + + Assert.Throws(() => + contentType.PropertyTypeCollection.Add( + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType" })); + + } + + [Test] + public void Cannot_Update_Duplicate_Property_Aliases() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + + contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[] + { + new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" } + }))); + + contentType.PropertyTypeCollection.Add(new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType2" }); + + var toUpdate = contentType.PropertyTypeCollection["myPropertyType2"]; + + Assert.Throws(() => toUpdate.Alias = "myPropertyType"); + + } [Test] public void Can_Deep_Clone_Content_Type_Sort() { var contentType = new ContentTypeSort(new Lazy(() => 3), 4, "test"); - var clone = (ContentTypeSort) contentType.DeepClone(); + var clone = (ContentTypeSort)contentType.DeepClone(); Assert.AreNotSame(clone, contentType); Assert.AreEqual(clone, contentType); Assert.AreEqual(clone.Id.Value, contentType.Id.Value); @@ -54,7 +86,7 @@ namespace Umbraco.Tests.Models contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -117,12 +149,12 @@ namespace Umbraco.Tests.Models { group.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -167,12 +199,12 @@ namespace Umbraco.Tests.Models { group.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; - contentType.AllowedContentTypes = new[] {new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2")}; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; + contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); @@ -264,12 +296,12 @@ namespace Umbraco.Tests.Models { propertyType.Id = ++i; } - contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } }; + contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } }; contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy(() => 888), 8, "sub"), new ContentTypeSort(new Lazy(() => 889), 9, "sub2") }; contentType.Id = 10; contentType.CreateDate = DateTime.Now; contentType.CreatorId = 22; - contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate") + contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate") { Id = 88 }); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 561822bbbe..0b655004da 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -248,7 +248,7 @@ namespace Umbraco.Tests.Persistence.Repositories IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); repository.Save(memberType); - foreach(var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs()) + foreach (var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs()) { var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key); prop.Alias = prop.Alias + "__0000"; From 40d2898fb88ac09a56b75780b99143ee20aa3e51 Mon Sep 17 00:00:00 2001 From: Ben Palmer Date: Tue, 16 Apr 2019 09:21:24 +0100 Subject: [PATCH 075/201] Make DropDownFlexibleConfiguration class public to allow configuration to be set when creating a new data type --- .../PropertyEditors/DropDownFlexibleConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs index d1c2d23c4f..0a9d750964 100644 --- a/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/DropDownFlexibleConfiguration.cs @@ -1,8 +1,8 @@ namespace Umbraco.Core.PropertyEditors { - internal class DropDownFlexibleConfiguration : ValueListConfiguration + public class DropDownFlexibleConfiguration : ValueListConfiguration { [ConfigurationField("multiple", "Enable multiple choice", "boolean", Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")] public bool Multiple { get; set; } } -} \ No newline at end of file +} From 0787824fba659a56d6f6b6d3bd4f0244fc024bcf Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Apr 2019 11:23:22 +0200 Subject: [PATCH 076/201] Pass default configuration to configuration editor for macro parameter editors --- src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs | 8 +++++++- src/Umbraco.Core/PropertyEditors/DataEditor.cs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index bb58a8ef72..8151753a43 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -13,12 +13,15 @@ namespace Umbraco.Core.PropertyEditors /// public class ConfigurationEditor : IConfigurationEditor { + private IDictionary _defaultConfiguration; + /// /// Initializes a new instance of the class. /// public ConfigurationEditor() { Fields = new List(); + _defaultConfiguration = new Dictionary(); } /// @@ -61,7 +64,10 @@ namespace Umbraco.Core.PropertyEditors /// [JsonProperty("defaultConfig")] - public virtual IDictionary DefaultConfiguration => new Dictionary(); + public virtual IDictionary DefaultConfiguration { + get => _defaultConfiguration; + internal set => _defaultConfiguration = value; + } /// public virtual object DefaultConfigurationObject => DefaultConfiguration; diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index ae6ace996e..43f4b68b99 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -173,7 +173,13 @@ namespace Umbraco.Core.PropertyEditors /// protected virtual IConfigurationEditor CreateConfigurationEditor() { - return new ConfigurationEditor(); + var editor = new ConfigurationEditor(); + // pass the default configuration if this is not a property value editor + if((Type & EditorType.PropertyValue) == 0) + { + editor.DefaultConfiguration = _defaultConfiguration; + } + return editor; } /// From d0701ae34ed9bc53e355830bcd72fae986e2f18b Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Mon, 15 Apr 2019 15:57:35 +0200 Subject: [PATCH 077/201] Add media url provider support --- .../Cache/DistributedCacheBinderTests.cs | 1 + .../PublishedContentCacheTests.cs | 1 + .../PublishedContentSnapshotTestBase.cs | 1 + .../Scoping/ScopedNuCacheTests.cs | 1 + .../Security/BackOfficeCookieManagerTests.cs | 5 +- .../TestControllerActivatorBase.cs | 1 + .../TestHelpers/TestObjects-Mocks.cs | 2 + .../TestHelpers/TestWithDatabaseBase.cs | 1 + .../Testing/TestingTests/MockTests.cs | 2 +- ...RenderIndexActionSelectorAttributeTests.cs | 4 + .../Web/Mvc/SurfaceControllerTests.cs | 4 + .../Web/Mvc/UmbracoViewPageTests.cs | 1 + .../Web/TemplateUtilitiesTests.cs | 1 + .../Web/WebExtensionMethodTests.cs | 4 + src/Umbraco.Web/Composing/Current.cs | 3 + src/Umbraco.Web/CompositionExtensions.cs | 7 ++ .../ImageCropperTemplateExtensions.cs | 8 +- .../Models/PublishedContentBase.cs | 49 ++-------- src/Umbraco.Web/PublishedContentExtensions.cs | 47 +++++++++- .../Routing/DefaultMediaUrlProvider.cs | 76 ++++++++++++++++ src/Umbraco.Web/Routing/IMediaUrlProvider.cs | 31 +++++++ .../Routing/MediaUrlProviderCollection.cs | 13 +++ .../MediaUrlProviderCollectionBuilder.cs | 9 ++ src/Umbraco.Web/Routing/UrlProvider.cs | 91 ++++++++++++++++++- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 3 + src/Umbraco.Web/Umbraco.Web.csproj | 4 + src/Umbraco.Web/UmbracoContext.cs | 4 +- src/Umbraco.Web/UmbracoContextFactory.cs | 6 +- 28 files changed, 322 insertions(+), 58 deletions(-) create mode 100644 src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/IMediaUrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs create mode 100644 src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs diff --git a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs index c092306473..e446e049b6 100644 --- a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs @@ -159,6 +159,7 @@ namespace Umbraco.Tests.Cache TestObjects.GetUmbracoSettings(), TestObjects.GetGlobalSettings(), new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); // just assert it does not throw diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs index 2a6739df38..fbf828ad20 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedContentCacheTests.cs @@ -81,6 +81,7 @@ namespace Umbraco.Tests.Cache.PublishedCache new WebSecurity(_httpContextFactory.HttpContext, Current.Services.UserService, globalSettings), umbracoSettings, Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs index 7a9a882baa..7e6ae75356 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -71,6 +71,7 @@ namespace Umbraco.Tests.PublishedContent new WebSecurity(httpContext, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index 1dcc928141..d969356ce9 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -118,6 +118,7 @@ namespace Umbraco.Tests.Scoping new WebSecurity(httpContext, Current.Services.UserService, globalSettings), umbracoSettings ?? SettingsForTests.GetDefaultUmbracoSettings(), urlProviders ?? Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs index e32df610b9..2b04a02f46 100644 --- a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs +++ b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Configuration; +using System.Linq; using System.Web; using Microsoft.Owin; using Moq; @@ -33,7 +34,7 @@ namespace Umbraco.Tests.Security Mock.Of(), Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), new List(),globalSettings, + TestObjects.GetUmbracoSettings(), new List(), Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); @@ -53,7 +54,7 @@ namespace Umbraco.Tests.Security Mock.Of(), Mock.Of(), new WebSecurity(Mock.Of(), Current.Services.UserService, globalSettings), - TestObjects.GetUmbracoSettings(), new List(), globalSettings, + TestObjects.GetUmbracoSettings(), new List(), Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index c6bbebf550..b5712a52aa 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -139,6 +139,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting webSecurity.Object, Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index 75e9cd60cb..647824ab66 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -122,6 +122,7 @@ namespace Umbraco.Tests.TestHelpers var umbracoSettings = GetUmbracoSettings(); var globalSettings = GetGlobalSettings(); var urlProviders = new UrlProviderCollection(Enumerable.Empty()); + var mediaUrlProviders = new MediaUrlProviderCollection(Enumerable.Empty()); if (accessor == null) accessor = new TestUmbracoContextAccessor(); @@ -133,6 +134,7 @@ namespace Umbraco.Tests.TestHelpers umbracoSettings, globalSettings, urlProviders, + mediaUrlProviders, Mock.Of()); return umbracoContextFactory.EnsureUmbracoContext(httpContext).UmbracoContext; diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 7f3c855593..94154e7d9c 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -380,6 +380,7 @@ namespace Umbraco.Tests.TestHelpers Factory.GetInstance()), umbracoSettings ?? Factory.GetInstance(), urlProviders ?? Enumerable.Empty(), + Enumerable.Empty(), globalSettings ?? Factory.GetInstance(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index 57381eb287..d85f610236 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -80,7 +80,7 @@ namespace Umbraco.Tests.Testing.TestingTests .Returns(UrlInfo.Url("/hello/world/1234")); var urlProvider = urlProviderMock.Object; - var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, umbracoContext.VariationContextAccessor); + var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, Enumerable.Empty(), umbracoContext.VariationContextAccessor); var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); diff --git a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs index 0b76de9879..eeb1eb4b37 100644 --- a/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/RenderIndexActionSelectorAttributeTests.cs @@ -72,6 +72,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -101,6 +102,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -130,6 +132,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -159,6 +162,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs index 7de2dd1aad..1a789023a5 100644 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs @@ -47,6 +47,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -74,6 +75,7 @@ namespace Umbraco.Tests.Web.Mvc TestObjects.GetUmbracoSettings(), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -104,6 +106,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); @@ -141,6 +144,7 @@ namespace Umbraco.Tests.Web.Mvc Mock.Of(section => section.WebRouting == webRoutingSettings), globalSettings, new UrlProviderCollection(Enumerable.Empty()), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of()); diff --git a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs index d1395c6f2e..5c291c9601 100644 --- a/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/UmbracoViewPageTests.cs @@ -440,6 +440,7 @@ namespace Umbraco.Tests.Web.Mvc new WebSecurity(http, Current.Services.UserService, globalSettings), TestObjects.GetUmbracoSettings(), Enumerable.Empty(), + Enumerable.Empty(), globalSettings, new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs index 5e9208faf9..5e4112449e 100644 --- a/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs @@ -105,6 +105,7 @@ namespace Umbraco.Tests.Web Mock.Of(section => section.WebRouting == Mock.Of(routingSection => routingSection.UrlProviderMode == "Auto")), globalSettings, new UrlProviderCollection(new[] { testUrlProvider.Object }), + new MediaUrlProviderCollection(Enumerable.Empty()), Mock.Of()); using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of())) diff --git a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs index 21b72a3832..9ba010642e 100644 --- a/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs +++ b/src/Umbraco.Tests/Web/WebExtensionMethodTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -31,6 +32,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); var r1 = new RouteData(); @@ -49,6 +51,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); @@ -77,6 +80,7 @@ namespace Umbraco.Tests.Web new WebSecurity(Mock.Of(), Current.Services.UserService, TestObjects.GetGlobalSettings()), TestObjects.GetUmbracoSettings(), new List(), + Enumerable.Empty(), TestObjects.GetGlobalSettings(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 420c0b8a4c..5be5e45ecd 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -101,6 +101,9 @@ namespace Umbraco.Web.Composing public static UrlProviderCollection UrlProviders => Factory.GetInstance(); + public static MediaUrlProviderCollection MediaUrlProviders + => Factory.GetInstance(); + public static HealthCheckCollectionBuilder HealthCheckCollectionBuilder => Factory.GetInstance(); diff --git a/src/Umbraco.Web/CompositionExtensions.cs b/src/Umbraco.Web/CompositionExtensions.cs index 9052b20934..27a56afc1e 100644 --- a/src/Umbraco.Web/CompositionExtensions.cs +++ b/src/Umbraco.Web/CompositionExtensions.cs @@ -90,6 +90,13 @@ namespace Umbraco.Web public static UrlProviderCollectionBuilder UrlProviders(this Composition composition) => composition.WithCollectionBuilder(); + /// + /// Gets the media url providers collection builder. + /// + /// The composition. + public static MediaUrlProviderCollectionBuilder MediaUrlProviders(this Composition composition) + => composition.WithCollectionBuilder(); + /// /// Gets the backoffice sections/applications collection builder. /// diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index ed9903e07a..d7f457287a 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -130,15 +130,15 @@ namespace Umbraco.Web if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) return string.Empty; + var mediaItemUrl = mediaItem.MediaUrl(propertyAlias); + //get the default obj from the value converter var cropperValue = mediaItem.Value(propertyAlias); //is it strongly typed? var stronglyTyped = cropperValue as ImageCropperValue; - string mediaItemUrl; if (stronglyTyped != null) { - mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -149,14 +149,12 @@ namespace Umbraco.Web if (jobj != null) { stronglyTyped = jobj.ToObject(); - mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); } //it's a single string - mediaItemUrl = cropperValue.ToString(); return GetCropUrl( mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -322,7 +320,7 @@ namespace Umbraco.Web if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) return null; - imageProcessorUrl.Append(cropDataSet.Src); + imageProcessorUrl.Append(imageUrl); cropDataSet.AppendCropBaseUrl(imageProcessorUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); if (crop != null & useCropDimensions) diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 39933b49be..cdcfd8a0cd 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -81,54 +81,23 @@ namespace Umbraco.Web.Models /// /// - /// The url of documents are computed by the document url providers. The url of medias are, at the moment, - /// computed here from the 'umbracoFile' property -- but we should move to media url providers at some point. + /// The url of documents are computed by the document url providers. The url of medias are computed by the media url providers /// public virtual string GetUrl(string culture = null) // TODO: consider .GetCulture("fr-FR").Url { + var umbracoContext = UmbracoContextAccessor.UmbracoContext; + + if (umbracoContext == null) + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); + if (umbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); + switch (ItemType) { case PublishedItemType.Content: - var umbracoContext = UmbracoContextAccessor.UmbracoContext; - - if (umbracoContext == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext is null."); - if (umbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot compute Url for a content item when UmbracoContext.UrlProvider is null."); - return umbracoContext.UrlProvider.GetUrl(this, culture); - case PublishedItemType.Media: - var prop = GetProperty(Constants.Conventions.Media.File); - if (prop?.GetValue() == null) - { - return string.Empty; - } - - var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - - // TODO: consider implementing media url providers - // note: that one does not support variations - //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always - // want to return the normal URL and the cropper stores data as json - switch (propType.EditorAlias) - { - case Constants.PropertyEditors.Aliases.UploadField: - - return prop.GetValue().ToString(); - case Constants.PropertyEditors.Aliases.ImageCropper: - //get the url from the json format - - var stronglyTyped = prop.GetValue() as ImageCropperValue; - if (stronglyTyped != null) - { - return stronglyTyped.Src; - } - return prop.GetValue()?.ToString(); - } - - return string.Empty; - + return umbracoContext.UrlProvider.GetMediaUrl(this, Constants.Conventions.Media.File, culture); default: throw new NotSupportedException(); } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 54afb7abbd..b776bb35db 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -45,21 +45,58 @@ namespace Umbraco.Web public static string UrlAbsolute(this IPublishedContent content) { // adapted from PublishedContentBase.Url + + if (Current.UmbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); + if (Current.UmbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); + switch (content.ItemType) { case PublishedItemType.Content: - if (Current.UmbracoContext == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); - if (Current.UmbracoContext.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true); case PublishedItemType.Media: - throw new NotSupportedException("AbsoluteUrl is not supported for media types."); + return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, Constants.Conventions.Media.File, true); default: throw new ArgumentOutOfRangeException(); } } + /// + /// Gets the url for the media. + /// + /// The content. + /// The property alias to resolve the url from. + /// The variation language. + /// The url for the content. + /// Better use the GetMediaUrl method but that method is here to complement MediaUrlAbsolute(). + public static string MediaUrl(this IPublishedContent content, string propertyAlias, string culture = null) + { + if (Current.UmbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); + if (Current.UmbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); + + return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, culture); + } + + /// + /// Gets the absolute url for the media. + /// + /// The content. + /// The property alias to resolve the url from. + /// The variation language. + /// The absolute url for the media. + public static string MediaUrlAbsolute(this IPublishedContent content, string propertyAlias, string culture = null) + { + if (Current.UmbracoContext == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); + if (Current.UmbracoContext.UrlProvider == null) + throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); + + return Current.UmbracoContext.UrlProvider.GetMediaUrl(content, propertyAlias, true, culture); + } + /// /// Gets the Url segment. /// diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs new file mode 100644 index 0000000000..8616d2fc7f --- /dev/null +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -0,0 +1,76 @@ +using System; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.Routing +{ + /// + /// Default media url provider. + /// + public class DefaultMediaUrlProvider : IMediaUrlProvider + { + /// + public virtual UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, + string propertyAlias, + UrlProviderMode mode, string culture, Uri current) + { + var prop = content.GetProperty(propertyAlias); + var value = prop?.GetValue(culture); + if (value == null) + { + return null; + } + + var propType = content.ContentType.GetPropertyType(propertyAlias); + + string path = null; + + switch (propType.EditorAlias) + { + case Constants.PropertyEditors.Aliases.UploadField: + path = value.ToString(); + break; + case Constants.PropertyEditors.Aliases.ImageCropper: + //get the url from the json format + + var stronglyTyped = value as ImageCropperValue; + if (stronglyTyped != null) + { + path = stronglyTyped.Src; + break; + } + path = value.ToString(); + break; + } + + return path == null ? null : UrlInfo.Url(AssembleUrl(path, current, mode).ToString(), culture); + } + + private Uri AssembleUrl(string path, Uri current, UrlProviderMode mode) + { + if (string.IsNullOrEmpty(path)) + return null; + + Uri uri; + + if (current == null) + mode = UrlProviderMode.Relative; // best we can do + + switch (mode) + { + case UrlProviderMode.Absolute: + uri = new Uri(current?.GetLeftPart(UriPartial.Authority) + path); + break; + case UrlProviderMode.Relative: + case UrlProviderMode.Auto: + uri = new Uri(path, UriKind.Relative); + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode)); + } + + return uri.Rewrite(UriUtility.ToAbsolute(uri.GetSafeAbsolutePath())); + } + } +} diff --git a/src/Umbraco.Web/Routing/IMediaUrlProvider.cs b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs new file mode 100644 index 0000000000..00d0ea7ec7 --- /dev/null +++ b/src/Umbraco.Web/Routing/IMediaUrlProvider.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides media urls. + /// + public interface IMediaUrlProvider + { + /// + /// Gets the nice url of a media item. + /// + /// The Umbraco context. + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// The url provider can ignore the mode and always return an absolute url, + /// e.g. a cdn url provider will most likely always return an absolute url. + /// If the provider is unable to provide a url, it returns null. + /// + UrlInfo GetMediaUrl(UmbracoContext umbracoContext, IPublishedContent content, string propertyAlias, UrlProviderMode mode, string culture, Uri current); + } +} diff --git a/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs new file mode 100644 index 0000000000..7e2362c552 --- /dev/null +++ b/src/Umbraco.Web/Routing/MediaUrlProviderCollection.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Routing +{ + public class MediaUrlProviderCollection : BuilderCollectionBase + { + public MediaUrlProviderCollection(IEnumerable items) + : base(items) + { + } + } +} diff --git a/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs b/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs new file mode 100644 index 0000000000..7bfc56ed0d --- /dev/null +++ b/src/Umbraco.Web/Routing/MediaUrlProviderCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Routing +{ + public class MediaUrlProviderCollectionBuilder : OrderedCollectionBuilderBase + { + protected override MediaUrlProviderCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs index 0662d46f49..77f4a3c51a 100644 --- a/src/Umbraco.Web/Routing/UrlProvider.cs +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Web.Composing; +using Umbraco.Web.Models; namespace Umbraco.Web.Routing { @@ -23,13 +24,15 @@ namespace Umbraco.Web.Routing /// The Umbraco context. /// Routing settings. /// The list of url providers. + /// The list of media url providers. /// The current variation accessor. - public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor) + public UrlProvider(UmbracoContext umbracoContext, IWebRoutingSection routingSettings, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor) { if (routingSettings == null) throw new ArgumentNullException(nameof(routingSettings)); _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; + _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); var provider = UrlProviderMode.Auto; Mode = provider; @@ -45,12 +48,14 @@ namespace Umbraco.Web.Routing /// /// The Umbraco context. /// The list of url providers. + /// The list of media url providers /// The current variation accessor. /// An optional provider mode. - public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IVariationContextAccessor variationContextAccessor, UrlProviderMode mode = UrlProviderMode.Auto) + public UrlProvider(UmbracoContext umbracoContext, IEnumerable urlProviders, IEnumerable mediaUrlProviders, IVariationContextAccessor variationContextAccessor, UrlProviderMode mode = UrlProviderMode.Auto) { _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); _urlProviders = urlProviders; + _mediaUrlProviders = mediaUrlProviders; _variationContextAccessor = variationContextAccessor; Mode = mode; @@ -58,6 +63,7 @@ namespace Umbraco.Web.Routing private readonly UmbracoContext _umbracoContext; private readonly IEnumerable _urlProviders; + private readonly IEnumerable _mediaUrlProviders; private readonly IVariationContextAccessor _variationContextAccessor; /// @@ -250,5 +256,86 @@ namespace Umbraco.Web.Routing } #endregion + + #region GetMediaUrl + + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, string propertyAlias, string culture = null, Uri current = null) + => GetMediaUrl(content, propertyAlias, Mode, culture, current); + + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// A value indicating whether the url should be absolute in any case. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, string propertyAlias, bool absolute, string culture = null, Uri current = null) + => GetMediaUrl(content, propertyAlias, GetMode(absolute), culture, current); + /// + /// Gets the url of a media item. + /// + /// The published content. + /// The property alias to resolve the url from. + /// The url mode. + /// The variation language. + /// The current absolute url. + /// The url for the media. + /// + /// The url is absolute or relative depending on mode and on current. + /// If the media is multi-lingual, gets the url for the specified culture or, + /// when no culture is specified, the current culture. + /// If the provider is unable to provide a url, it returns . + /// + public string GetMediaUrl(IPublishedContent content, + string propertyAlias, UrlProviderMode mode, + string culture = null, Uri current = null) + { + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + + if (content == null) + return ""; + + // this the ONLY place where we deal with default culture - IMediaUrlProvider always receive a culture + // be nice with tests, assume things can be null, ultimately fall back to invariant + // (but only for variant content of course) + if (content.ContentType.VariesByCulture()) + { + if (culture == null) + culture = _variationContextAccessor?.VariationContext?.Culture ?? ""; + } + + if (current == null) + current = _umbracoContext.CleanedUmbracoUrl; + + var url = _mediaUrlProviders.Select(provider => + provider.GetMediaUrl(_umbracoContext, content, propertyAlias, mode, culture, current)) + .FirstOrDefault(u => u != null); + + return url?.Text ?? ""; + } + + #endregion } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index f1c8fcc12f..5a464701e0 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -183,6 +183,9 @@ namespace Umbraco.Web.Runtime .Append() .Append(); + composition.WithCollectionBuilder() + .Append(); + composition.RegisterUnique(); composition.WithCollectionBuilder() diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b3cab3b2cb..e565f354c8 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -214,7 +214,11 @@ + + + + diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 8ab6f8c946..9ae9a5cb16 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -33,6 +33,7 @@ namespace Umbraco.Web WebSecurity webSecurity, IUmbracoSettingsSection umbracoSettings, IEnumerable urlProviders, + IEnumerable mediaUrlProviders, IGlobalSettings globalSettings, IVariationContextAccessor variationContextAccessor) { @@ -41,6 +42,7 @@ namespace Umbraco.Web if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); if (umbracoSettings == null) throw new ArgumentNullException(nameof(umbracoSettings)); if (urlProviders == null) throw new ArgumentNullException(nameof(urlProviders)); + if (mediaUrlProviders == null) throw new ArgumentNullException(nameof(mediaUrlProviders)); VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); @@ -71,7 +73,7 @@ namespace Umbraco.Web // OriginalRequestUrl = GetRequestFromContext()?.Url ?? new Uri("http://localhost"); CleanedUmbracoUrl = UriUtility.UriToUmbraco(OriginalRequestUrl); - UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, variationContextAccessor); + UrlProvider = new UrlProvider(this, umbracoSettings.WebRouting, urlProviders, mediaUrlProviders, variationContextAccessor); } /// diff --git a/src/Umbraco.Web/UmbracoContextFactory.cs b/src/Umbraco.Web/UmbracoContextFactory.cs index 9bb2a79411..2a812036bf 100644 --- a/src/Umbraco.Web/UmbracoContextFactory.cs +++ b/src/Umbraco.Web/UmbracoContextFactory.cs @@ -29,12 +29,13 @@ namespace Umbraco.Web private readonly IUmbracoSettingsSection _umbracoSettings; private readonly IGlobalSettings _globalSettings; private readonly UrlProviderCollection _urlProviders; + private readonly MediaUrlProviderCollection _mediaUrlProviders; private readonly IUserService _userService; /// /// Initializes a new instance of the class. /// - public UmbracoContextFactory(IUmbracoContextAccessor umbracoContextAccessor, IPublishedSnapshotService publishedSnapshotService, IVariationContextAccessor variationContextAccessor, IDefaultCultureAccessor defaultCultureAccessor, IUmbracoSettingsSection umbracoSettings, IGlobalSettings globalSettings, UrlProviderCollection urlProviders, IUserService userService) + public UmbracoContextFactory(IUmbracoContextAccessor umbracoContextAccessor, IPublishedSnapshotService publishedSnapshotService, IVariationContextAccessor variationContextAccessor, IDefaultCultureAccessor defaultCultureAccessor, IUmbracoSettingsSection umbracoSettings, IGlobalSettings globalSettings, UrlProviderCollection urlProviders, MediaUrlProviderCollection mediaUrlProviders, IUserService userService) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); _publishedSnapshotService = publishedSnapshotService ?? throw new ArgumentNullException(nameof(publishedSnapshotService)); @@ -44,6 +45,7 @@ namespace Umbraco.Web _umbracoSettings = umbracoSettings ?? throw new ArgumentNullException(nameof(umbracoSettings)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); _urlProviders = urlProviders ?? throw new ArgumentNullException(nameof(urlProviders)); + _mediaUrlProviders = mediaUrlProviders ?? throw new ArgumentNullException(nameof(mediaUrlProviders)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); } @@ -55,7 +57,7 @@ namespace Umbraco.Web var webSecurity = new WebSecurity(httpContext, _userService, _globalSettings); - return new UmbracoContext(httpContext, _publishedSnapshotService, webSecurity, _umbracoSettings, _urlProviders, _globalSettings, _variationContextAccessor); + return new UmbracoContext(httpContext, _publishedSnapshotService, webSecurity, _umbracoSettings, _urlProviders, _mediaUrlProviders, _globalSettings, _variationContextAccessor); } /// From a3ee7188246ede5bd1b257a1a6f81443236b8416 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Tue, 16 Apr 2019 13:52:39 +0200 Subject: [PATCH 078/201] Add media url provider tests --- .../Routing/MediaUrlProviderTests.cs | 152 ++++++++++++++++++ .../TestHelpers/TestWithDatabaseBase.cs | 4 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs new file mode 100644 index 0000000000..5de99fdd38 --- /dev/null +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Tests.PublishedContent; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Stubs; +using Umbraco.Tests.Testing; +using Umbraco.Web.Routing; + +namespace Umbraco.Tests.Routing +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] + public class MediaUrlProviderTests : BaseWebTest + { + private DefaultMediaUrlProvider _mediaUrlProvider; + + public override void SetUp() + { + base.SetUp(); + + _mediaUrlProvider = new DefaultMediaUrlProvider(); + } + + public override void TearDown() + { + base.TearDown(); + + _mediaUrlProvider = null; + } + + [Test] + public void Get_Media_Url_Resolves_Url_From_Upload_Property_Editor() + { + const string expected = "/media/rfeiw584/test.jpg"; + + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Resolves_Url_From_Image_Cropper_Property_Editor() + { + const string expected = "/media/rfeiw584/test.jpg"; + + var configuration = new ImageCropperConfiguration(); + var imageCropperValue = JsonConvert.SerializeObject(new ImageCropperValue + { + Src = expected + }); + + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.ImageCropper, imageCropperValue, configuration); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Can_Resolve_Absolute_Url() + { + const string mediaUrl = "/media/rfeiw584/test.jpg"; + var expected = $"http://localhost{mediaUrl}"; + + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, mediaUrl, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Absolute, null, null); + + Assert.AreEqual(expected, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported() + { + var umbracoContext = GetUmbracoContext("/", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.Boolean, "0", null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "test", UrlProviderMode.Absolute, null, null); + + Assert.AreEqual(string.Empty, resolvedUrl); + } + + [Test] + public void Get_Media_Url_Can_Resolve_Variant_Property_Url() + { + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + + var umbracoFilePropertyType = CreatePropertyType(Constants.PropertyEditors.Aliases.UploadField, null, ContentVariation.Culture); + + const string enMediaUrl = "/media/rfeiw584/en.jpg"; + const string daMediaUrl = "/media/uf8ewud2/da.jpg"; + + var property = new SolidPublishedPropertyWithLanguageVariants + { + Alias = "umbracoFile", + PropertyType = umbracoFilePropertyType, + }; + + property.SetValue("en", enMediaUrl, true); + property.SetValue("da", daMediaUrl); + + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), new [] { umbracoFilePropertyType }, ContentVariation.Culture); + var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}}; + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, "umbracoFile", UrlProviderMode.Auto, "da", null); + Assert.AreEqual(daMediaUrl, resolvedUrl); + } + + private static TestPublishedContent CreatePublishedContent(string propertyEditorAlias, object propertyValue, object dataTypeConfiguration) + { + var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing); + + var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), + new[] {umbracoFilePropertyType}, ContentVariation.Nothing); + + return new TestPublishedContent(contentType, 1234, Guid.NewGuid(), + new Dictionary {{"umbracoFile", propertyValue } }, false); + } + + private static PublishedPropertyType CreatePropertyType(string propertyEditorAlias, object dataTypeConfiguration, ContentVariation variation) + { + var uploadDataType = new PublishedDataType(1234, propertyEditorAlias, new Lazy(() => dataTypeConfiguration)); + + var propertyValueConverters = new PropertyValueConverterCollection(new IPropertyValueConverter[] + { + new UploadPropertyConverter(), + new ImageCropperValueConverter(), + }); + + var publishedModelFactory = Mock.Of(); + var publishedContentTypeFactory = new Mock(); + publishedContentTypeFactory.Setup(x => x.GetDataType(It.IsAny())) + .Returns(uploadDataType); + + return new PublishedPropertyType("umbracoFile", 42, true, variation, propertyValueConverters, publishedModelFactory, publishedContentTypeFactory.Object); + } + } +} diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index 94154e7d9c..146cb23c1f 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -353,7 +353,7 @@ namespace Umbraco.Tests.TestHelpers } } - protected UmbracoContext GetUmbracoContext(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null, IGlobalSettings globalSettings = null, IPublishedSnapshotService snapshotService = null) + protected UmbracoContext GetUmbracoContext(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null, IEnumerable mediaUrlProviders = null, IGlobalSettings globalSettings = null, IPublishedSnapshotService snapshotService = null) { // ensure we have a PublishedCachesService var service = snapshotService ?? PublishedSnapshotService as PublishedSnapshotService; @@ -380,7 +380,7 @@ namespace Umbraco.Tests.TestHelpers Factory.GetInstance()), umbracoSettings ?? Factory.GetInstance(), urlProviders ?? Enumerable.Empty(), - Enumerable.Empty(), + mediaUrlProviders ?? Enumerable.Empty(), globalSettings ?? Factory.GetInstance(), new TestVariationContextAccessor()); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 6188f577bb..ddd67df6e5 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -143,6 +143,7 @@ + From 4d8d10f07a5efdae27d11cf99d928c208c5af707 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 16:03:36 +1000 Subject: [PATCH 079/201] adds comments, ignores test for now --- .../Persistence/Factories/MemberTypeReadOnlyFactory.cs | 7 +++++++ .../Persistence/Repositories/MemberTypeRepositoryTest.cs | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index c7ce98a89c..9beeddde63 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -54,6 +54,12 @@ namespace Umbraco.Core.Persistence.Factories // no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation needsSaving = true; + //fixme - this is wrong, this will add the standard prop without a group, yet by default it is attempted to be added to the + // group with the `Membership` alias just like we do in the MemberTypeRepository.PersistNewItem: + // //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType + // entity.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); + // that's exactly how it would need to be done here too, but... need to decide where/how this is done properly. + //Add the standard PropertyType to the current list propertyTypes.Add(standardPropertyType.Value); @@ -154,6 +160,7 @@ namespace Umbraco.Core.Persistence.Factories foreach (var typeDto in dto.PropertyTypes.Where(x => (x.PropertyTypeGroupId.HasValue == false || x.PropertyTypeGroupId.Value == 0) && x.Id.HasValue)) { //Internal dictionary for adding "MemberCanEdit" and "VisibleOnProfile" properties to each PropertyType + memberType.MemberTypePropertyTypes.Add(typeDto.Alias, new MemberTypePropertyProfileAccess(typeDto.ViewOnProfile, typeDto.CanEdit, typeDto.IsSensitive)); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 0b655004da..f84c477fa9 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -236,6 +236,7 @@ namespace Umbraco.Tests.Persistence.Repositories /// This demonstates an issue found: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698 /// [Test] + [Ignore("Still testing")] public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception() { //TODO: Fix this bug and then change this test @@ -254,7 +255,9 @@ namespace Umbraco.Tests.Persistence.Repositories prop.Alias = prop.Alias + "__0000"; } - Assert.Throws(() => repository.Save(memberType)); + repository.Save(memberType); + + //Assert.Throws(() => ); } } From 522fdefb8c0535594be34d61c6b2b2be1056be1c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 16:42:17 +1000 Subject: [PATCH 080/201] Ensures that ALL membership properties are created by the installer, removes the logic that updates a membertype while fetching it, fixes the logic of mapping membership properties when fetching and they don't exist. --- .../Migrations/Install/DatabaseDataCreator.cs | 2 + .../Factories/MemberTypeReadOnlyFactory.cs | 45 +++++++++---------- .../Implement/MemberTypeRepository.cs | 8 +--- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index f0e6dd2e5b..1de983636b 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -226,6 +226,8 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); } diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index 9beeddde63..bafd73067f 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -9,11 +9,10 @@ namespace Umbraco.Core.Persistence.Factories { internal static class MemberTypeReadOnlyFactory { - public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto, out bool needsSaving) + public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto) { var standardPropertyTypes = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); - needsSaving = false; - + var memberType = new MemberType(dto.ParentId); try @@ -41,33 +40,29 @@ namespace Umbraco.Core.Persistence.Factories var propertyTypeGroupCollection = GetPropertyTypeGroupCollection(dto, memberType, standardPropertyTypes); memberType.PropertyGroups = propertyTypeGroupCollection; - var propertyTypes = GetPropertyTypes(dto, memberType, standardPropertyTypes); + memberType.NoGroupPropertyTypes = GetNoGroupPropertyTypes(dto, memberType, standardPropertyTypes); - //By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before. + // By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before. + // In theory this should not happen! The only reason this did happen was because: + // A) we didn't install all of the default membership properties by default + // B) the existing data is super old when we didn't store membership properties at all (very very old) + // So what to do? We absolutely do not want to update the database when only a read was requested, this will cause problems + // so we should just add these virtual properties, they will have no ids but they will have aliases and this should be perfectly + // fine for any membership provider logic to work since neither membership providers or asp.net identity care about whether a property + // has an ID or not. + // When the member type is saved, all the properties will correctly be added. + + //This will add this group if it doesn't exist, no need to error check here + memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); foreach (var standardPropertyType in standardPropertyTypes) { - if (dto.PropertyTypes.Any(x => x.Alias.Equals(standardPropertyType.Key))) continue; + //This will add the property if it doesn't exist, no need to error check here + memberType.AddPropertyType(standardPropertyType.Value, Constants.Conventions.Member.StandardPropertiesGroupName); - // beware! - // means that we can return a memberType "from database" that has some property types - // that do *not* come from the database and therefore are incomplete eg have no key, - // no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation - needsSaving = true; - - //fixme - this is wrong, this will add the standard prop without a group, yet by default it is attempted to be added to the - // group with the `Membership` alias just like we do in the MemberTypeRepository.PersistNewItem: - // //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType - // entity.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName); - // that's exactly how it would need to be done here too, but... need to decide where/how this is done properly. - - //Add the standard PropertyType to the current list - propertyTypes.Add(standardPropertyType.Value); - //Internal dictionary for adding "MemberCanEdit", "VisibleOnProfile", "IsSensitive" properties to each PropertyType - memberType.MemberTypePropertyTypes.Add(standardPropertyType.Key, - new MemberTypePropertyProfileAccess(false, false, false)); + if (!memberType.MemberTypePropertyTypes.TryGetValue(standardPropertyType.Key, out var memberTypePropertyProfile)) + memberType.MemberTypePropertyTypes[standardPropertyType.Key] = new MemberTypePropertyProfileAccess(false, false, false); } - memberType.NoGroupPropertyTypes = propertyTypes; return memberType; } @@ -153,7 +148,7 @@ namespace Umbraco.Core.Persistence.Factories return propertyGroups; } - private static List GetPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary standardProps) + private static List GetNoGroupPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary standardProps) { //Find PropertyTypes that does not belong to a PropertyTypeGroup var propertyTypes = new List(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index afb6ac8b43..ffeff13207 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -308,13 +308,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dtos == null || dtos.Any() == false) return Enumerable.Empty(); - return dtos.Select(x => - { - bool needsSaving; - var memberType = MemberTypeReadOnlyFactory.BuildEntity(x, out needsSaving); - if (needsSaving) PersistUpdatedItem(memberType); - return memberType; - }).ToList(); + return dtos.Select(MemberTypeReadOnlyFactory.BuildEntity).ToList(); } /// From 0b6a369047cc301574f403e6a8d96791dacbc0ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 17:12:34 +1000 Subject: [PATCH 081/201] When removing a property group, we need to first remove the group and then re-assign the property types so that duplicates are not added --- src/Umbraco.Core/Models/ContentTypeBase.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 04fac1a855..23e83e0e21 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -409,15 +409,16 @@ namespace Umbraco.Core.Models var group = PropertyGroups[propertyGroupName]; if (group == null) return; - // re-assign the group's properties to no group + // first remove the group + PropertyGroups.RemoveItem(propertyGroupName); + + // Then re-assign the group's properties to no group foreach (var property in group.PropertyTypes) { property.PropertyGroupId = null; _noGroupPropertyTypes.Add(property); } - // actually remove the group - PropertyGroups.RemoveItem(propertyGroupName); OnPropertyChanged(nameof(PropertyGroups)); } From a2a247b41337ca4b1d70e7188757dd47a7448f65 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Apr 2019 21:00:15 +1000 Subject: [PATCH 082/201] removes the enforcement of non duplicate property type aliases for now --- src/Umbraco.Core/Models/ContentTypeBase.cs | 25 +++++++++++--------- src/Umbraco.Tests/Models/ContentTypeTests.cs | 2 ++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 23e83e0e21..725ebe1555 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -95,17 +95,20 @@ namespace Umbraco.Core.Models protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { - //detect if there are any duplicate aliases - this cannot be allowed - if (e.Action == NotifyCollectionChangedAction.Add - || e.Action == NotifyCollectionChangedAction.Replace) - { - var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); - if (allAliases.HasDuplicates(false)) - { - var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); - throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); - } - } + //enable this to detect duplicate property aliases. We do want this, however making this change in a + //patch release might be a little dangerous + + ////detect if there are any duplicate aliases - this cannot be allowed + //if (e.Action == NotifyCollectionChangedAction.Add + // || e.Action == NotifyCollectionChangedAction.Replace) + //{ + // var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias); + // if (allAliases.HasDuplicates(false)) + // { + // var newAliases = string.Join(", ", e.NewItems.Cast().Select(x => x.Alias)); + // throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}"); + // } + //} OnPropertyChanged(nameof(PropertyTypes)); } diff --git a/src/Umbraco.Tests/Models/ContentTypeTests.cs b/src/Umbraco.Tests/Models/ContentTypeTests.cs index 3b35bb2dfc..9c3b976bf3 100644 --- a/src/Umbraco.Tests/Models/ContentTypeTests.cs +++ b/src/Umbraco.Tests/Models/ContentTypeTests.cs @@ -16,6 +16,7 @@ namespace Umbraco.Tests.Models public class ContentTypeTests : UmbracoTestBase { [Test] + [Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")] public void Cannot_Add_Duplicate_Property_Aliases() { var contentType = MockedContentTypes.CreateBasicContentType(); @@ -32,6 +33,7 @@ namespace Umbraco.Tests.Models } [Test] + [Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")] public void Cannot_Update_Duplicate_Property_Aliases() { var contentType = MockedContentTypes.CreateBasicContentType(); From 7db0440b5c08927a5c753fc3e49a0446ee7fc2ea Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 16 Apr 2019 10:11:16 +0200 Subject: [PATCH 083/201] Merge branch 'v7/dev' into v8/dev # Conflicts: # src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs # src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs # src/Umbraco.Core/Services/ContentService.cs # src/Umbraco.Core/Services/IContentService.cs # src/Umbraco.Core/UriExtensions.cs # src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js # src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js # src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js # src/Umbraco.Web.UI.Client/src/common/services/search.service.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html # src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html # src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html # src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html # src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js # src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html # src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs # src/Umbraco.Web/Editors/ContentController.cs # src/Umbraco.Web/Editors/EntityController.cs # src/Umbraco.Web/Editors/MediaController.cs # src/Umbraco.Web/PropertyEditors/ContentPicker2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MediaPicker2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePicker2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RichTextPreValueEditor.cs # src/Umbraco.Web/PublishedCache/ContextualPublishedCache.cs # src/Umbraco.Web/PublishedCache/ContextualPublishedContentCache.cs # src/Umbraco.Web/PublishedCache/ContextualPublishedMediaCache.cs # src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs # src/Umbraco.Web/Search/UmbracoTreeSearcher.cs # src/Umbraco.Web/Trees/ContentTreeControllerBase.cs # src/Umbraco.Web/Trees/TreeControllerBase.cs # src/Umbraco.Web/Trees/TreeQueryStringParameters.cs # src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs # src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs # src/Umbraco.Web/umbraco.presentation/content.cs # src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs --- .../tree/umbtreesearchbox.directive.js | 7 ++ .../src/common/resources/content.resource.js | 17 ++- .../src/common/resources/entity.resource.js | 41 +++++-- .../src/common/resources/media.resource.js | 4 +- .../src/common/services/search.service.js | 20 +++- .../src/common/services/tinymce.service.js | 30 ++++- .../linkpicker/linkpicker.controller.js | 27 ++++- .../linkpicker/linkpicker.html | 2 + .../mediapicker/mediapicker.controller.js | 37 ++++-- .../treepicker/treepicker.controller.js | 4 + .../treepicker/treepicker.html | 3 +- .../contentpicker/contentpicker.controller.js | 5 +- .../contentpicker/contentpicker.html | 1 + .../grid/editors/media.controller.js | 17 ++- .../mediapicker/mediapicker.controller.js | 43 ++++--- .../multiurlpicker.controller.js | 3 +- .../relatedlinks/relatedlinks.controller.js | 2 + .../propertyeditors/rte/rte.prevalues.html | 8 ++ src/Umbraco.Web.UI/favicon.ico | Bin 0 -> 15406 bytes src/Umbraco.Web/Editors/EntityController.cs | 108 ++++++++++++------ src/Umbraco.Web/Editors/MediaController.cs | 61 +++++++++- .../ContentPickerConfiguration.cs | 3 + .../PropertyEditors/GridConfiguration.cs | 3 + .../MediaPickerConfiguration.cs | 3 + .../MultiNodePickerConfiguration.cs | 3 + .../MultiUrlPickerConfiguration.cs | 3 + .../PropertyEditors/RichTextConfiguration.cs | 3 + src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 39 ++++++- .../Trees/ContentTreeControllerBase.cs | 11 +- src/Umbraco.Web/Trees/TreeControllerBase.cs | 10 ++ .../Trees/TreeQueryStringParameters.cs | 1 + ...EnsureUserPermissionForContentAttribute.cs | 42 ++++--- .../FilterAllowedOutgoingMediaAttribute.cs | 9 +- 33 files changed, 449 insertions(+), 121 deletions(-) create mode 100644 src/Umbraco.Web.UI/favicon.ico diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index 4ba4cf96bb..b81e62a66b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -12,6 +12,7 @@ function treeSearchBox(localizationService, searchService, $q) { searchFromName: "@", showSearch: "@", section: "@", + ignoreUserStartNodes: "@", hideSearchCallback: "=", searchCallback: "=" }, @@ -34,6 +35,7 @@ function treeSearchBox(localizationService, searchService, $q) { scope.showSearch = "false"; } + //used to cancel any request in progress if another one needs to take it's place var canceler = null; @@ -60,6 +62,11 @@ function treeSearchBox(localizationService, searchService, $q) { searchArgs["searchFrom"] = scope.searchFromId; } + //append ignoreUserStartNodes value if there is one + if (scope.ignoreUserStartNodes) { + searchArgs["ignoreUserStartNodes"] = scope.ignoreUserStartNodes; + } + searcher(searchArgs).then(function (data) { scope.searchCallback(data); //set back to null so it can be re-created diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index b807a4dc31..d571de0e2d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -365,17 +365,28 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * * @param {Int} id id of content item to return - * @param {Int} culture optional culture to retrieve the item in + * @param {Bool} options.ignoreUserStartNodes set to true to allow a user to choose nodes that they normally don't have access to * @returns {Promise} resourcePromise object containing the content item. * */ - getById: function (id) { + getById: function (id, options) { + var defaults = { + ignoreUserStartNodes: false + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentApiBaseUrl", "GetById", - { id: id })), + [{ id: id }, { ignoreUserStartNodes: options.ignoreUserStartNodes }])), 'Failed to retrieve data for content id ' + id) .then(function (result) { return $q.when(umbDataFormatter.formatContentGetData(result)); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 753d180880..d5145727ac 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -284,15 +284,31 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity. * */ - getAncestors: function (id, type, culture) { + getAncestors: function (id, type, culture, options) { + var defaults = { + ignoreUserStartNodes: false + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; if (culture === undefined) culture = ""; return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetAncestors", - [{ id: id }, { type: type }, { culture: culture }])), - 'Failed to retrieve ancestor data for id ' + id); + [ + { id: id }, + { type: type }, + { culture: culture }, + { ignoreUserStartNodes: options.ignoreUserStartNodes } + ])), + + 'Failed to retrieve ancestor data for id ' + id); }, /** @@ -424,7 +440,8 @@ function entityResource($q, $http, umbRequestHelper) { pageNumber: 1, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + ignoreUserStartNodes: false }; if (options === undefined) { options = {}; @@ -453,7 +470,8 @@ function entityResource($q, $http, umbRequestHelper) { pageSize: options.pageSize, orderBy: options.orderBy, orderDirection: options.orderDirection, - filter: encodeURIComponent(options.filter) + filter: encodeURIComponent(options.filter), + ignoreUserStartNodes: options.ignoreUserStartNodes } )), 'Failed to retrieve child data for id ' + parentId); @@ -481,12 +499,19 @@ function entityResource($q, $http, umbRequestHelper) { * @returns {Promise} resourcePromise object containing the entity array. * */ - search: function (query, type, searchFrom, canceler) { + search: function (query, type, options, canceler) { var args = [{ query: query }, { type: type }]; - if (searchFrom) { - args.push({ searchFrom: searchFrom }); + + if(options !== undefined) { + if (options.searchFrom) { + args.push({ searchFrom: options.searchFrom }); + } + if (options.ignoreUserStartNodes) { + args.push({ ignoreUserStartNodes: options.ignoreUserStartNodes }); + } } + var httpConfig = {}; if (canceler) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 1d6d5171a1..462184c9f2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -329,7 +329,8 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { filter: '', orderDirection: "Ascending", orderBy: "SortOrder", - orderBySystemField: true + orderBySystemField: true, + ignoreUserStartNodes: false }; if (options === undefined) { options = {}; @@ -367,6 +368,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { "GetChildren", [ { id: parentId }, + { ignoreUserStartNodes: options.ignoreUserStartNodes }, { pageNumber: options.pageNumber }, { pageSize: options.pageSize }, { orderBy: options.orderBy }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index 04c431767c..a2010d20f2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -42,7 +42,11 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) { + var options = { + searchFrom: args.searchFrom + } + + return entityResource.search(args.term, "Member", options).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureMemberResult(item); }); @@ -67,7 +71,12 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { + var options = { + searchFrom: args.searchFrom, + ignoreUserStartNodes: args.ignoreUserStartNodes + } + + return entityResource.search(args.term, "Document", options, args.canceler).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureContentResult(item); }); @@ -92,7 +101,12 @@ angular.module('umbraco.services') throw "args.term is required"; } - return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { + var options = { + searchFrom: args.searchFrom, + ignoreUserStartNodes: args.ignoreUserStartNodes + } + + return entityResource.search(args.term, "Media", options).then(function (data) { _.each(data, function (item) { searchResultFormatter.configureMediaResult(item); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index ce4bf6077c..3c4f5a7d73 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -1143,11 +1143,25 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s let self = this; + function getIgnoreUserStartNodes(args) { + var ignoreUserStartNodes = false; + // Most property editors have a "config" property with ignoreUserStartNodes on then + if (args.model.config) { + ignoreUserStartNodes = Object.toBoolean(args.model.config.ignoreUserStartNodes); + } + // EXCEPT for the grid's TinyMCE editor, that one wants to be special and the config is called "configuration" instead + else if (args.model.configuration) { + ignoreUserStartNodes = Object.toBoolean(args.model.configuration.ignoreUserStartNodes); + } + return ignoreUserStartNodes; + } + //create link picker self.createLinkPicker(args.editor, function (currentTarget, anchorElement) { var linkPicker = { currentTarget: currentTarget, anchors: editorState.current ? self.getAnchorNames(JSON.stringify(editorState.current.properties)) : [], + ignoreUserStartNodes: getIgnoreUserStartNodes(args), submit: function (model) { self.insertLinkInEditor(args.editor, model.target, anchorElement); editorService.close(); @@ -1161,13 +1175,25 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //Create the insert media plugin self.createMediaPicker(args.editor, function (currentTarget, userData) { + + var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + var startNodeIsVirtual = userData.startMediaIds.length !== 1; + + var ignoreUserStartNodes = getIgnoreUserStartNodes(args); + if (ignoreUserStartNodes) { + ignoreUserStartNodes = true; + startNodeId = -1; + startNodeIsVirtual = true; + } + var mediaPicker = { currentTarget: currentTarget, onlyImages: true, showDetails: true, disableFolderSelect: true, - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, submit: function (model) { self.insertMediaInEditor(args.editor, model.selection[0]); editorService.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index f4725fa82d..1e3cf54450 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -28,9 +28,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", searchFromName: null, showSearch: false, results: [], - selectedSearchResults: [] + selectedSearchResults: [], + ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes }; + $scope.customTreeParams = dialogOptions.ignoreUserStartNodes ? "ignoreUserStartNodes=" + dialogOptions.ignoreUserStartNodes : ""; $scope.showTarget = $scope.model.hideTarget !== true; // this ensures that we only sync the tree once and only when it's ready @@ -73,7 +75,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }); // get the content properties to build the anchor name list - contentResource.getById(id).then(function (resp) { + + var options = {}; + options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes; + + contentResource.getById(id, options).then(function (resp) { $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); $scope.model.target.url = resp.urls[0].text; }); @@ -119,7 +125,10 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (args.node.id < 0) { $scope.model.target.url = "/"; } else { - contentResource.getById(args.node.id).then(function (resp) { + var options = {}; + options.ignoreUserStartNodes = dialogOptions.ignoreUserStartNodes; + + contentResource.getById(args.node.id, options).then(function (resp) { $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); $scope.model.target.url = resp.urls[0].text; }); @@ -139,9 +148,17 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { + var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + var startNodeIsVirtual = userData.startMediaIds.length !== 1; + if (dialogOptions.ignoreUserStartNodes) { + startNodeId = -1; + startNodeIsVirtual = true; + } + var mediaPicker = { - startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0], - startNodeIsVirtual: userData.startMediaIds.length !== 1, + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + ignoreUserStartNodes: dialogOptions.ignoreUserStartNodes, submit: function (model) { var media = model.selection[0]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 71fcf2f493..414dbecfc2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -68,6 +68,7 @@ search-from-id="{{searchInfo.searchFromId}}" search-from-name="{{searchInfo.searchFromName}}" show-search="{{searchInfo.showSearch}}" + ignore-user-start-nodes="{{searchInfo.ignoreUserStartNodes}}" section="{{section}}"> @@ -84,6 +85,7 @@ section="content" hideheader="true" hideoptions="true" + customtreeparams="{{customTreeParams}}" api="dialogTreeApi" on-init="onTreeInit()" enablelistviewexpand="true" diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 2d6a2be471..2ba2300730 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -20,11 +20,13 @@ angular.module("umbraco") $scope.showDetails = dialogOptions.showDetails; $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; + $scope.ignoreUserStartNodes = Object.toBoolean(dialogOptions.ignoreUserStartNodes); $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); $scope.lockedFolder = true; $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; + var userStartNodes = []; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if ($scope.onlyImages) { @@ -54,7 +56,8 @@ angular.module("umbraco") pageSize: 100, totalItems: 0, totalPages: 0, - filter: '' + filter: "", + ignoreUserStartNodes: $scope.model.ignoreUserStartNodes }; //preload selected item @@ -66,7 +69,7 @@ angular.module("umbraco") function onInit() { if ($scope.startNodeId !== -1) { entityResource.getById($scope.startNodeId, "media") - .then(function (ent) { + .then(function(ent) { $scope.startNodeId = ent.id; run(); }); @@ -143,7 +146,7 @@ angular.module("umbraco") } }; - $scope.gotoFolder = function(folder) { + $scope.gotoFolder = function (folder) { if (!$scope.multiPicker) { deselectAllImages($scope.model.selection); } @@ -152,8 +155,10 @@ angular.module("umbraco") folder = { id: -1, name: "Media", icon: "icon-folder" }; } + var options = {}; if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media") + options.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes; + entityResource.getAncestors(folder.id, "media", options) .then(function(anc) { $scope.path = _.filter(anc, function(f) { @@ -169,13 +174,26 @@ angular.module("umbraco") $scope.path = []; } - $scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual; + $scope.lockedFolder = (folder.id === -1 && $scope.model.startNodeIsVirtual) || hasFolderAccess(folder) === false; + $scope.currentFolder = folder; localStorageService.set("umbLastOpenedMediaNodeId", folder.id); - return getChildren(folder.id); + options.ignoreUserStartNodes = $scope.ignoreUserStartNodes; + return getChildren(folder.id, options); }; + function hasFolderAccess(node) { + var nodePath = node.path ? node.path.split(',') : [node.id]; + + for (var i = 0; i < nodePath.length; i++) { + if (userStartNodes.indexOf(parseInt(nodePath[i])) !== -1) + return true; + } + + return false; + } + $scope.clickHandler = function(image, event, index) { if (image.isFolder) { if ($scope.disableFolderSelect) { @@ -299,7 +317,8 @@ angular.module("umbraco") pageSize: 100, totalItems: 0, totalPages: 0, - filter: '' + filter: "", + ignoreUserStartNodes: $scope.model.ignoreUserStartNodes }; getChildren($scope.currentFolder.id); } @@ -367,9 +386,9 @@ angular.module("umbraco") } } - function getChildren(id) { + function getChildren(id, options) { $scope.loading = true; - return mediaResource.getChildren(id) + return mediaResource.getChildren(id, options) .then(function(data) { $scope.searchOptions.filter = ""; $scope.images = data.items ? data.items : []; 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 5883313753..a6e2838b56 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 @@ -36,6 +36,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", selectedSearchResults: [] } vm.startNodeId = $scope.model.startNodeId; + vm.ignoreUserStartNodes = $scope.model.ignoreUserStartNodes; //Used for toggling an empty-state message //Some trees can have no items (dictionary & forms email templates) vm.hasItems = true; @@ -171,6 +172,9 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if (vm.startNodeId) { queryParams["startNodeId"] = $scope.model.startNodeId; } + if (vm.ignoreUserStartNodes) { + queryParams["ignoreUserStartNodes"] = $scope.model.ignoreUserStartNodes; + } if (vm.selectedLanguage && vm.selectedLanguage.id) { queryParams["culture"] = vm.selectedLanguage.culture; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html index c592b4ec3b..acd838f7bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html @@ -27,7 +27,7 @@ {{language.name}} - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 866cfb54ab..894ad2eedf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -81,6 +81,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper showOpenButton: false, showEditButton: false, showPathOnHover: false, + ignoreUserStartNodes: false, maxNumber: 1, minNumber: 0, startNode: { @@ -118,7 +119,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper $scope.model.config.showOpenButton = Object.toBoolean($scope.model.config.showOpenButton); $scope.model.config.showEditButton = Object.toBoolean($scope.model.config.showEditButton); $scope.model.config.showPathOnHover = Object.toBoolean($scope.model.config.showPathOnHover); - + $scope.model.config.ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes); + var entityType = $scope.model.config.startNode.type === "member" ? "Member" : $scope.model.config.startNode.type === "media" @@ -134,6 +136,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper entityType: entityType, filterCssClass: "not-allowed not-published", startNodeId: null, + ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes, currentNode: editorState ? editorState.current : null, callback: function (data) { if (angular.isArray(data)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index a589cf8947..be4dbb9b12 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -14,6 +14,7 @@ sortable="!sortableOptions.disabled" allow-remove="allowRemoveButton" allow-open="model.config.showOpenButton && allowOpenButton && !dialogEditor" + ignore-user-startnodes="model.config.ignoreUserStartNodes" on-remove="remove($index)" on-open="openContentEditor(node)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index eb1032a9c7..71bb51f686 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,25 +1,32 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", function ($scope, $timeout, userService, editorService) { + var ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes); $scope.thumbnailUrl = getThumbnailUrl(); if (!$scope.model.config.startNodeId) { - userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; - $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; - }); + if (ignoreUserStartNodes === true) { + $scope.model.config.startNodeId = -1; + $scope.model.config.startNodeIsVirtual = true; + + } else { + userService.getCurrentUser().then(function (userData) { + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + }); + } } $scope.setImage = function(){ var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - var mediaPicker = { startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined, showDetails: true, disableFolderSelect: true, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index bac8eb903a..c937360693 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -7,9 +7,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false; + var ignoreUserStartNodes = Object.toBoolean($scope.model.config.ignoreUserStartNodes); $scope.allowEditMedia = false; $scope.allowAddMedia = false; + + function setupViewModel() { $scope.mediaItems = []; $scope.ids = []; @@ -90,26 +93,31 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // reload. We only reload the images that is already picked but has been updated. // We have to get the entities from the server because the media // can be edited without being selected - _.each($scope.images, function (image, i) { - if (updatedMediaNodes.indexOf(image.udi) !== -1) { - image.loading = true; - entityResource.getById(image.udi, "media") - .then(function (mediaEntity) { - angular.extend(image, mediaEntity); - image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); - image.loading = false; - }); - } - }) + _.each($scope.images, + function (image, i) { + if (updatedMediaNodes.indexOf(image.udi) !== -1) { + image.loading = true; + entityResource.getById(image.udi, "media") + .then(function (mediaEntity) { + angular.extend(image, mediaEntity); + image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); + image.loading = false; + }); + } + }); } function init() { - userService.getCurrentUser().then(function (userData) { if (!$scope.model.config.startNodeId) { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; } + if (ignoreUserStartNodes === true) { + $scope.model.config.startNodeId = -1; + $scope.model.config.startNodeIsVirtual = true; + } + // only allow users to add and edit media if they have access to the media section var hasAccessToMedia = userData.allowedSections.indexOf("media") !== -1; $scope.allowEditMedia = hasAccessToMedia; @@ -167,12 +175,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl var mediaPicker = { startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, + ignoreUserStartNodes: ignoreUserStartNodes, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, allowMediaEdit: true, - submit: function(model) { + submit: function (model) { editorService.close(); @@ -182,7 +191,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } - $scope.mediaItems.push(media); + $scope.mediaItems.push(media); if ($scope.model.config.idType === "udi") { $scope.ids.push(media.udi); @@ -205,7 +214,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; - + $scope.sortableOptions = { disabled: !$scope.isMultiPicker, @@ -217,7 +226,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. $timeout(function () { - angular.forEach($scope.mediaItems, function(value, key) { + angular.forEach($scope.mediaItems, function (value, key) { r.push($scope.model.config.idType === "udi" ? value.udi : value.id); }); $scope.ids = r; @@ -236,5 +245,5 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; init(); - + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 063a726f44..5b02479813 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -69,9 +69,10 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en url: link.url, target: link.target } : null; - + var linkPicker = { currentTarget: target, + ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes), submit: function (model) { if (model.target.url || model.target.anchor) { // if an anchor exists, check that it is appropriately prefixed diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index d54a17e15a..979baef0f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -25,6 +25,7 @@ section: "content", treeAlias: "content", multiPicker: false, + ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes), idType: $scope.model.config.idType ? $scope.model.config.idType : "int", submit: function (model) { select(model.selection[0]); @@ -47,6 +48,7 @@ section: "content", treeAlias: "content", multiPicker: false, + ignoreUserStartNodes: Object.toBoolean($scope.model.config.ignoreUserStartNodes), idType: $scope.model.config.idType ? $scope.model.config.idType : "udi", submit: function (model) { select(model.selection[0]); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 6314e0b31e..13515ca7a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -28,6 +28,14 @@ + +
    + +
    +
    +
    × diff --git a/src/Umbraco.Web.UI/favicon.ico b/src/Umbraco.Web.UI/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c0749ddf7f1b68606b0672aa8d709779d6b0817a GIT binary patch literal 15406 zcmeHO4YX869ls>@X@QnjYWV^}e8GFWcjn6B>$!Vp$z%56M^t7dom1(l=#bin)j2&i z%2cetQnXWw@WhR`dv_loS%x3T=@5!3T48}ABp~u32!W{m{mFjID={PHn zcbrjx0SYiG^>`%u1tQ&A671(ejz3-!e0M3w_mqUUCh+;Jpo5KxB9d=yeIwzOa^69d z*GvU8UkL>J<&v{Tyh*1+xTs%h!ZFqfcCStYvfWSzC-|%Q-VXWyMJ332FW^aaRk=tG zlA)AsI~DaW%Qk}X`Rt&>d!H^Jy?1BYPA(IlUOLp5LWYkP_rA`HZM16z)^d*zh$3x!-RGoUF4+rm@kUkM|e^u3i_8Qr@ z-hLH*enBC}CRW?E{!(4Rcl~0PpI>dq`0Ds2;a*tIdS_JIL->KQvDYEG0?bH`@XS5h_~kO z`cmV!1MlnA7=>wo*VCxm-dlfgQHOh)91~pz#n1KxZ^EVDvP@<$^iU4-i~JV+ZnF6b zcI7Y8kh)NPG4p>JeVZA^)#;Qn?g+sA6tq@B7uz7iHhe!EYW5#!)>_XXqK0P4kjqjdIw$fjI3P10+pM<#&o>lv( z`ijVL%z3-Wj}$Yh7DcDg=dz4582$M^`u0)`ADb-uFt4tjHKN|H+GOdT#NG?rc&HzI z>*OL|(Cw#BvGJw$qR)oQxoWoaI=YR?$2XO;?H`Jhf%=VcTi+Pn+S*~q#2z*#`tP| z97k3BEGO@-NW%=y<00!iU>V2(s+6m^aI#>3RF~aR_|&6CQ2dL2`Ygyp>qhJx1k4Vszvn2tZlAePIv_ZV>kK1J z*hAhyfQw%~(|QbG)cPFDpJ+jb-T85?$0->wueO+YATId@?6(?s)xltGRQRPFJG-vU zq6300X5CNK@cDg4eBs>>-K>ddyIbKy#%~&7;^gqz532Zr|IUOj*HGbYG_}D#x@-9C zN;6E1EeP-W8a}_>gpYph#MnG*YJ*+^g^&36M@E>K{!=`t@WErA5g)b+86G#aq5h9m ze7O$C^wl5-Y;cCcr@okY$aPM5J;t^iUtPu5V`Bp^BfemlR`CUY&;$b+;kVgUrZ&jD zt%{#*-2M@FOi}pYL-F+?^A_>k4aE%4nA*Vq)t(G{7tttXIqk&`!H=)wX~OMcCt}|a zwfAIacOuS$t^eCdORy}_pjZqr|1rWCX44^btWPLNEMpG>6HKjd+61B;NEZh*9qZ{T)U)L;v~@hui`7tQGE+zvUd8 z)_=MQZA7publ&vd={RqI4(=Iv8TfLtr!ar|E{M}<9R5B~GEi)5i=il=F(5JszHXu5 z!%Y1S*Y)b5d1HZf7BR$o!hGVsJ7Euqt)^lA^?hC-hCyt3xr$@r_(iqJx#{yx%;ilD z*u`eVRzEI`Z#yB1Zlm?ohe9@e24d?gp_4MoV~}S96Xjh(mqKSu{%j{}rC{ zBq@}sD9y-|fQ<1s4>_`ON81PBXORbVzlAw|2jagKu&Z?_n=y`CFosW~eq~vB3yN8G zMKPNev>(ss*pR;NfsDc=uN*{qE6Dj3#7gs|Uy9Fzh1WvLIj@6V-UZ)>9Aw+*PR#4% z?E4OxXKV=Ng)8wbVuyy;Z*47;xO_ioJclwTKd$|xzDpL5xoF~;V~fK59BhK-c37Xy z`9=-_b~UXlGg8KP@e(C(rMxZV?t&ff9WdE@bpjp-U{gi-!?DRFjE1EBHDd9N6gv*I z>^1tti*GF;pAaosd<(4$GT(wRt>^6KqD);$w;xbrZ}u6zRe!1pAzr2>30`n>k=*-X^<@vXKwEzhESJ{PiA z&X*$XGK$AQ)?-c!R*3Vh*iU~Pc9VENtHwzBCLe2%kA11{&BwXo>>|#TD4$COIhSh? z@6H0;T+sg$p0tM08mw$2(UW{Dczj*Ab@iTS!-k;!pNVK9R*>s2@-KyqdtndGaul7U z&pggu!S9oZA-d4F^-{JZ{eeGRYRFf^Pkk?;?EA33+}dl4eab(F`$*^X7?W9;r#Lf9 zFeW~`sZ)!W=sXKDJ!!=yUyA&r%7KLC=+8IhOmJTPW%R+mP9k2?&wSwPzZiSYKstf! z7^4Ly87Ws!d1bl>K1U%` z17-$(YJ7MPOSS-*^|7`9U4rKViiIhc2flxeg@-PR$XU%t464gtp43KX2+!E{WwL1* z<7}d}Qm09Fggx?Ypc|i8!#xp%jVq;mzWc*YZO+iR!5>@uZPN>AtY6mJ zdW_zOQkGqcvDzPpD|4TsJsV`)Puu%Swoo4nlHRO%u{K;A6FS4Im)|=NvRlt};CJa9 zt#%(Ik{{m{E0OY4`VPE1X-&5A!dSgPb4}*~`Xw7) z;&#DT>-%`z{TKt;jJbK8CUp2`z#&&q8%yIm);v1zQ}TCW9$?%aGQ!$u8Qb|fl`|(E zJ@SqZ?s6GnHCZe7y%Nkp#D36;l79|!j>cNB%1YnHzt+Bi?kXwz(ud~wQ#Sf^t`7a% z<%j-}qia|6Y3+gjSKH_(ZKqg@;x9#?^0|=99*YUeFU6e4*YvZk$LDm7QU2r|7~%aT zseCp#_~#9C+uGiy=+jz_KE-n!A~I!yoG`BM~wDEc^K zX3(oWPvi6Dg{X7(n(h(%crTWP`%y)|B0nqkPv+V@q`9QiE<;za&*yCPq1W3H7n~1R z%UbR2#h&EzIuGE@w$V@AuANB}j~;>h3v`E6*P-bhXR!+>ju{5|_gG+YkB?neC%^Ab zx8Nk&Q%M@ay;i4P&LXdBxnBm|A1Gm+TY6^eai9@D7ioNO?^U-^toa{lmi%4?Fv@ca8!CCmE)97Ad{CXs?H^tr`{Jw^AGs=os9fjnKlLvGj#=i8HBIXpzRKObV z1=VQq4Ml(Sa3w!IVT03~CQl=>_W>6B8#?2!%N3Meorg zPJY3kU6>g|$Ewy}DU;^#jT?`M`CRe!&xi9AwoX9y$?&sl4{IMPvOh!rXJV5#+)nFL zNu(xY9L)CsW9M0Od=32J&SHk06V7pkWsUnLTcVuV|9DPQe)}ImCY_6uEomFgX zA28taTIL+tZF+t-shn|e(zoJ&mE;;nb~!Niv$lYJQuwZQoLr5z$^0Z@joyCjK%8#R zcSrSA&QIJ$66^`&a=xqQAR3jcQTWBUcLm?{8N^+`!gD41vpbQml4D2z`$Dk_y9Kf4 zMP#3i@^2PA?H?%jPj_hKnX&KDnHTPr$o>85%G~Dzu<4)1ONa4 literal 0 HcmV?d00001 diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index e648a4d140..05f7b6525c 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -99,8 +99,25 @@ namespace Umbraco.Web.Editors /// A starting point for the search, generally a node id, but for members this is a member type alias /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [HttpGet] public IEnumerable Search(string query, UmbracoEntityTypes type, string searchFrom = null) + { + return Search(query, type, ignoreUserStartNodes: false, searchFrom); + } + + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// If set to true, user and group start node permissions will be ignored. + /// + [HttpGet] + public IEnumerable Search(string query, UmbracoEntityTypes type, bool? ignoreUserStartNodes, string searchFrom = null) { // TODO: Should we restrict search results based on what app the user has access to? // - Theoretically you shouldn't be able to see member data if you don't have access to members right? @@ -110,7 +127,7 @@ namespace Umbraco.Web.Editors //TODO: This uses the internal UmbracoTreeSearcher, this instead should delgate to the ISearchableTree implementation for the type - return ExamineSearch(query, type, searchFrom); + return ExamineSearch(query, type, searchFrom, ignoreUserStartNodes != null && ignoreUserStartNodes.Value); } /// @@ -534,6 +551,7 @@ namespace Umbraco.Web.Editors } } + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] public PagedResult GetPagedDescendants( int id, UmbracoEntityTypes type, @@ -542,6 +560,20 @@ namespace Umbraco.Web.Editors string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") + { + return GetPagedDescendants(id, type, pageNumber, pageSize, + ignoreUserStartNodes: false, orderBy, orderDirection, filter); + } + + public PagedResult GetPagedDescendants( + int id, + UmbracoEntityTypes type, + int pageNumber, + int pageSize, + bool ignoreUserStartNodes, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + string filter = "") { if (pageNumber <= 0) throw new HttpResponseException(HttpStatusCode.NotFound); @@ -569,7 +601,7 @@ namespace Umbraco.Web.Editors break; } - entities = aids == null || aids.Contains(Constants.System.Root) + entities = aids == null || aids.Contains(Constants.System.Root) || ignoreUserStartNodes ? Services.EntityService.GetPagedDescendants(objectType.Value, pageNumber - 1, pageSize, out totalRecords, SqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection), includeTrashed: false) @@ -611,9 +643,15 @@ namespace Umbraco.Web.Editors } } - public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings) + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormDataCollection queryStrings) { - return GetResultForAncestors(id, type, queryStrings); + return GetResultForAncestors(id, type, queryStrings, ignoreUserStartNodes: false); + } + + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings, bool ignoreUserStartNodes) + { + return GetResultForAncestors(id, type, queryStrings, ignoreUserStartNodes); } /// @@ -622,10 +660,11 @@ namespace Umbraco.Web.Editors /// /// /// + /// If set to true, user and group start node permissions will be ignored. /// - private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) + private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null, bool ignoreUserStartNodes = false) { - return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, searchFrom); + return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, ignoreUserStartNodes, searchFrom); } private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) @@ -651,7 +690,7 @@ namespace Umbraco.Web.Editors } } - private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null) + private IEnumerable GetResultForAncestors(int id, UmbracoEntityTypes entityType, FormDataCollection queryStrings = null, bool ignoreUserStartNodes = false) { var objectType = ConvertToObjectType(entityType); if (objectType.HasValue) @@ -660,35 +699,38 @@ namespace Umbraco.Web.Editors var ids = Services.EntityService.Get(id).Path.Split(',').Select(int.Parse).Distinct().ToArray(); - int[] aids = null; - switch (entityType) + if (ignoreUserStartNodes == false) { - case UmbracoEntityTypes.Document: - aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); - break; - case UmbracoEntityTypes.Media: - aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); - break; - } - - if (aids != null) - { - var lids = new List(); - var ok = false; - foreach (var i in ids) + int[] aids = null; + switch (entityType) { - if (ok) - { - lids.Add(i); - continue; - } - if (aids.Contains(i)) - { - lids.Add(i); - ok = true; - } + case UmbracoEntityTypes.Document: + aids = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService); + break; + case UmbracoEntityTypes.Media: + aids = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService); + break; + } + + if (aids != null) + { + var lids = new List(); + var ok = false; + foreach (var i in ids) + { + if (ok) + { + lids.Add(i); + continue; + } + if (aids.Contains(i)) + { + lids.Add(i); + ok = true; + } + } + ids = lids.ToArray(); } - ids = lids.ToArray(); } var culture = queryStrings?.GetValue("culture"); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 3b7142397b..6710cf59f6 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -246,6 +246,7 @@ namespace Umbraco.Web.Editors /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(int id, + bool ignoreUserStartNodes, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -255,7 +256,7 @@ namespace Umbraco.Web.Editors { //if a request is made for the root node data but the user's start node is not the default, then // we need to return their start nodes - if (id == Constants.System.Root && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + if (id == Constants.System.Root && UserStartNodes.Length > 0 && (UserStartNodes.Contains(Constants.System.Root) == false && ignoreUserStartNodes == false)) { if (pageNumber > 0) return new PagedResult>(0, 0, 0); @@ -311,6 +312,7 @@ namespace Umbraco.Web.Editors } /// + /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Returns the child media objects - using the entity GUID id /// /// @@ -321,8 +323,34 @@ namespace Umbraco.Web.Editors /// /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Guid id, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + return GetChildren(id, ignoreUserStartNodes: false, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Returns the child media objects - using the entity GUID id + /// + /// + /// /// If set to true, user and group start node permissions will be ignored. + /// + /// + /// + /// + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren(Guid id, + bool ignoreUserStartNodes, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -333,12 +361,13 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.Get(id); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, ignoreUserStartNodes, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); } throw new HttpResponseException(HttpStatusCode.NotFound); } /// + /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Returns the child media objects - using the entity UDI id /// /// @@ -349,6 +378,7 @@ namespace Umbraco.Web.Editors /// /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(Udi id, int pageNumber = 0, @@ -357,6 +387,31 @@ namespace Umbraco.Web.Editors Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + return GetChildren(id, ignoreUserStartNodes: false, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Returns the child media objects - using the entity UDI id + /// + /// + /// If set to true, user and group start node permissions will be ignored. + /// + /// + /// + /// + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren(Udi id, + bool ignoreUserStartNodes, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") { var guidUdi = id as GuidUdi; if (guidUdi != null) @@ -364,7 +419,7 @@ namespace Umbraco.Web.Editors var entity = Services.EntityService.Get(guidUdi.Guid); if (entity != null) { - return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + return GetChildren(entity.Id, ignoreUserStartNodes, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); } } diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs index 7879e2b42b..5653e3fe03 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerConfiguration.cs @@ -10,5 +10,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNodeId", "Start node", "treepicker")] // + config in configuration editor ctor public Udi StartNodeId { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs index e2b46b360d..136ab88204 100644 --- a/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/GridConfiguration.cs @@ -15,5 +15,8 @@ namespace Umbraco.Web.PropertyEditors // TODO: Make these strongly typed, for now this works though [ConfigurationField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] public JObject Rte { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.
    Note: this applies to all editors in this grid editor except for the rich text editor, which has it's own option for that.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs index 4844e2f822..fa430e103b 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs @@ -19,5 +19,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNodeId", "Start node", "mediapicker")] public Udi StartNodeId { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs index b6333c3140..a0a2467b1c 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs @@ -22,5 +22,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("showOpenButton", "Show open button (this feature is in preview!)", "boolean", Description = "Opens the node in a dialog")] public bool ShowOpen { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs index 515512eff8..ec9439ceea 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerConfiguration.cs @@ -9,5 +9,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("maxNumber", "Maximum number of items", "number")] public int MaxNumber { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs index 13bf269bcd..d99c2b17e0 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextConfiguration.cs @@ -14,5 +14,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("hideLabel", "Hide Label", "boolean")] public bool HideLabel { get; set; } + + [ConfigurationField("ignoreUserStartNodes", "Ignore user start nodes", "boolean", Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 43db9ff0ba..7fb51a61eb 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -39,6 +39,7 @@ namespace Umbraco.Web.Search } /// + /// This method is obsolete, use the overload with ignoreUserStartNodes instead /// Searches for results based on the entity type /// /// @@ -50,11 +51,39 @@ namespace Umbraco.Web.Search /// /// /// + [Obsolete("This method is obsolete, use the overload with ignoreUserStartNodes instead", false)] public IEnumerable ExamineSearch( string query, UmbracoEntityTypes entityType, int pageSize, - long pageIndex, out long totalFound, string searchFrom = null) + long pageIndex, + out long totalFound, + string searchFrom = null) + { + return ExamineSearch(query, entityType, pageSize, pageIndex, out totalFound, ignoreUserStartNodes: false, searchFrom); + } + + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// + /// + /// If set to true, user and group start node permissions will be ignored. + /// + public IEnumerable ExamineSearch( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + bool ignoreUserStartNodes, + string searchFrom = null) { var sb = new StringBuilder(); @@ -85,12 +114,12 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); - AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, _entityService); + AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); - AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, _entityService); + AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); @@ -288,7 +317,7 @@ namespace Umbraco.Web.Search } } - private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, IEntityService entityService) + private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, bool ignoreUserStartNodes, IEntityService entityService) { if (sb == null) throw new ArgumentNullException(nameof(sb)); if (entityService == null) throw new ArgumentNullException(nameof(entityService)); @@ -311,7 +340,7 @@ namespace Umbraco.Web.Search // make sure we don't find anything sb.Append("+__Path:none "); } - else if (startNodeIds.Contains(-1) == false) // -1 = no restriction + else if (startNodeIds.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction { var entityPaths = entityService.GetAllPaths(objectType, startNodeIds); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 0c02dd6e46..d01e9fffb4 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -69,7 +69,7 @@ namespace Umbraco.Web.Trees { var node = base.CreateRootNode(queryStrings); - if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false) + if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false && IgnoreUserStartNodes(queryStrings) == false) { node.AdditionalData["noAccess"] = true; } @@ -90,11 +90,11 @@ namespace Umbraco.Web.Trees internal TreeNode GetSingleTreeNodeWithAccessCheck(IEntitySlim e, string parentId, FormDataCollection queryStrings) { var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out var hasPathAccess); - if (entityIsAncestorOfStartNodes == false) + if (IgnoreUserStartNodes(queryStrings) == false && entityIsAncestorOfStartNodes == false) return null; var treeNode = GetSingleTreeNode(e, parentId, queryStrings); - if (hasPathAccess == false) + if (IgnoreUserStartNodes(queryStrings) == false && hasPathAccess == false) { treeNode.AdditionalData["noAccess"] = true; } @@ -134,7 +134,7 @@ namespace Umbraco.Web.Trees // ensure that the user has access to that node, otherwise return the empty tree nodes collection // TODO: in the future we could return a validation statement so we can have some UI to notify the user they don't have access - if (HasPathAccess(id, queryStrings) == false) + if (IgnoreUserStartNodes(queryStrings) == false && HasPathAccess(id, queryStrings) == false) { Logger.Warn("User {Username} does not have access to node with id {Id}", Security.CurrentUser.Username, id); return nodes; @@ -255,8 +255,9 @@ namespace Umbraco.Web.Trees /// protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { + var ignoreUserStartNodes = queryStrings.GetValue(TreeQueryStringParameters.IgnoreUserStartNodes); //check if we're rendering the root - if (id == Constants.System.RootString && UserStartNodes.Contains(Constants.System.Root)) + if (id == Constants.System.RootString && UserStartNodes.Contains(Constants.System.Root) || ignoreUserStartNodes) { var altStartId = string.Empty; diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 4acf807b77..2e409c2820 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -369,6 +369,16 @@ namespace Umbraco.Web.Trees return queryStrings.GetValue(TreeQueryStringParameters.Use) == "dialog"; } + /// + /// If the request should allows a user to choose nodes that they normally don't have access to + /// + /// + /// + protected bool IgnoreUserStartNodes(FormDataCollection queryStrings) + { + return queryStrings.GetValue(TreeQueryStringParameters.IgnoreUserStartNodes); + } + /// /// An event that allows developers to modify the tree node collection that is being rendered /// diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 466aff5a1f..0fcf5321e4 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -8,6 +8,7 @@ public const string Use = "use"; public const string Application = "application"; public const string StartNodeId = "startNodeId"; + public const string IgnoreUserStartNodes = "ignoreUserStartNodes"; //public const string OnNodeClick = "OnNodeClick"; //public const string RenderParent = "RenderParent"; } diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index efee045890..67099dc58a 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Models; using Umbraco.Web.Actions; using Umbraco.Core.Security; using System.Net; +using System.Web; namespace Umbraco.Web.WebApi.Filters { @@ -66,7 +67,7 @@ namespace Umbraco.Web.WebApi.Filters //not logged in throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); } - + int nodeId; if (_nodeId.HasValue == false) { @@ -116,24 +117,29 @@ namespace Umbraco.Web.WebApi.Filters nodeId = _nodeId.Value; } - var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, - Current.UmbracoContext.Security.CurrentUser, - Current.Services.UserService, - Current.Services.ContentService, - Current.Services.EntityService, - out var contentItem, - _permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) - throw new HttpResponseException(HttpStatusCode.NotFound); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) - throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); - - if (contentItem != null) + var queryStringCollection = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.Query); + var ignoreUserStartNodes = bool.Parse(queryStringCollection["ignoreUserStartNodes"]); + if (ignoreUserStartNodes == false) { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; + var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, + Current.UmbracoContext.Security.CurrentUser, + Current.Services.UserService, + Current.Services.ContentService, + Current.Services.EntityService, + out var contentItem, + _permissionToCheck.HasValue ? new[] {_permissionToCheck.Value} : null); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) + throw new HttpResponseException(HttpStatusCode.NotFound); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) + throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); + + if (contentItem != null) + { + //store the content item in request cache so it can be resolved in the controller without re-looking it up + actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; + } } base.OnActionExecuting(actionContext); diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs index 21dc60e6cc..f8b02f08ca 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -3,12 +3,14 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Web; using System.Web.Http.Filters; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Composing; using Umbraco.Core.Security; +using Umbraco.Web.Trees; namespace Umbraco.Web.WebApi.Filters { @@ -72,7 +74,12 @@ namespace Umbraco.Web.WebApi.Filters protected virtual void FilterItems(IUser user, IList items) { - FilterBasedOnStartNode(items, user); + bool.TryParse(HttpContext.Current.Request.QueryString.Get(TreeQueryStringParameters.IgnoreUserStartNodes), out var ignoreUserStartNodes); + + if (ignoreUserStartNodes == false) + { + FilterBasedOnStartNode(items, user); + } } internal void FilterBasedOnStartNode(IList items, IUser user) From 791581f502e7a6a5cf36d710ee5ea28aa33e825f Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 17 Apr 2019 14:52:38 +0200 Subject: [PATCH 084/201] Use ConcurrentDictionary specific methods --- src/Umbraco.Core/Mapping/UmbracoMapper.cs | 12 ++++++------ src/Umbraco.Tests/Mapping/MappingTests.cs | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index a2ddebe431..2d495b38b5 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -107,16 +107,12 @@ namespace Umbraco.Core.Mapping private Dictionary> DefineCtors(Type sourceType) { - if (!_ctors.TryGetValue(sourceType, out var sourceCtor)) - sourceCtor = _ctors[sourceType] = new Dictionary>(); - return sourceCtor; + return _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); } private Dictionary> DefineMaps(Type sourceType) { - if (!_maps.TryGetValue(sourceType, out var sourceMap)) - sourceMap = _maps[sourceType] = new Dictionary>(); - return sourceMap; + return _maps.GetOrAdd(sourceType, _ => new Dictionary>()); } #endregion @@ -332,6 +328,8 @@ namespace Umbraco.Core.Mapping if (_ctors.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor)) return ctor; + // we *may* run this more than once but it does not matter + ctor = null; foreach (var (stype, sctors) in _ctors) { @@ -353,6 +351,8 @@ namespace Umbraco.Core.Mapping if (_maps.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map)) return map; + // we *may* run this more than once but it does not matter + map = null; foreach (var (stype, smap) in _maps) { diff --git a/src/Umbraco.Tests/Mapping/MappingTests.cs b/src/Umbraco.Tests/Mapping/MappingTests.cs index 47be99bb0e..79d383857a 100644 --- a/src/Umbraco.Tests/Mapping/MappingTests.cs +++ b/src/Umbraco.Tests/Mapping/MappingTests.cs @@ -136,6 +136,7 @@ namespace Umbraco.Tests.Mapping void ThreadLoop() { + // keep failing at mapping - and looping through the maps for (var i = 0; i < 10; i++) { try @@ -169,8 +170,6 @@ namespace Umbraco.Tests.Mapping { thread.Join(); } - - Assert.IsNull(caught); } private class Thing1 From bdac2a4a41338680104179bc4545e5592a4b4f24 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 18 Mar 2019 13:27:00 +0100 Subject: [PATCH 085/201] Use umb-checkbox for user filters --- .../src/views/users/views/users/users.html | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index e32297341d..965880d94f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -128,15 +128,10 @@ -
    - - -
    + +
    @@ -150,15 +145,10 @@ -
    - - -
    + +
    From 4107f475880e6175729211b5a80489a3489e88d6 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 20 Mar 2019 14:17:14 +0100 Subject: [PATCH 086/201] Add optional culture parameter to UrlAbsolute extension --- src/Umbraco.Web/PublishedContentExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 54afb7abbd..b10381bb94 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -41,8 +41,9 @@ namespace Umbraco.Web /// Gets the absolute url for the content. ///
    /// The content. + /// The culture to get the url for (defaults to current culture) /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content) + public static string UrlAbsolute(this IPublishedContent content, string culture = null) { // adapted from PublishedContentBase.Url switch (content.ItemType) @@ -52,7 +53,7 @@ namespace Umbraco.Web throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext is null."); if (Current.UmbracoContext.UrlProvider == null) throw new InvalidOperationException("Cannot resolve a Url for a content item when Current.UmbracoContext.UrlProvider is null."); - return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true); + return Current.UmbracoContext.UrlProvider.GetUrl(content.Id, true, culture); case PublishedItemType.Media: throw new NotSupportedException("AbsoluteUrl is not supported for media types."); default: From a48d97e6314ad70403880df5c28deb6354f9442c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 21 Mar 2019 12:27:11 +0100 Subject: [PATCH 087/201] Create explicit overloads for UrlAbsolute --- src/Umbraco.Web/PublishedContentExtensions.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index b10381bb94..dea4f17eef 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -41,9 +41,16 @@ namespace Umbraco.Web /// Gets the absolute url for the content. /// /// The content. - /// The culture to get the url for (defaults to current culture) /// The absolute url for the content. - public static string UrlAbsolute(this IPublishedContent content, string culture = null) + public static string UrlAbsolute(this IPublishedContent content) => content.UrlAbsolute(null); + + /// + /// Gets the absolute url for the content. + /// + /// The content. + /// The culture to get the url for + /// The absolute url for the content. + public static string UrlAbsolute(this IPublishedContent content, string culture) { // adapted from PublishedContentBase.Url switch (content.ItemType) From 2d32863fc957581575e519893e7b4b023cca1c87 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Mar 2019 14:06:16 +0100 Subject: [PATCH 088/201] Audit current user when moving media + clean up a few audit related things --- src/Umbraco.Web/Editors/MediaController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 97e24ee6b8..fe238219d7 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -88,7 +88,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, UmbracoUser.Id); + var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, Security.CurrentUser.Id); var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); //remove this tab if it exists: umbContainerView @@ -422,7 +422,7 @@ namespace Umbraco.Web.Editors //if the current item is in the recycle bin if (foundMedia.IsInRecycleBin() == false) { - var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); + var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, Security.CurrentUser.Id); if (moveResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -432,7 +432,7 @@ namespace Umbraco.Web.Editors } else { - var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); + var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, Security.CurrentUser.Id); if (deleteResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -456,7 +456,7 @@ namespace Umbraco.Web.Editors var destinationParentID = move.ParentId; var sourceParentID = toMove.ParentId; - var moveResult = Services.MediaService.WithResult().Move(toMove, move.ParentId); + var moveResult = Services.MediaService.WithResult().Move(toMove, move.ParentId, Security.CurrentUser.Id); if (sourceParentID == destinationParentID) { @@ -525,7 +525,7 @@ namespace Umbraco.Web.Editors } //save the item - var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); //return the updated model var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); From 2a930e5a6663c6b94d21fe3c81104fe6b016f863 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 24 Mar 2019 18:45:22 +0100 Subject: [PATCH 089/201] Log the ID of the current user when emptying the media recycle bin --- src/Umbraco.Core/Services/IMediaService.cs | 8 ++++++++ src/Umbraco.Core/Services/MediaService.cs | 12 ++++++++++-- src/Umbraco.Web/Editors/MediaController.cs | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index f5f136b789..4e3189bf72 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -289,8 +289,16 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] void EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + void EmptyRecycleBin(int userId = 0); + /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index b95a8a258c..873cff2bf5 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -981,7 +981,15 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - public void EmptyRecycleBin() + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public void EmptyRecycleBin() => EmptyRecycleBin(0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + public void EmptyRecycleBin(int userId = 0) { using (new WriteLock(Locker)) { @@ -1006,7 +1014,7 @@ namespace Umbraco.Core.Services recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success; uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); - Audit(uow, AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, Constants.System.RecycleBinMedia); + Audit(uow, AuditType.Delete, "Empty Media Recycle Bin performed by user", userId, Constants.System.RecycleBinMedia); uow.Commit(); } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index fe238219d7..1cc561ce9f 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -583,7 +583,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage EmptyRecycleBin() { - Services.MediaService.EmptyRecycleBin(); + Services.MediaService.EmptyRecycleBin(Security.CurrentUser.Id); return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); } From eb10d71bff78155af9f32faef453c765e42ce21b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 17 Apr 2019 16:47:23 +0200 Subject: [PATCH 090/201] Don't just Parse, the value might be null, have to use TryParse --- .../WebApi/Filters/EnsureUserPermissionForContentAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs index 67099dc58a..0c53838592 100644 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs @@ -118,7 +118,7 @@ namespace Umbraco.Web.WebApi.Filters } var queryStringCollection = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.Query); - var ignoreUserStartNodes = bool.Parse(queryStringCollection["ignoreUserStartNodes"]); + bool.TryParse(queryStringCollection["ignoreUserStartNodes"], out var ignoreUserStartNodes); if (ignoreUserStartNodes == false) { var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, From 73e4c6022dba6574a989355241976fd6c8da7807 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 25 Mar 2019 11:34:49 +0100 Subject: [PATCH 091/201] Update the title of the macro view picker --- .../src/views/macros/views/macro.settings.controller.js | 8 +++++++- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js index c250138adb..c8a08d57ca 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/views/macro.settings.controller.js @@ -15,9 +15,15 @@ function MacrosSettingsController($scope, editorService, localizationService) { $scope.model.openViewPicker = openViewPicker; $scope.model.removeMacroView = removeMacroView; + var labels = {}; + + localizationService.localizeMany(["macro_selectViewFile"]).then(function(data) { + labels.selectViewFile = data[0]; + }); + function openViewPicker() { const controlPicker = { - title: "Select view", + title: labels.selectViewFile, section: "settings", treeAlias: "partialViewMacros", entityType: "partialView", diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index e75ff6d83d..9c2b15d5ab 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1338,6 +1338,7 @@ Mange hilsner fra Umbraco robotten Indtast makronavn Parametre Definer de parametre, der skal være tilgængelige, når du bruger denne makro. + Vælg partial view makrofil Alternativt felt diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 372bc3158d..f1ac5dd6dc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1615,6 +1615,7 @@ To manage your website, simply open the Umbraco back office and start adding con Enter macro name Parameters Define the parameters that should be available when using this macro. + Select partial view macro file Building models 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 fb5a6bd359..7ba4d05786 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1625,6 +1625,7 @@ To manage your website, simply open the Umbraco back office and start adding con Enter macro name Parameters Define the parameters that should be available when using this macro. + Select partial view macro file Building models From 29a542a0ddded21521b09963d210f68538d223bd Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 25 Mar 2019 13:20:30 +0100 Subject: [PATCH 092/201] Support stylesheets in subfolders in the RTE --- .../src/common/services/tinymce.service.js | 9 +++++- .../rte/rte.prevalues.controller.js | 28 ++++++++++++------- .../propertyeditors/rte/rte.prevalues.html | 4 +-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 3c4f5a7d73..2fd75184ca 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -48,7 +48,14 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s if (configuredStylesheets) { angular.forEach(configuredStylesheets, function (val, key) { - stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css"); + if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/") === 0) { + // current format (full path to stylesheet) + stylesheets.push(val); + } + else { + // legacy format (stylesheet name only) - must prefix with stylesheet folder and postfix with ".css" + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css"); + } promises.push(stylesheetResource.getRulesByName(val).then(function (rules) { angular.forEach(rules, function (rule) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 59e0429678..1b489d6283 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -40,14 +40,17 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", $scope.stylesheets = stylesheets; }); - $scope.selected = function(cmd, alias, lookup){ - if (lookup && angular.isArray(lookup)) { - cmd.selected = lookup.indexOf(alias) >= 0; - return cmd.selected; - } - return false; + $scope.commandSelected = function(cmd) { + cmd.selected = $scope.model.value.toolbar.indexOf(cmd.alias) >= 0; + return cmd.selected; }; + $scope.cssSelected = function (css) { + // support both current format (full stylesheet path) and legacy format (stylesheet name only) + css.selected = $scope.model.value.stylesheets.indexOf(css.path) >= 0 ||$scope.model.value.stylesheets.indexOf(css.name) >= 0; + return css.selected; + } + $scope.selectCommand = function(command){ var index = $scope.model.value.toolbar.indexOf(command.alias); @@ -60,11 +63,16 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", $scope.selectStylesheet = function (css) { - var index = $scope.model.value.stylesheets.indexOf(css.name); + // find out if the stylesheet is already selected; first look for the full stylesheet path (current format) + var index = $scope.model.value.stylesheets.indexOf(css.path); + if (index === -1) { + // ... then look for the stylesheet name (legacy format) + index = $scope.model.value.stylesheets.indexOf(css.name); + } - if(css.selected && index === -1){ - $scope.model.value.stylesheets.push(css.name); - }else if(index >= 0){ + if(index === -1){ + $scope.model.value.stylesheets.push(css.path); + }else{ $scope.model.value.stylesheets.splice(index, 1); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 13515ca7a9..463b1a03e0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -4,7 +4,7 @@
    \ No newline at end of file + From f55e05da79ca827b192a03d6d1271501bced0b66 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Mar 2019 13:38:53 +0100 Subject: [PATCH 106/201] Don't explode when passing bad parameters to log resource --- .../src/common/resources/log.resource.js | 4 ++-- src/Umbraco.Web/Editors/LogController.cs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js index dad5345e6c..bcdaddd22f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js @@ -53,7 +53,7 @@ function logResource($q, $http, umbRequestHelper) { * * @param {Object} options options object * @param {Int} options.id the id of the entity - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10 * @param {Int} options.pageNumber if paging data, current page index, default = 1 * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending` * @param {Date} options.sinceDate if provided this will only get log entries going back to this date @@ -122,7 +122,7 @@ function logResource($q, $http, umbRequestHelper) { * * * @param {Object} options options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10, set to 0 to disable paging + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 10 * @param {Int} options.pageNumber if paging data, current page index, default = 1 * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Descending` * @param {Date} options.sinceDate if provided this will only get log entries going back to this date diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs index 4f750920d3..c3e3790378 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web/Editors/LogController.cs @@ -18,10 +18,15 @@ namespace Umbraco.Web.Editors [UmbracoApplicationAuthorize(Core.Constants.Applications.Content, Core.Constants.Applications.Media)] public PagedResult GetPagedEntityLog(int id, int pageNumber = 1, - int pageSize = 0, + int pageSize = 10, Direction orderDirection = Direction.Descending, DateTime? sinceDate = null) { + if (pageSize <= 0 || pageNumber <= 0) + { + return new PagedResult(0, pageNumber, pageSize); + } + long totalRecords; var dateQuery = sinceDate.HasValue ? SqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; var result = Services.AuditService.GetPagedItemsByEntity(id, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter: dateQuery); @@ -37,10 +42,15 @@ namespace Umbraco.Web.Editors public PagedResult GetPagedCurrentUserLog( int pageNumber = 1, - int pageSize = 0, + int pageSize = 10, Direction orderDirection = Direction.Descending, DateTime? sinceDate = null) { + if (pageSize <= 0 || pageNumber <= 0) + { + return new PagedResult(0, pageNumber, pageSize); + } + long totalRecords; var dateQuery = sinceDate.HasValue ? SqlContext.Query().Where(x => x.CreateDate >= sinceDate) : null; var userId = Security.GetUserId().ResultOr(0); From 59df7f1399e85c29aac0c462793e4640a4816cd9 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Mar 2019 15:45:32 +0100 Subject: [PATCH 107/201] Ensure consistency in the way panels are spaced --- .../src/less/components/html/umb-expansion-panel.less | 2 +- src/Umbraco.Web.UI.Client/src/less/components/umb-box.less | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less index 2d1cbd10ff..2a8137e5f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-expansion-panel.less @@ -1,7 +1,7 @@ .umb-expansion-panel { background: @white; border-radius: 3px; - margin-bottom: 16px; + margin-bottom: 20px; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.16); } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less index fb83504a1f..c0e91e28c2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less @@ -1,7 +1,7 @@ .umb-box { background: @white; border-radius: 3px; - margin-bottom: 8px; + margin-bottom: 20px; box-shadow: 0 1px 1px 0 rgba(0,0,0,.16); } @@ -28,4 +28,4 @@ .umb-box-content { padding: 20px; -} \ No newline at end of file +} From 8b6fbee47e5e5a3a18ea84b68c0421a61676197d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Apr 2019 10:00:51 +0200 Subject: [PATCH 108/201] V8: Add back button to listviews in media (#5116) --- src/Umbraco.Web.UI.Client/src/views/media/edit.html | 4 +++- .../src/views/media/media.edit.controller.js | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html index 987cd91ffc..cb620675a5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -14,7 +14,9 @@ hide-description="true" hide-alias="true" navigation="content.apps" - on-select-navigation-item="appChanged(item)"> + on-select-navigation-item="appChanged(item)" + show-back-button="showBack()" + on-back="onBack()"> 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 8dda2d6669..d4d538b82c 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 @@ -9,7 +9,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, entityResource, navigationService, notificationsService, localizationService, serverValidationManager, contentEditingHelper, fileManager, formHelper, - editorState, umbRequestHelper, $http, eventsService) { + editorState, umbRequestHelper, $http, eventsService, $location) { var evts = []; var nodeId = null; @@ -279,6 +279,17 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, } } + $scope.showBack = function () { + return !infiniteMode && !!$scope.page.listViewPath; + } + + /** Callback for when user clicks the back-icon */ + $scope.onBack = function() { + if ($scope.page.listViewPath) { + $location.path($scope.page.listViewPath); + } + }; + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { From 32104fff412e3972391c7d941d230cde0eb2d25f Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Apr 2019 10:21:38 +0200 Subject: [PATCH 109/201] V8: Add back button to listviews for members (#5158) * Add back button to edit member * Don't set illegal location (with querystring params) --- .../src/views/member/edit.html | 6 ++++-- .../src/views/member/member.edit.controller.js | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index ee6e9c625c..d4078e56c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -13,8 +13,10 @@ menu="page.menu" hide-icon="true" hide-description="true" - hide-alias="true"> - + hide-alias="true" + show-back-button="showBack()" + on-back="onBack()"> + diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index bf32f83c4e..6d5bc9036c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -15,14 +15,9 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR $scope.page.menu.currentSection = appState.getSectionState("currentSection"); $scope.page.menu.currentNode = null; //the editors affiliated node $scope.page.nameLocked = false; - $scope.page.listViewPath = null; $scope.page.saveButtonState = "init"; $scope.page.exportButton = "init"; - $scope.page.listViewPath = ($routeParams.page && $routeParams.listName) - ? "/member/member/list/" + $routeParams.listName + "?page=" + $routeParams.page - : null; - //build a path to sync the tree with function buildTreePath(data) { return $routeParams.listName ? "-1," + $routeParams.listName : "-1"; @@ -192,6 +187,19 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR }; + $scope.showBack = function () { + return !!$routeParams.listName; + } + + /** Callback for when user clicks the back-icon */ + $scope.onBack = function () { + $location.path("/member/member/list/" + $routeParams.listName); + $location.search("listName", null); + if ($routeParams.page) { + $location.search("page", $routeParams.page); + } + }; + $scope.export = function() { var memberKey = $scope.content.key; memberResource.exportMemberData(memberKey); From e180bb672212332556ac8ac17573a42c6311ba27 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 22 Apr 2019 10:44:29 +0200 Subject: [PATCH 110/201] V8: Fix broken listview pagination when using the back link (#5149) --- .../components/umbpagination.directive.js | 4 +++ .../common/services/listviewhelper.service.js | 31 +++++++++++++++++-- .../grid/grid.listviewlayout.controller.js | 2 +- .../list/list.listviewlayout.controller.js | 5 ++- 4 files changed, 36 insertions(+), 6 deletions(-) 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 e7abc81841..4c1a8747d1 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 @@ -91,6 +91,10 @@ Use this directive to generate a pagination. function link(scope, el, attr, ctrl) { function activate() { + // page number is sometimes a string - let's make sure it's an int before we do anything with it + if (scope.pageNumber) { + scope.pageNumber = parseInt(scope.pageNumber); + } scope.pagination = []; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 8aaddbf98f..e317c509df 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -45,7 +45,7 @@ (function () { 'use strict'; - function listViewHelper(localStorageService) { + function listViewHelper($location, localStorageService, urlHelper) { var firstSelectedIndex = 0; var localStorageKey = "umblistViewLayout"; @@ -559,6 +559,32 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#editItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for opening an item in a list view for editing. + * + * @param {Object} item The item to edit + */ + function editItem(item) { + if (!item.editPath) { + return; + } + var parts = item.editPath.split("?"); + var path = parts[0]; + var params = parts[1] + ? urlHelper.getQueryStringParams("?" + parts[1]) + : {}; + + $location.path(path); + for (var p in params) { + $location.search(p, params[p]) + } + } + function isMatchingLayout(id, layout) { // legacy format uses "nodeId", be sure to look for both return layout.id === id || layout.nodeId === id; @@ -579,7 +605,8 @@ isSelectedAll: isSelectedAll, setSortingDirection: setSortingDirection, setSorting: setSorting, - getButtonPermissions: getButtonPermissions + getButtonPermissions: getButtonPermissions, + editItem: editItem }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js index 292ca3f975..9da70e38dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js @@ -117,7 +117,7 @@ } function goToItem(item, $event, $index) { - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); + listViewHelper.editItem(item); } activate(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index 294dd50147..4230633e96 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { + function ListViewListLayoutController($scope, listViewHelper, mediaHelper, mediaTypeHelper, urlHelper) { var vm = this; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; @@ -53,8 +53,7 @@ } function clickItem(item) { - // if item.id is 2147483647 (int.MaxValue) use item.key - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); + listViewHelper.editItem(item); } function isSortDirection(col, direction) { From 687cc4630fad30bce84ec622b1d716434fa47cbc Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 22 Apr 2019 10:47:00 +0200 Subject: [PATCH 111/201] Fix missing semicolon --- .../src/common/services/listviewhelper.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index e317c509df..20d014ab0f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -581,7 +581,7 @@ $location.path(path); for (var p in params) { - $location.search(p, params[p]) + $location.search(p, params[p]); } } From 344b7731ce9c5213ddbafa6f792625f8a6d34344 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 22 Apr 2019 12:10:53 +0200 Subject: [PATCH 112/201] Fix JS unit tests --- .../src/common/services/tree.service.js | 12 ++++++++++-- .../test/unit/common/services/tree-service.spec.js | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 73893739a6..d61d1c3ba1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -29,6 +29,15 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS return cacheKey; } + // Adapted from: https://stackoverflow.com/a/2140723 + // Please note, we can NOT test this functionality correctly in Phantom because it implements + // the localeCompare method incorrectly: https://github.com/ariya/phantomjs/issues/11063 + function invariantEquals(a, b) { + return typeof a === "string" && typeof b === "string" + ? a.localeCompare(b, undefined, { sensitivity: "base" }) === 0 + : a === b; + } + return { /** Internal method to return the tree cache */ @@ -166,8 +175,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { - // localeCompare returns 0 when strings are equal, so return false if there's no match - return item.alias.localeCompare(treeAlias, undefined, { ignorePunctuation: true }) !== 0; + return invariantEquals(item.alias, treeAlias); }); return found ? found.packageFolder : undefined; diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js index c2ad072078..4d19cf557a 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js @@ -303,7 +303,7 @@ describe('tree service tests', function () { }); it('returns undefined for a not found tree', function () { - //we know this exists in the mock umbraco server vars + //we know this does not exist in the mock umbraco server vars var found = treeService.getTreePackageFolder("asdfasdf"); expect(found).not.toBeDefined(); }); @@ -315,8 +315,8 @@ describe('tree service tests', function () { it('hasChildren has to be updated on parent', function () { var tree = getContentTree(); - while (tree.children.length > 0){ - treeService.removeNode(tree.children[0]) + while (tree.children.length > 0) { + treeService.removeNode(tree.children[0]); } expect(tree.hasChildren).toBe(false); From 655befde420faf427f72f1b39cc585cc15a8641d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 8 Apr 2019 21:18:09 +0200 Subject: [PATCH 113/201] Read image dimensions via GDI if EXIF fails with an exception --- src/Umbraco.Web/Media/ImageHelper.cs | 31 +++++++++++----------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web/Media/ImageHelper.cs b/src/Umbraco.Web/Media/ImageHelper.cs index 5a5724dc7d..899f817d5a 100644 --- a/src/Umbraco.Web/Media/ImageHelper.cs +++ b/src/Umbraco.Web/Media/ImageHelper.cs @@ -1,17 +1,7 @@ using System; -using System.Collections.Generic; using System.Drawing; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; -using Umbraco.Core.Media; -using Umbraco.Core.Models; -using Umbraco.Web.Composing; using Umbraco.Web.Media.Exif; namespace Umbraco.Web.Media @@ -28,9 +18,9 @@ namespace Umbraco.Web.Media /// use potentially large amounts of memory. public static Size GetDimensions(Stream stream) { + //Try to load with exif try { - //Try to load with exif var jpgInfo = ImageFile.FromStream(stream); if (jpgInfo != null @@ -45,11 +35,17 @@ namespace Umbraco.Web.Media return new Size(width, height); } } + } + catch + { + //We will just swallow, just means we can't read exif data, we don't want to log an error either + } - //we have no choice but to try to read in via GDI + //we have no choice but to try to read in via GDI + try + { using (var image = Image.FromStream(stream)) { - var fileWidth = image.Width; var fileHeight = image.Height; return new Size(fileWidth, fileHeight); @@ -57,13 +53,10 @@ namespace Umbraco.Web.Media } catch (Exception) { - //We will just swallow, just means we can't read exif data, we don't want to log an error either - return new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize); + //We will just swallow, just means we can't read via GDI, we don't want to log an error either } - + + return new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize); } - - - } } From 072c0140554ac826468996750629e210d1c5ec1c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 8 Apr 2019 20:59:12 +0200 Subject: [PATCH 114/201] Read image dimensions via GDI if EXIF fails with an exception --- src/Umbraco.Core/IO/MediaFileSystem.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index a4305506bb..d843b95709 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -377,21 +377,28 @@ namespace Umbraco.Core.IO return new Size(width, height); } } + } + catch + { + //We will just swallow, just means we can't read exif data, we don't want to log an error either + } - //we have no choice but to try to read in via GDI + //we have no choice but to try to read in via GDI + try + { using (var image = Image.FromStream(stream)) { - var fileWidth = image.Width; var fileHeight = image.Height; return new Size(fileWidth, fileHeight); } } - catch (Exception) + catch { - //We will just swallow, just means we can't read exif data, we don't want to log an error either - return new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize); + //We will just swallow, just means we can't read via GDI, we don't want to log an error either } + + return new Size(Constants.Conventions.Media.DefaultSize, Constants.Conventions.Media.DefaultSize); } #endregion From c7b4d1b206c5773a500fa45417e967f989259d6c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 9 Apr 2019 07:41:17 +0200 Subject: [PATCH 115/201] Always show file names for media/files without thumbnail --- .../src/views/components/umb-media-grid.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 1f5235146d..a91bc8c876 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -4,7 +4,7 @@ -
    +
    {{item.name}}
    From a969ff6e10b9e32810a74af893f41352b0c7289a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 9 Apr 2019 22:21:13 +0200 Subject: [PATCH 116/201] Make sure Nested Content scaffolds are listed in defined sort order --- .../views/common/overlays/itempicker/itempicker.controller.js | 4 ++++ .../src/views/common/overlays/itempicker/itempicker.html | 2 +- .../propertyeditors/nestedcontent/nestedcontent.controller.js | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js index 9e758a5b21..74a677b7c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js @@ -8,6 +8,10 @@ function ItemPickerOverlay($scope, localizationService) { $scope.model.title = value; }); } + + if (!$scope.model.orderBy) { + $scope.model.orderBy = "name"; + } } $scope.selectItem = function(item) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index aac4830d52..328344ea84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -12,7 +12,7 @@
    + + From 1370894b38a21b4ddc0b03b5e4299475e7c85ec6 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 9 Mar 2019 22:16:26 +0100 Subject: [PATCH 189/201] Remove accordion group behavior from member editing --- src/Umbraco.Web.UI.Client/src/views/member/edit.html | 7 +++---- .../src/views/member/member.edit.controller.js | 10 ---------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index d4078e56c0..dbaf96abc2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -20,14 +20,13 @@ -
    +
    -
    +
    {{ group.label }}
    -  
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index 6d5bc9036c..a76862a1d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -39,11 +39,6 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR editorState.set($scope.content); - // set all groups to open - angular.forEach($scope.content.tabs, function(group){ - group.open = true; - }); - $scope.page.loading = false; }); @@ -58,11 +53,6 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR editorState.set($scope.content); - // set all groups to open - angular.forEach($scope.content.tabs, function(group){ - group.open = true; - }); - $scope.page.loading = false; }); From 4ebfd8ca376209f6b028652cfcab47b5a5d05859 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 9 Mar 2019 22:10:33 +0100 Subject: [PATCH 190/201] Remove accordion group behavior from media editing --- .../src/views/media/apps/content/content.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/media/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/media/apps/content/content.html index f8bbf5ed54..633eccdf62 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/apps/content/content.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/apps/content/content.html @@ -1,12 +1,11 @@
    -
    +
    -
    +
    {{ group.label }}
    -  
    -
    +
    From 6274a08a4f69cc6d976120537916f4952647ee8f Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 6 Mar 2019 14:42:33 +0100 Subject: [PATCH 191/201] Don't allow opening an uncreated template --- .../src/views/components/umb-grid-selector.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html index 2e5939dae9..7eb80bacc3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html @@ -6,7 +6,7 @@
    {{ defaultItem.name }}
    - + (Default {{itemLabel}})
    From 6bfcc7bb34ce1db607b2d7e39e1ae85fd059a6c0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 6 Mar 2019 14:30:53 +0100 Subject: [PATCH 192/201] Don't "Create matching template" before the content type is created --- .../documenttypes/views/templates/templates.controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js index a147da3afb..e2a964c293 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js @@ -70,6 +70,10 @@ }; function checkIfTemplateExists() { + if ($scope.model.id === 0) { + return; + } + var existingTemplate = vm.availableTemplates.find(function (availableTemplate) { return (availableTemplate.name === $scope.model.name || availableTemplate.placeholder); }); From d04b967fe6ef8eda6429ea4026837d7d79dbfb07 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 30 Apr 2019 21:34:00 +0200 Subject: [PATCH 193/201] V8: Hide "umbContent" app if the content has no properties (#4840) --- .../directives/components/content/edit.controller.js | 4 ++-- .../content/umbvariantcontenteditors.directive.js | 8 +++++--- .../views/components/content/umb-variant-content.html | 11 +++++++++-- .../src/views/contentblueprints/edit.controller.js | 6 +++++- .../ContentApps/ContentEditorContentAppFactory.cs | 5 ++++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 67bb5a64a5..f7704ab870 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -55,9 +55,9 @@ } } - + // if we still dont have a app, lets show the first one: - if (isAppPresent === false) { + if (isAppPresent === false && content.apps.length) { content.apps[0].active = true; $scope.appChanged(content.apps[0]); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index 3f67b74380..a4dac046e5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -198,9 +198,11 @@ return a.alias === "umbContent"; }); - //The view model for the content app is simply the index of the variant being edited - var variantIndex = vm.content.variants.indexOf(variant); - contentApp.viewModel = variantIndex; + if (contentApp) { + //The view model for the content app is simply the index of the variant being edited + var variantIndex = vm.content.variants.indexOf(variant); + contentApp.viewModel = variantIndex; + } // make sure the same app it set to active in the new variant if(activeAppAlias) { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html index 94fb7edae8..9cd245b0d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html @@ -5,7 +5,7 @@
    - + - +
    + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js index 3140af9d6c..59351ffd38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js @@ -25,7 +25,11 @@ function ContentBlueprintEditController($scope, $routeParams, contentResource) { var contentApp = _.find(content.apps, function (app) { return app.alias === "umbContent"; }); - content.apps = [contentApp]; + if (contentApp) { + content.apps = [contentApp]; + } else { + content.apps = []; + } } content.allowPreview = false; content.allowedActions = ["A", "S", "C"]; diff --git a/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs index da7dbee364..ea2dc1b2d6 100644 --- a/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ContentEditorContentAppFactory.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.ContentApps { switch (o) { - case IContent _: + case IContent content when content.Properties.Count > 0: return _contentApp ?? (_contentApp = new ContentApp { Alias = "umbContent", @@ -28,6 +28,9 @@ namespace Umbraco.Web.ContentApps Weight = Weight }); + case IContent _: + return null; + case IMedia media when !media.ContentType.IsContainer || media.Properties.Count > 0: return _mediaApp ?? (_mediaApp = new ContentApp { From 958c8b1214162b1708d0e177b58f5f3c06424f75 Mon Sep 17 00:00:00 2001 From: Samuel Butler Date: Wed, 1 May 2019 05:54:35 +1000 Subject: [PATCH 194/201] Umbraco installer double-encodes connection string password fix (#4820) --- src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs index 9d1aec1c71..d119759488 100644 --- a/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/DatabaseConfigureStep.cs @@ -53,7 +53,7 @@ namespace Umbraco.Web.Install.InstallSteps } else { - var password = database.Password.Replace("&", "&").Replace(">", ">").Replace("<", "<").Replace("\"", """).Replace("'", "''"); + var password = database.Password.Replace("'", "''"); password = string.Format("'{0}'", password); _databaseBuilder.ConfigureDatabaseConnection( From 4d8e5e14160d9bc6c4a95fc7d2a7bf184180566a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 1 Mar 2019 15:06:03 +0100 Subject: [PATCH 195/201] Make sure to hide the member types when creating a new member --- src/Umbraco.Web.UI.Client/src/views/member/create.html | 2 +- .../src/views/member/member.create.controller.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/member/create.html b/src/Umbraco.Web.UI.Client/src/views/member/create.html index a0140d0834..2292cf18ee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/create.html @@ -6,7 +6,7 @@