From 83b4b6a5f4d84f5e57cdbb55f8d4970df1af28a4 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 8 Jun 2019 22:50:56 +0200 Subject: [PATCH 001/610] Annotate each crop with "Automatic" or "User defined" --- .../src/less/property-editors.less | 16 ++++++++++++++-- .../imagecropper/imagecropper.controller.js | 6 +++++- .../imagecropper/imagecropper.html | 2 ++ 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 | 8 +++++--- 6 files changed, 30 insertions(+), 6 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 9e8dd37ab9..3ff8432f07 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -710,7 +710,8 @@ } .imagecropper .umb-sortable-thumbnails li .crop-name, - .imagecropper .umb-sortable-thumbnails li .crop-size { + .imagecropper .umb-sortable-thumbnails li .crop-size, + .imagecropper .umb-sortable-thumbnails li .crop-annotation { display: block; text-align: left; font-size: 13px; @@ -722,17 +723,28 @@ margin: 10px 0 5px; } - .imagecropper .umb-sortable-thumbnails li .crop-size { + .imagecropper .umb-sortable-thumbnails li .crop-size, + .imagecropper .umb-sortable-thumbnails li .crop-annotation { font-size: 10px; font-style: italic; margin: 0 0 5px; } + .imagecropper .umb-sortable-thumbnails li .crop-annotation { + color: @gray-6; + } + .btn-crop-delete { display: block; text-align: left; } + .imagecropper .cropList-container { + h5 { + margin-left: 10px; + margin-top: 0; + } + } // // folder-browser // -------------------------------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index b5ae731c94..3a8e6db8d8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -13,7 +13,7 @@ angular.module('umbraco') $scope.clear = clear; $scope.reset = reset; $scope.close = close; - $scope.focalPointChanged = focalPointChanged; + $scope.isCustomCrop = isCustomCrop; //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; @@ -201,6 +201,10 @@ angular.module('umbraco') $scope.imageCropperForm.$setDirty(); }; + function isCustomCrop(crop) { + return !!crop.coordinates; + } + var unsubscribe = $scope.$on("formSubmitting", function () { $scope.currentCrop = null; $scope.currentPoint = null; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 7438fa04da..5604ac553d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -58,6 +58,8 @@
{{value.alias}} {{value.width}}px x {{value.height}}px + User defined + Automatic
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 9e6bdc5e57..b70f0f07af 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1019,6 +1019,8 @@ Mange hilsner fra Umbraco robotten Nulstil Acceptér Fortryd + Automatisk + Brugerdefineret Vælg en version at sammenligne med den nuværende version diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 189bd9f10b..4bf77a48d6 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1273,6 +1273,8 @@ To manage your website, simply open the Umbraco back office and start adding con Add new crop Done Undo edits + Automatic + User defined Select a version to compare with the current version diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 6ce6f82ccc..010fe7fb4a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1274,11 +1274,13 @@ To manage your website, simply open the Umbraco back office and start adding con Enter the link - Reset crop + Reset crop Save crop Add new crop - Done - Undo edits + Done + Undo edits + Automatic + User defined Select a version to compare with the current version From 6425592cc587b072129fe7bfbf520bb18c635a83 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 9 Jun 2019 12:11:45 +0200 Subject: [PATCH 002/610] Only show "custom crop" text ("automatic crop" is implicit) --- .../src/views/propertyeditors/imagecropper/imagecropper.html | 3 +-- 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, 1 insertion(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 5604ac553d..e2428cec05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -58,8 +58,7 @@
{{value.alias}} {{value.width}}px x {{value.height}}px - User defined - Automatic + User defined 
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index b70f0f07af..15ee17ca3a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1019,7 +1019,6 @@ Mange hilsner fra Umbraco robotten Nulstil Acceptér Fortryd - Automatisk Brugerdefineret diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 4bf77a48d6..1035848a1c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1273,7 +1273,6 @@ To manage your website, simply open the Umbraco back office and start adding con Add new crop Done Undo edits - Automatic User defined 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 010fe7fb4a..35d89d283f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1279,7 +1279,6 @@ To manage your website, simply open the Umbraco back office and start adding con Add new crop Done Undo edits - Automatic User defined From 201c91d3a8e90409a0b949249b8cc9b853835d50 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 9 Jun 2019 12:32:27 +0200 Subject: [PATCH 003/610] Whoops, accidental removal of focalPointChanged --- .../propertyeditors/imagecropper/imagecropper.controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index 3a8e6db8d8..e3576426a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -14,6 +14,7 @@ angular.module('umbraco') $scope.reset = reset; $scope.close = close; $scope.isCustomCrop = isCustomCrop; + $scope.focalPointChanged = focalPointChanged; //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; From 3031459b8a43e33d501433e2e0e916ac67b31793 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Jul 2019 19:44:11 +0200 Subject: [PATCH 004/610] Don't allow logins for users with no content and/or media start nodes --- src/Umbraco.Web/Security/BackOfficeSignInManager.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index b33487bc8d..66b90a1396 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -112,6 +112,19 @@ namespace Umbraco.Web.Security return SignInStatus.LockedOut; } + // We need to verify that the user belongs to one or more groups that define content and media start nodes. + // To do so we have to create the user claims identity and validate the calculated start nodes. + var userIdentity = await CreateUserIdentityAsync(user); + if(userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity) + { + if(backOfficeIdentity.StartContentNodes.Length == 0 || backOfficeIdentity.StartMediaNodes.Length == 0) + { + _logger.WriteCore(TraceEventType.Information, 0, + $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null); + return SignInStatus.Failure; + } + } + await UserManager.ResetAccessFailedCountAsync(user.Id); return await SignInOrTwoFactor(user, isPersistent); } From f4d8f58505bdb3d0c942ddfb165ffe095977aaf4 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 25 Jul 2019 07:40:38 +0200 Subject: [PATCH 005/610] Update src/Umbraco.Web/Security/BackOfficeSignInManager.cs Co-Authored-By: Ronald Barendse --- src/Umbraco.Web/Security/BackOfficeSignInManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index 66b90a1396..fb8aff42e8 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -115,7 +115,7 @@ namespace Umbraco.Web.Security // We need to verify that the user belongs to one or more groups that define content and media start nodes. // To do so we have to create the user claims identity and validate the calculated start nodes. var userIdentity = await CreateUserIdentityAsync(user); - if(userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity) + if (userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity) { if(backOfficeIdentity.StartContentNodes.Length == 0 || backOfficeIdentity.StartMediaNodes.Length == 0) { From f27b7549671e666928f422024f81bb82837b0c2d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 25 Jul 2019 07:40:44 +0200 Subject: [PATCH 006/610] Update src/Umbraco.Web/Security/BackOfficeSignInManager.cs Co-Authored-By: Ronald Barendse --- src/Umbraco.Web/Security/BackOfficeSignInManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index fb8aff42e8..8e5e532731 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -117,7 +117,7 @@ namespace Umbraco.Web.Security var userIdentity = await CreateUserIdentityAsync(user); if (userIdentity is UmbracoBackOfficeIdentity backOfficeIdentity) { - if(backOfficeIdentity.StartContentNodes.Length == 0 || backOfficeIdentity.StartMediaNodes.Length == 0) + if (backOfficeIdentity.StartContentNodes.Length == 0 || backOfficeIdentity.StartMediaNodes.Length == 0) { _logger.WriteCore(TraceEventType.Information, 0, $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null); From 8a60c95a36242ee6cbae36232599bed38e902ec9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 12 Sep 2019 17:04:29 +1000 Subject: [PATCH 007/610] Fixes default weighting for Umbraco.Web handlers and adds unit test --- .../ApplicationEventsResolver.cs | 14 +++++--- .../ApplicationEventsResolverTests.cs | 32 +++++++++++++++++++ .../Resolvers/LazyManyObjectResolverTests.cs | 6 ++-- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Tests/Resolvers/ApplicationEventsResolverTests.cs diff --git a/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs b/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs index eda1b94e37..20c49de588 100644 --- a/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs +++ b/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs @@ -89,16 +89,20 @@ namespace Umbraco.Core.ObjectResolution } } - protected override int GetObjectWeight(object o) - { + protected override int GetObjectWeight(object o) => GetObjectWeightInternal(o, DefaultPluginWeight); + + internal static int GetObjectWeightInternal(object o, int defaultPluginWeight) + { var type = o.GetType(); var attr = type.GetCustomAttribute(true); if (attr != null) return attr.Weight; var name = type.Assembly.FullName; // we should really attribute all our Core handlers, so this is temp - var core = name.InvariantStartsWith("Umbraco.") || name.InvariantStartsWith("Concorde."); - return core ? -DefaultPluginWeight : DefaultPluginWeight; + var core = name.InvariantStartsWith("umbraco,") // This handles the umbraco.dll (Umbraco.Web) project + || name.InvariantStartsWith("Umbraco.") // This handles all other Umbraco.* assemblies - in the case of v7, this is ONLY Umbraco.Core + || name.InvariantStartsWith("Concorde."); // Special case for Cloud assemblies + return core ? -defaultPluginWeight : defaultPluginWeight; } /// @@ -202,4 +206,4 @@ namespace Umbraco.Core.ObjectResolution _orderedAndFiltered = null; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Resolvers/ApplicationEventsResolverTests.cs b/src/Umbraco.Tests/Resolvers/ApplicationEventsResolverTests.cs new file mode 100644 index 0000000000..3fba16fde3 --- /dev/null +++ b/src/Umbraco.Tests/Resolvers/ApplicationEventsResolverTests.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using umbraco.BusinessLogic; +using Umbraco.Core; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.ObjectResolution; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Tests.Resolvers +{ + [TestFixture] + public class ApplicationEventsResolverTests + { + [Test] + public void Core_Event_Handler_Weight_Test() + { + //from the 'umbraco' (Umbraco.Web) assembly + Assert.AreEqual(-100, ApplicationEventsResolver.GetObjectWeightInternal(new GridPropertyEditor(), 100)); + //from the 'Umbraco.Core' assembly + Assert.AreEqual(-100, ApplicationEventsResolver.GetObjectWeightInternal(new IdentityModelMappings(), 100)); + //from the 'Umbraco.Test' assembly + Assert.AreEqual(-100, ApplicationEventsResolver.GetObjectWeightInternal(new MyTestEventHandler(), 100)); + + //from the 'umbraco.BusinessLogic' assembly - which we are not checking for and not setting as the negative of the default + Assert.AreEqual(100, ApplicationEventsResolver.GetObjectWeightInternal(new ApplicationRegistrar(), 100)); + } + + private class MyTestEventHandler : ApplicationEventHandler + { + + } + } +} diff --git a/src/Umbraco.Tests/Resolvers/LazyManyObjectResolverTests.cs b/src/Umbraco.Tests/Resolvers/LazyManyObjectResolverTests.cs index 33bb1ab34c..841c302e52 100644 --- a/src/Umbraco.Tests/Resolvers/LazyManyObjectResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/LazyManyObjectResolverTests.cs @@ -10,7 +10,9 @@ using Umbraco.Core.ObjectResolution; namespace Umbraco.Tests.Resolvers { - [TestFixture] + + + [TestFixture] public class LazyManyObjectResolverTests { @@ -191,4 +193,4 @@ namespace Umbraco.Tests.Resolvers #endregion } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index d45c556d37..4a67223b3a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -197,6 +197,7 @@ + From 905b2414263975c0eeb79066281a0479b5988fd6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 26 Sep 2019 13:39:53 +0200 Subject: [PATCH 008/610] #2996 added GlobalSettings.DebugMode switch for JSON formatting indentation. (cherry picked from commits d3c4aace160b78739a8eeb673cb561e96cc04101 / 16837d018a44c324e620d7c72b63be015a87895c) --- src/Umbraco.Core/Serialization/JsonNetSerializer.cs | 6 ++++-- src/Umbraco.Web/Editors/BackOfficeController.cs | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs index 800278abf0..52f39c6109 100644 --- a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Serialization { @@ -60,7 +61,8 @@ namespace Umbraco.Core.Serialization /// public IStreamedResult ToStream(object input) { - string s = JsonConvert.SerializeObject(input, Formatting.Indented, _settings); + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + string s = JsonConvert.SerializeObject(input, formatting, _settings); byte[] bytes = Encoding.UTF8.GetBytes(s); MemoryStream ms = new MemoryStream(bytes); @@ -69,4 +71,4 @@ namespace Umbraco.Core.Serialization #endregion } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 093c095b5d..4a79038fc0 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -177,8 +177,8 @@ namespace Umbraco.Web.Editors //the dictionary returned is fine but the delimiter between an 'area' and a 'value' is a '/' but the javascript // in the back office requres the delimiter to be a '_' so we'll just replace it .ToDictionary(key => key.Key.Replace("/", "_"), val => val.Value); - - return new JsonNetResult { Data = textForCulture, Formatting = Formatting.Indented }; + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + return new JsonNetResult { Data = textForCulture, Formatting = formatting }; } /// @@ -230,8 +230,8 @@ namespace Umbraco.Web.Editors typeof(BackOfficeController) + "GetManifestAssetList", () => getResult(), new TimeSpan(0, 10, 0)); - - return new JsonNetResult { Data = result, Formatting = Formatting.Indented }; + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + return new JsonNetResult { Data = result, Formatting = formatting }; } [UmbracoAuthorize(Order = 0)] @@ -244,8 +244,8 @@ namespace Umbraco.Web.Editors new DirectoryInfo(Server.MapPath(SystemDirectories.AppPlugins)), new DirectoryInfo(Server.MapPath(SystemDirectories.Config)), HttpContext.IsDebuggingEnabled); - - return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.Indented }; + var formatting = GlobalSettings.DebugMode ? Formatting.Indented : Formatting.None; + return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = formatting }; } From a37b1075a100b3b50b32382bb6af68477f6179a5 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth <322383+benjaminhowarth1@users.noreply.github.com> Date: Mon, 30 Sep 2019 16:54:28 +0100 Subject: [PATCH 009/610] #2996 resubmitting ContentExtensions and ObjectExtensions fixes (#6473) (cherry picked from commit 79bf9b753caf97a55d474c5c6db8821a33ca398f) --- src/Umbraco.Core/ObjectExtensions.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 479e425c99..5c1c5fdeac 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -8,7 +8,10 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Xml; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Collections; +using Formatting = Newtonsoft.Json.Formatting; namespace Umbraco.Core { @@ -125,6 +128,11 @@ namespace Umbraco.Core return Attempt.Succeed(input); } + if (target == typeof(string) && inputType == typeof(JObject)) + { + return Attempt.Succeed(JsonConvert.SerializeObject(input, Formatting.None)); + } + // Check for string so that overloaders of ToString() can take advantage of the conversion. if (target == typeof(string)) { From 10bf55dbaadae4fd8515caa016169d8990cab807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Thu, 29 Aug 2019 13:23:11 +0200 Subject: [PATCH 010/610] Backoffice support for viewing segments --- .../umbvariantcontenteditors.directive.js | 80 +++++++++++++------ .../src/views/components/content/edit.html | 2 +- .../content/umb-variant-content-editors.html | 2 +- 3 files changed, 56 insertions(+), 28 deletions(-) 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 a4dac046e5..2118b88880 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 @@ -9,7 +9,7 @@ bindings: { page: "<", content: "<", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant - culture: "<", + variantId: "<", onSelectApp: "&?", onSelectAppAnchor: "&?", onBack: "&?", @@ -40,12 +40,14 @@ //Used to track how many content views there are (for split view there will be 2, it could support more in theory) vm.editors = []; //Used to track the open variants across the split views + // The values are the variant ids of the currently open variants. + // See getVariantId() for the current format. vm.openVariants = []; /** Called when the component initializes */ function onInit() { prevContentDateUpdated = angular.copy(vm.content.updateDate); - setActiveCulture(); + setActiveVariant(); } /** Called when the component has linked all elements, this is when the form controller is available */ @@ -59,15 +61,15 @@ */ function onChanges(changes) { - if (changes.culture && !changes.culture.isFirstChange() && changes.culture.currentValue !== changes.culture.previousValue) { - setActiveCulture(); + if (changes.variantId && !changes.variantId.isFirstChange() && changes.variantId.currentValue !== changes.variantId.previousValue) { + setActiveVariant(); } } /** Allows us to deep watch whatever we want - executes on every digest cycle */ function doCheck() { if (!angular.equals(vm.content.updateDate, prevContentDateUpdated)) { - setActiveCulture(); + setActiveVariant(); prevContentDateUpdated = angular.copy(vm.content.updateDate); } } @@ -79,13 +81,13 @@ } /** - * Set the active variant based on the current culture (query string) + * Set the active variant based on the current culture + segment (query string) */ - function setActiveCulture() { + function setActiveVariant() { // set the active variant - var activeVariant = null; + var activeVariant = null; _.each(vm.content.variants, function (v) { - if (v.language && v.language.culture === vm.culture) { + if (getVariantId(v) === vm.variantId) { v.active = true; activeVariant = v; } @@ -105,9 +107,10 @@ if (vm.editors.length > 1) { //now re-sync any other editor content (i.e. if split view is open) for (var s = 1; s < vm.editors.length; s++) { + var editorVariantId = getVariantId(vm.editors[s].content); //get the variant from the scope model var variant = _.find(vm.content.variants, function (v) { - return v.language.culture === vm.editors[s].content.language.culture; + return getVariantId(v) === editorVariantId; }); vm.editors[s].content = initVariant(variant, s); } @@ -122,19 +125,19 @@ */ function insertVariantEditor(index, variant) { - var variantCulture = variant.language ? variant.language.culture : "invariant"; + var variantId = getVariantId(variant); - //check if the culture at the index is the same, if it's null an editor will be added - var currentCulture = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].culture; + //check if the variant at the index is the same, if it's null an editor will be added + var currentVariantId = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].variantId; - if (currentCulture !== variantCulture) { + if (currentVariantId !== variantId) { //Not the current culture which means we need to modify the array. //NOTE: It is not good enough to just replace the `content` object at a given index in the array // since that would mean that directives are not re-initialized. vm.editors.splice(index, 1, { content: variant, //used for "track-by" ng-repeat - culture: variantCulture + variantId: variantId }); } else { @@ -143,6 +146,27 @@ } } + function getVariantId(variant) { + var hasLanguage = variant.language && !!variant.language.culture; + var hasSegment = !!variant.segment; + + var sep = ";"; + + if (!hasLanguage && !hasSegment) { + // Invariant + return ""; + } else if (hasLanguage && !hasSegment) { + // Culture only + return variant.language.culture; + } else if (!hasLanguage && hasSegment) { + // Segment only + return sep + variant.segment; + } else { + // Culture and Segment + return variant.language.culture + sep + variant.segment; + } + } + function initVariant(variant, editorIndex) { //The model that is assigned to the editor contains the current content variant along //with a copy of the contentApps. This is required because each editor renders it's own @@ -161,7 +185,7 @@ if (!variant.variants) { variant.variants = _.map(vm.content.variants, function (v) { - return _.pick(v, "active", "language", "state"); + return _.pick(v, "active", "language", "segment", "state"); }); } else { @@ -169,13 +193,14 @@ angular.extend(variant.variants, _.map(vm.content.variants, function (v) { - return _.pick(v, "active", "language", "state"); + return _.pick(v, "active", "language", "segment", "state"); })); } //ensure the current culture is set as the active one for (var i = 0; i < variant.variants.length; i++) { - if (variant.variants[i].language.culture === variant.language.culture) { + if (variant.variants[i].language.culture === variant.language.culture && + variant.variants[i].segment === variant.segment) { variant.variants[i].active = true; } else { @@ -183,12 +208,13 @@ } } + var variantId = getVariantId(variant); // keep track of the open variants across the different split views // push the first variant then update the variant index based on the editor index - if(vm.openVariants && vm.openVariants.length === 0) { - vm.openVariants.push(variant.language.culture); + if (vm.openVariants && vm.openVariants.length === 0) { + vm.openVariants.push(variantId); } else { - vm.openVariants[editorIndex] = variant.language.culture; + vm.openVariants[editorIndex] = variantId; } } @@ -221,11 +247,11 @@ * @param {any} selectedVariant */ function openSplitView(selectedVariant) { - var selectedCulture = selectedVariant.language.culture; + var variant = getVariantId(selectedVariant); //Find the whole variant model based on the culture that was chosen var variant = _.find(vm.content.variants, function (v) { - return v.language.culture === selectedCulture; + return getVariantId(v) === variant; }); insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); @@ -282,8 +308,10 @@ */ function selectVariant(variant, editorIndex) { + var variantId = getVariantId(variant); + // prevent variants already open in a split view to be opened - if(vm.openVariants.indexOf(variant.language.culture) !== -1) { + if (vm.openVariants.indexOf(variantId) !== -1) { return; } @@ -292,7 +320,7 @@ if (editorIndex === 0) { //If we've made it this far, then update the query string. //The editor will respond to this query string changing. - $location.search("cculture", variant.language.culture); + $location.search("cculture", variantId); } else { @@ -307,7 +335,7 @@ //get the variant content model and initialize the editor with that var contentVariant = _.find(vm.content.variants, function (v) { - return v.language.culture === variant.language.culture; + return getVariantId(v) === variantId; }); editor.content = initVariant(contentVariant, editorIndex); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html index 8dd78883c2..da91e0bee5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -9,7 +9,7 @@
Date: Fri, 11 Oct 2019 14:18:46 +0200 Subject: [PATCH 011/610] Support for saving edited segment values --- .../src/common/services/umbdataformatter.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index a03a71febe..c2d0c9b7f0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -368,6 +368,7 @@ name: v.name || "", //if its null/empty,we must pass up an empty string else we get json converter errors properties: getContentProperties(v.tabs), culture: v.language ? v.language.culture : null, + segment: v.segment, publish: v.publish, save: v.save, releaseDate: v.releaseDate, From 318a511aec5b43da86737793098fcf4a2363e2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Fri, 30 Aug 2019 11:38:40 +0200 Subject: [PATCH 012/610] Improve support for invariant properties in combination with segments --- .../src/common/services/umbdataformatter.service.js | 12 ++++++++++-- .../views/components/content/umb-tabbed-content.html | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index c2d0c9b7f0..f7a7a52ee8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -410,7 +410,7 @@ _.each(tab.properties, function (property, propIndex) { //in theory if there's more than 1 variant, that means they would all have a language //but we'll do our safety checks anyways here - if (firstVariant.language && !property.culture) { + if (firstVariant.language && !property.culture && !property.segment) { invariantProperties.push({ tabIndex: tabIndex, propIndex: propIndex, @@ -426,7 +426,15 @@ var variant = displayModel.variants[j]; _.each(invariantProperties, function (invProp) { - variant.tabs[invProp.tabIndex].properties[invProp.propIndex] = invProp.property; + var tab = variant.tabs[invProp.tabIndex]; + var prop = tab.properties[invProp.propIndex]; + + if (prop.segment) { + // Do not touch segmented properties + return; + } + + tab.properties[invProp.propIndex] = invProp.property; }); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index 9cdafd82b1..8d7bdad873 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -11,13 +11,13 @@ data-element="property-{{property.alias}}" ng-repeat="property in group.properties track by property.alias" property="property" - show-inherit="content.variants.length > 1 && !property.culture && !activeVariant.language.isDefault" + show-inherit="content.variants.length > 1 && ((!activeVariant.language.isDefault && !property.culture) || (activeVariant.segment && !property.segment)) && !property.unlockInvariantValue" inherits-from="defaultVariant.language.name"> -
+
+ preview="content.variants.length > 1 && ((!activeVariant.language.isDefault && !property.culture) || (activeVariant.segment && !property.segment)) && !property.unlockInvariantValue">
From 73ddba5158ed0f64ecf7d7e4f0e589f3cad62747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Fri, 30 Aug 2019 13:32:44 +0200 Subject: [PATCH 013/610] Show segment name in switcher. --- .../umbeditorcontentheader.directive.js | 321 ++++++++++-------- .../editor/umb-editor-content-header.html | 10 +- 2 files changed, 186 insertions(+), 145 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index b3948bd7c4..09d44e0578 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -3,10 +3,10 @@ function EditorContentHeader(serverValidationManager, localizationService, editorState) { - function link(scope, el, attr, ctrl) { + var unsubscribe = []; - + if (!scope.serverValidationNameField) { scope.serverValidationNameField = "Name"; } @@ -15,36 +15,30 @@ } scope.isNew = scope.content.state == "NotCreated"; - - localizationService.localizeMany([ - scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit", - "placeholders_a11yName", - scope.isNew ? "general_new" : "general_edit"] - ).then(function (data) { + localizationService.localizeMany([ + scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit", + "placeholders_a11yName"] + ).then(function (data) { scope.a11yMessage = data[0]; scope.a11yName = data[1]; - var title = data[2] + ": "; if (!scope.isNew) { scope.a11yMessage += " " + scope.content.name; - title += scope.content.name; + } else { var name = editorState.current.contentTypeName; scope.a11yMessage += " " + name; scope.a11yName = name + " " + scope.a11yName; - title += name; } - - scope.$emit("$changeTitle", title); }); scope.vm = {}; scope.vm.dropdownOpen = false; scope.vm.currentVariant = ""; scope.vm.variantsWithError = []; scope.vm.defaultVariant = null; - + scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors. - + function checkErrorsOnOtherVariants() { var check = false; angular.forEach(scope.content.variants, function (variant) { @@ -54,10 +48,10 @@ }); scope.vm.errorsOnOtherVariants = check; } - + function onCultureValidation(valid, errors, allErrors, culture) { var index = scope.vm.variantsWithError.indexOf(culture); - if (valid === true) { + if(valid === true) { if (index !== -1) { scope.vm.variantsWithError.splice(index, 1); } @@ -68,165 +62,212 @@ } checkErrorsOnOtherVariants(); } - + function onInit() { - + // find default. angular.forEach(scope.content.variants, function (variant) { if (variant.language.isDefault) { scope.vm.defaultVariant = variant; } }); - + setCurrentVariant(); - + angular.forEach(scope.content.apps, (app) => { if (app.alias === "umbContent") { app.anchors = scope.content.tabs; } }); + + + angular.forEach(scope.content.variants, function (variant) { + unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation)); + }); + + unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation)); + + + + } - - angular.forEach(scope.content.variants, function (variant) { - unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation)); - }); - - unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation)); - - - - } - - function setCurrentVariant() { - angular.forEach(scope.content.variants, function (variant) { - if (variant.active) { - scope.vm.currentVariant = variant; - checkErrorsOnOtherVariants(); + function getVariantDisplayName(variant) { + if (variant == null) { + return ""; } - }); - } - scope.goBack = function () { - if (scope.onBack) { - scope.onBack(); + var parts = []; + + if (variant.language && variant.language.name) { + parts.push(variant.language.name); + } + + if (variant.segment) { + parts.push(variant.segment); + } + + if (parts.length === 0) { + // Invariant + parts.push("Default"); + } + + return parts.join(" - "); } - }; - scope.selectVariant = function (event, variant) { - - if (scope.onSelectVariant) { - scope.vm.dropdownOpen = false; - scope.onSelectVariant({ "variant": variant }); + function setCurrentVariant() { + angular.forEach(scope.content.variants, function (variant) { + if (variant.active) { + scope.vm.currentVariant = variant; + checkErrorsOnOtherVariants(); + } + }); } - }; - scope.selectNavigationItem = function (item) { - if (scope.onSelectNavigationItem) { - scope.onSelectNavigationItem({ "item": item }); - } - } + scope.getVariantDisplayName = getVariantDisplayName; - scope.selectAnchorItem = function (item, anchor) { - if (scope.onSelectAnchorItem) { - scope.onSelectAnchorItem({ "item": item, "anchor": anchor }); - } - } + scope.goBack = function () { + if (scope.onBack) { + scope.onBack(); + } + }; - scope.closeSplitView = function () { - if (scope.onCloseSplitView) { - scope.onCloseSplitView(); - } - }; + scope.selectVariant = function (event, variant) { - scope.openInSplitView = function (event, variant) { - if (scope.onOpenInSplitView) { - scope.vm.dropdownOpen = false; - scope.onOpenInSplitView({ "variant": variant }); - } - }; + if (scope.onSelectVariant) { + scope.vm.dropdownOpen = false; + scope.onSelectVariant({ "variant": variant }); + } + }; - /** - * keep track of open variants - this is used to prevent the same variant to be open in more than one split view - * @param {any} culture - */ - scope.variantIsOpen = function (culture) { - return (scope.openVariants.indexOf(culture) !== -1); - } - - /** - * Check whether a variant has a error, used to display errors in variant switcher. - * @param {any} culture - */ - scope.variantHasError = function (culture) { - // if we are looking for the default language we also want to check for invariant. - if (culture === scope.vm.defaultVariant.language.culture) { - if (scope.vm.variantsWithError.indexOf("invariant") !== -1) { - return true; + scope.selectNavigationItem = function(item) { + if(scope.onSelectNavigationItem) { + scope.onSelectNavigationItem({"item": item}); } } - if (scope.vm.variantsWithError.indexOf(culture) !== -1) { - return true; + + scope.selectAnchorItem = function(item, anchor) { + if(scope.onSelectAnchorItem) { + scope.onSelectAnchorItem({"item": item, "anchor": anchor}); + } } - return false; - } - onInit(); + scope.closeSplitView = function () { + if (scope.onCloseSplitView) { + scope.onCloseSplitView(); + } + }; - //watch for the active culture changing, if it changes, update the current variant - if (scope.content.variants) { - scope.$watch(function () { - for (var i = 0; i < scope.content.variants.length; i++) { - var v = scope.content.variants[i]; - if (v.active) { - return v.language.culture; + scope.openInSplitView = function (event, variant) { + if (scope.onOpenInSplitView) { + scope.vm.dropdownOpen = false; + scope.onOpenInSplitView({ "variant": variant }); + } + }; + + function getVariantId(variant) { + var hasLanguage = variant.language && !!variant.language.culture; + var hasSegment = !!variant.segment; + + var sep = ";"; + + if (!hasLanguage && !hasSegment) { + // Invariant + return ""; + } else if (hasLanguage && !hasSegment) { + // Culture only + return variant.language.culture; + } else if (!hasLanguage && hasSegment) { + // Segment only + return sep + variant.segment; + } else { + // Culture and Segment + return variant.language.culture + sep + variant.segment; + } + } + + /** + * keep track of open variants - this is used to prevent the same variant to be open in more than one split view + * @param {any} culture + */ + scope.variantIsOpen = function (variant) { + var variantId = getVariantId(variant); + return (scope.openVariants.indexOf(variantId) !== -1); + } + + /** + * Check whether a variant has a error, used to display errors in variant switcher. + * @param {any} culture + */ + scope.variantHasError = function(culture) { + // if we are looking for the default language we also want to check for invariant. + if (culture === scope.vm.defaultVariant.language.culture) { + if(scope.vm.variantsWithError.indexOf("invariant") !== -1) { + return true; } } - return scope.vm.currentVariant.language.culture; //should never get here - }, function (newValue, oldValue) { - if (newValue !== scope.vm.currentVariant.language.culture) { - setCurrentVariant(); + if(scope.vm.variantsWithError.indexOf(culture) !== -1) { + return true; + } + return false; + } + + onInit(); + + //watch for the active culture changing, if it changes, update the current variant + if (scope.content.variants) { + scope.$watch(function () { + for (var i = 0; i < scope.content.variants.length; i++) { + var v = scope.content.variants[i]; + if (v.active) { + return v.language.culture; + } + } + return scope.vm.currentVariant.language.culture; //should never get here + }, function (newValue, oldValue) { + if (newValue !== scope.vm.currentVariant.language.culture) { + setCurrentVariant(); + } + }); + } + + scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); } }); } - scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-content-header.html', + scope: { + name: "=", + nameDisabled: " - {{vm.currentVariant.language.name}} +   - + - {{vm.currentVariant.language.name}} + - + - {{variant.language.name}} +
Open in split view
From f11f4c0a4efe67c75e8f3952a9d917812ad5a6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Fri, 30 Aug 2019 14:41:32 +0200 Subject: [PATCH 014/610] Show segment name in Publish dialog (cherry picked from commit 43679c0b8a591ee9459b397ceb4ef0f9abcbc604) --- .../content/overlays/publish.controller.js | 26 +++++++++++++++++++ .../src/views/content/overlays/publish.html | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index 8caa38ea17..fe667f31b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -12,6 +12,32 @@ vm.dirtyVariantFilter = dirtyVariantFilter; vm.pristineVariantFilter = pristineVariantFilter; + $scope.getVariantDisplayName = getVariantDisplayName; + + // TODO: Move to some variantService / helper + function getVariantDisplayName(variant) { + if (variant == null) { + return ""; + } + + var parts = []; + + if (variant.language && variant.language.name) { + parts.push(variant.language.name); + } + + if (variant.segment) { + parts.push(variant.segment); + } + + if (parts.length === 0) { + // Invariant + parts.push("Default"); + } + + return parts.join(" - "); + } + /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index a9afdffda6..7152c70170 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -23,7 +23,7 @@ server-validation-field="{{variant.htmlId}}" />
diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 36750d74ec..22a546ad7d 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -945,70 +945,73 @@ namespace Umbraco.Web.Editors return hasPathAccess; } - /// - /// Returns the references (usages) for the media item - /// - /// - /// - public MediaReferences GetReferences(int id) + public PagedResult GetPagedContentReferences(int id, int pageNumber = 1, int pageSize = 100) { - var result = new MediaReferences(); - - var relations = Services.RelationService.GetByChildId(id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); - var relationEntities = Services.RelationService.GetParentEntitiesFromRelations(relations).ToList(); - - var documents = new List(); - var members = new List(); - var media = new List(); - - foreach (var item in relationEntities) + if (pageNumber <= 0 || pageSize <= 0) { - switch (item) - { - case DocumentEntitySlim doc: - documents.Add(new MediaReferences.EntityTypeReferences { - Id = doc.Id, - Key = doc.Key, - Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), - Icon = doc.ContentTypeIcon, - Name = doc.Name, - Alias = doc.ContentTypeAlias - }); - break; - - case MemberEntitySlim memb: - members.Add(new MediaReferences.EntityTypeReferences - { - Id = memb.Id, - Key = memb.Key, - Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), - Icon = memb.ContentTypeIcon, - Name = memb.Name, - Alias = memb.ContentTypeAlias - }); - break; - - case MediaEntitySlim med: - media.Add(new MediaReferences.EntityTypeReferences - { - Id = med.Id, - Key = med.Key, - Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), - Icon = med.ContentTypeIcon, - Name = med.Name, - Alias = med.ContentTypeAlias - }); - break; - - default: - break; - } + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - result.Content = documents; - result.Members = members; - result.Media = media; - return result; + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Document); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Cast().Select(doc => new EntityTypeReferences + { + Id = doc.Id, + Key = doc.Key, + Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), + Icon = doc.ContentTypeIcon, + Name = doc.Name, + Alias = doc.ContentTypeAlias + }) + }; + } + + public PagedResult GetPagedMemberReferences(int id, int pageNumber = 1, int pageSize = 100) + { + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Member); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Cast().Select(memb => new EntityTypeReferences + { + Id = memb.Id, + Key = memb.Key, + Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), + Icon = memb.ContentTypeIcon, + Name = memb.Name, + Alias = memb.ContentTypeAlias + }) + }; + } + + public PagedResult GetPagedMediaReferences(int id, int pageNumber = 1, int pageSize = 100) + { + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } + + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Media); + + return new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = relations.Cast().Select(med => new EntityTypeReferences + { + Id = med.Id, + Key = med.Key, + Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), + Icon = med.ContentTypeIcon, + Name = med.Name, + Alias = med.ContentTypeAlias + }) + }; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs index b10022b105..a1fbdfa1e1 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs @@ -1,24 +1,9 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; +using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing { - [DataContract(Name = "mediaReferences", Namespace = "")] - public class MediaReferences + [DataContract(Name = "entityType", Namespace = "")] + public class EntityTypeReferences : EntityBasic { - [DataMember(Name = "content")] - public IEnumerable Content { get; set; } = Enumerable.Empty(); - - [DataMember(Name = "members")] - public IEnumerable Members { get; set; } = Enumerable.Empty(); - - [DataMember(Name = "media")] - public IEnumerable Media { get; set; } = Enumerable.Empty(); - - [DataContract(Name = "entityType", Namespace = "")] - public class EntityTypeReferences : EntityBasic - { - } } } From 675b6e7b7c8c009d5b75adcabdd19e020d8eccb9 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 15 Nov 2019 13:53:56 +0000 Subject: [PATCH 114/610] Remove test page size of 1 to check if each pager was working fine --- .../directives/components/media/umbmedianodeinfo.directive.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index 6f1dde18a0..d21a31e2a8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -12,17 +12,14 @@ scope.changeContentPageNumber = changeContentPageNumber; scope.contentOptions = {}; - scope.contentOptions.pageSize = 1; scope.hasContentReferences = false; scope.changeMediaPageNumber = changeMediaPageNumber; scope.mediaOptions = {}; - scope.mediaOptions.pageSize = 1; scope.hasMediaReferences = false; scope.changeMemberPageNumber = changeMemberPageNumber; scope.memberOptions = {}; - scope.memberOptions.pageSize = 1; scope.hasMemberReferences = false; From 31b85a2cd6605b4478fe50a93406369b8381f045 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 18 Nov 2019 11:43:31 +0000 Subject: [PATCH 115/610] Remove code duplication & do some cleanup - need to work on passing the type variable to Cast --- .../media/umbmedianodeinfo.directive.js | 9 +- .../src/common/resources/media.resource.js | 65 ++------------ src/Umbraco.Web/Editors/MediaController.cs | 88 ++++++++----------- .../Models/ContentEditing/MediaReferences.cs | 9 -- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 5 files changed, 49 insertions(+), 123 deletions(-) delete mode 100644 src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index d21a31e2a8..a4c0e29fe4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -12,14 +12,17 @@ scope.changeContentPageNumber = changeContentPageNumber; scope.contentOptions = {}; + scope.contentOptions.entityType = "DOCUMENT"; scope.hasContentReferences = false; scope.changeMediaPageNumber = changeMediaPageNumber; scope.mediaOptions = {}; + scope.mediaOptions.entityType = "MEDIA"; scope.hasMediaReferences = false; scope.changeMemberPageNumber = changeMemberPageNumber; scope.memberOptions = {}; + scope.memberOptions.entityType = "MEMBER"; scope.hasMemberReferences = false; @@ -124,7 +127,7 @@ } function loadContentRelations() { - return mediaResource.getPagedContentReferences($routeParams.id, scope.contentOptions) + return mediaResource.getPagedReferences($routeParams.id, scope.contentOptions) .then(function (data) { scope.contentReferences = data; scope.hasContentReferences = data.items.length > 0; @@ -132,7 +135,7 @@ } function loadMediaRelations() { - return mediaResource.getPagedMediaReferences($routeParams.id, scope.mediaOptions) + return mediaResource.getPagedReferences($routeParams.id, scope.mediaOptions) .then(function (data) { scope.mediaReferences = data; scope.hasMediaReferences = data.items.length > 0; @@ -140,7 +143,7 @@ } function loadMemberRelations() { - return mediaResource.getPagedMemberReferences($routeParams.id, scope.memberOptions) + return mediaResource.getPagedReferences($routeParams.id, scope.memberOptions) .then(function (data) { scope.memberReferences = data; scope.hasMemberReferences = data.items.length > 0; 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 cb8c883bb9..e4e3cc6f3f 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 @@ -554,11 +554,12 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to retrieve media items for search: ' + query); }, - getPagedContentReferences: function (id, options) { + getPagedReferences: function (id, options) { var defaults = { pageSize: 25, - pageNumber: 1 + pageNumber: 1, + entityType: "DOCUMENT" }; if (options === undefined) { options = {}; @@ -572,71 +573,17 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { $http.get( umbRequestHelper.getApiUrl( "mediaApiBaseUrl", - "GetPagedContentReferences", - { - id: id, - pageNumber: options.pageNumber, - pageSize: options.pageSize - } - )), - "Failed to retrieve usages for media of id " + id); - }, - - getPagedMemberReferences: function (id, options) { - - var defaults = { - pageSize: 25, - pageNumber: 1 - }; - 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( - "mediaApiBaseUrl", - "GetPagedMemberReferences", - { - id: id, - pageNumber: options.pageNumber, - pageSize: options.pageSize - } - )), - "Failed to retrieve usages for media of id " + id); - }, - - getPagedMediaReferences: function (id, options) { - - var defaults = { - pageSize: 25, - pageNumber: 1 - }; - 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( - "mediaApiBaseUrl", - "GetPagedMediaReferences", + "GetPagedReferences", { id: id, + entityType: options.entityType, pageNumber: options.pageNumber, pageSize: options.pageSize } )), "Failed to retrieve usages for media of id " + id); } + }; } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 22a546ad7d..2d64ae252a 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -945,71 +945,57 @@ namespace Umbraco.Web.Editors return hasPathAccess; } - public PagedResult GetPagedContentReferences(int id, int pageNumber = 1, int pageSize = 100) + public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) { if (pageNumber <= 0 || pageSize <= 0) { throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Document); + UmbracoObjectTypes entity; + string udiEntity; + Type castType; - return new PagedResult(totalRecords, pageNumber, pageSize) + switch (entityType.ToUpperInvariant()) { - Items = relations.Cast().Select(doc => new EntityTypeReferences - { - Id = doc.Id, - Key = doc.Key, - Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), - Icon = doc.ContentTypeIcon, - Name = doc.Name, - Alias = doc.ContentTypeAlias - }) - }; - } + case "DOCUMENT": + entity = UmbracoObjectTypes.Document; + udiEntity = Constants.UdiEntityType.Document; + castType = typeof(DocumentEntitySlim); + break; - public PagedResult GetPagedMemberReferences(int id, int pageNumber = 1, int pageSize = 100) - { - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + case "MEDIA": + entity = UmbracoObjectTypes.Media; + udiEntity = Constants.UdiEntityType.Media; + castType = typeof(MediaEntitySlim); + break; + + case "MEMBER": + entity = UmbracoObjectTypes.Member; + udiEntity = Constants.UdiEntityType.Member; + castType = typeof(MemberEntitySlim); + break; + + default: + entity = UmbracoObjectTypes.Document; + udiEntity = Constants.UdiEntityType.Document; + castType = typeof(DocumentEntitySlim); + break; } - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Member); + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, entity); - return new PagedResult(totalRecords, pageNumber, pageSize) + + return new PagedResult(totalRecords, pageNumber, pageSize) { - Items = relations.Cast().Select(memb => new EntityTypeReferences + Items = relations.Cast().Select(rel => new EntityBasic { - Id = memb.Id, - Key = memb.Key, - Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), - Icon = memb.ContentTypeIcon, - Name = memb.Name, - Alias = memb.ContentTypeAlias - }) - }; - } - - public PagedResult GetPagedMediaReferences(int id, int pageNumber = 1, int pageSize = 100) - { - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } - - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, UmbracoObjectTypes.Media); - - return new PagedResult(totalRecords, pageNumber, pageSize) - { - Items = relations.Cast().Select(med => new EntityTypeReferences - { - Id = med.Id, - Key = med.Key, - Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), - Icon = med.ContentTypeIcon, - Name = med.Name, - Alias = med.ContentTypeAlias + Id = rel.Id, + Key = rel.Key, + Udi = Udi.Create(udiEntity, rel.Key), + Icon = rel.ContentTypeIcon, + Name = rel.Name, + Alias = rel.ContentTypeAlias }) }; } diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs deleted file mode 100644 index a1fbdfa1e1..0000000000 --- a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models.ContentEditing -{ - [DataContract(Name = "entityType", Namespace = "")] - public class EntityTypeReferences : EntityBasic - { - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 361a548123..5eca5ad7fe 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -222,7 +222,6 @@ - From dc494ff52587d3e29530fde75ac2b01ad58ae3da Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 18 Nov 2019 12:18:50 +0000 Subject: [PATCH 116/610] MemberEntitySlim has the same properties as ContentEntitySlim so lets inherit it --- src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs | 11 ++--------- src/Umbraco.Web/Editors/MediaController.cs | 9 ++------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs index 335e269467..338f363856 100644 --- a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs +++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs @@ -1,13 +1,6 @@ namespace Umbraco.Core.Models.Entities { - public class MemberEntitySlim : EntitySlim, IMemberEntitySlim + public class MemberEntitySlim : ContentEntitySlim, IMemberEntitySlim { - public string ContentTypeAlias { get; set; } - - /// - public string ContentTypeIcon { get; set; } - - /// - public string ContentTypeThumbnail { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 2d64ae252a..751e386edb 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -954,32 +954,27 @@ namespace Umbraco.Web.Editors UmbracoObjectTypes entity; string udiEntity; - Type castType; switch (entityType.ToUpperInvariant()) { - case "DOCUMENT": + case "DOCUMENT": entity = UmbracoObjectTypes.Document; udiEntity = Constants.UdiEntityType.Document; - castType = typeof(DocumentEntitySlim); break; case "MEDIA": entity = UmbracoObjectTypes.Media; udiEntity = Constants.UdiEntityType.Media; - castType = typeof(MediaEntitySlim); break; case "MEMBER": entity = UmbracoObjectTypes.Member; udiEntity = Constants.UdiEntityType.Member; - castType = typeof(MemberEntitySlim); break; default: entity = UmbracoObjectTypes.Document; udiEntity = Constants.UdiEntityType.Document; - castType = typeof(DocumentEntitySlim); break; } @@ -988,7 +983,7 @@ namespace Umbraco.Web.Editors return new PagedResult(totalRecords, pageNumber, pageSize) { - Items = relations.Cast().Select(rel => new EntityBasic + Items = relations.Cast().Select(rel => new EntityBasic { Id = rel.Id, Key = rel.Key, From 895f68d9e2c6283bce0bd1dd3ef838f577467f2b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2019 12:15:27 +1100 Subject: [PATCH 117/610] Fixes bulk insert records, adjusts parsing of object types --- src/Umbraco.Core/Models/ObjectTypes.cs | 2 +- .../NPocoDatabaseExtensions-Bulk.cs | 18 +++++++++-- .../Implement/RelationRepository.cs | 2 ++ .../Persistence/UmbracoDatabase.cs | 4 +++ src/Umbraco.Web/Editors/MediaController.cs | 32 +++---------------- 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Models/ObjectTypes.cs b/src/Umbraco.Core/Models/ObjectTypes.cs index dd943ee02b..2ddbdca77a 100644 --- a/src/Umbraco.Core/Models/ObjectTypes.cs +++ b/src/Umbraco.Core/Models/ObjectTypes.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core.Models /// public static UmbracoObjectTypes GetUmbracoObjectType(string name) { - return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, false); + return (UmbracoObjectTypes) Enum.Parse(typeof (UmbracoObjectTypes), name, true); } #region Guid object type utilities diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs index 0574e37c4c..10db1ca18e 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -14,7 +14,21 @@ namespace Umbraco.Core.Persistence /// public static partial class NPocoDatabaseExtensions { - // TODO: review NPoco native InsertBulk to replace the code below + /// + /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the underlying RetryDbConnection and ProfiledDbTransaction + /// + /// + /// This is required to use NPoco's own method because we use wrapped DbConnection and DbTransaction instances. + /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for bulk inserting of records for + /// any other database type and in which case will just insert records one at a time. + /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own BulkInsertRecords methods + /// do not handle this scenario. + /// + public static void ConfigureNPocoBulkExtensions() + { + SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); + SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); + } /// /// Bulk-inserts records within a transaction. @@ -235,7 +249,7 @@ namespace Umbraco.Core.Persistence //we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared //to the order in which they are declared in the model then this will not work, so instead we will add column mappings by name so that this explicitly uses //the names instead of their ordering. - foreach(var col in bulkReader.ColumnMappings) + foreach (var col in bulkReader.ColumnMappings) { copy.ColumnMappings.Add(col.DestinationColumn, col.DestinationColumn); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index cff5e48854..56a6336f75 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -236,6 +236,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement }, RelationFactory.BuildDto); // value = DTO + // Use NPoco's own InsertBulk command which will automatically re-populate the new Ids on the entities, our own + // BulkInsertRecords does not cater for this. Database.InsertBulk(entitiesAndDtos.Values); // All dtos now have IDs assigned diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 072813b4e6..a95d95ea08 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -44,6 +44,8 @@ namespace Umbraco.Core.Persistence _commandRetryPolicy = commandRetryPolicy; EnableSqlTrace = EnableSqlTraceDefault; + + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); } /// @@ -57,6 +59,8 @@ namespace Umbraco.Core.Persistence _logger = logger; EnableSqlTrace = EnableSqlTraceDefault; + + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); } #endregion diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 751e386edb..e9b879c48b 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -952,34 +952,10 @@ namespace Umbraco.Web.Editors throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); } - UmbracoObjectTypes entity; - string udiEntity; - - switch (entityType.ToUpperInvariant()) - { - case "DOCUMENT": - entity = UmbracoObjectTypes.Document; - udiEntity = Constants.UdiEntityType.Document; - break; - - case "MEDIA": - entity = UmbracoObjectTypes.Media; - udiEntity = Constants.UdiEntityType.Media; - break; - - case "MEMBER": - entity = UmbracoObjectTypes.Member; - udiEntity = Constants.UdiEntityType.Member; - break; - - default: - entity = UmbracoObjectTypes.Document; - udiEntity = Constants.UdiEntityType.Document; - break; - } - - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out long totalRecords, entity); + var objectType = ObjectTypes.GetUmbracoObjectType(entityType); + var udiType = ObjectTypes.GetUdiType(objectType); + var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out var totalRecords, objectType); return new PagedResult(totalRecords, pageNumber, pageSize) { @@ -987,7 +963,7 @@ namespace Umbraco.Web.Editors { Id = rel.Id, Key = rel.Key, - Udi = Udi.Create(udiEntity, rel.Key), + Udi = Udi.Create(udiType, rel.Key), Icon = rel.ContentTypeIcon, Name = rel.Name, Alias = rel.ContentTypeAlias From beebdce0a2f1b1eceade48d283369ed30def73c9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Nov 2019 12:18:14 +1100 Subject: [PATCH 118/610] Fixes $routeParams issue --- .../components/media/umbmedianodeinfo.directive.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index a4c0e29fe4..dfa1afc247 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $routeParams, $q) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $q) { function link(scope, element, attrs, ctrl) { @@ -127,7 +127,7 @@ } function loadContentRelations() { - return mediaResource.getPagedReferences($routeParams.id, scope.contentOptions) + return mediaResource.getPagedReferences(scope.node.id, scope.contentOptions) .then(function (data) { scope.contentReferences = data; scope.hasContentReferences = data.items.length > 0; @@ -135,7 +135,7 @@ } function loadMediaRelations() { - return mediaResource.getPagedReferences($routeParams.id, scope.mediaOptions) + return mediaResource.getPagedReferences(scope.node.id, scope.mediaOptions) .then(function (data) { scope.mediaReferences = data; scope.hasMediaReferences = data.items.length > 0; @@ -143,7 +143,7 @@ } function loadMemberRelations() { - return mediaResource.getPagedReferences($routeParams.id, scope.memberOptions) + return mediaResource.getPagedReferences(scope.node.id, scope.memberOptions) .then(function (data) { scope.memberReferences = data; scope.hasMemberReferences = data.items.length > 0; From b13120f0a11c25968d554f3064e485f8d651070e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Nov 2019 11:32:11 +0100 Subject: [PATCH 119/610] minor style adjustment for media references --- .../src/views/components/media/umb-media-node-info.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 21a28a905f..a606aa5588 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -6,7 +6,7 @@
- + @@ -43,7 +43,7 @@
-
+
Used in Documents
@@ -79,7 +79,7 @@
-
+
Used in Members
@@ -115,7 +115,7 @@
-
+
Used in Media
From 2420b9c25362dc1bf55314edbe6a864c34f1bb64 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 26 Nov 2019 10:28:56 +0000 Subject: [PATCH 120/610] Update CoreComposer to use the easier to read extension method on composition as opposed to declaring WithCollectionBuilder --- src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index 1f004846d0..86e61aeb90 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -43,7 +43,7 @@ namespace Umbraco.Core.Runtime // register persistence mappers - required by database factory so needs to be done here // means the only place the collection can be modified is in a runtime - afterwards it // has been frozen and it is too late - composition.WithCollectionBuilder().AddCoreMappers(); + composition.Mappers().AddCoreMappers(); // register the scope provider composition.RegisterUnique(); // implements both IScopeProvider and IScopeAccessor @@ -70,7 +70,7 @@ namespace Umbraco.Core.Runtime composition.ManifestFilters(); // properties and parameters derive from data editors - composition.WithCollectionBuilder() + composition.DataEditors() .Add(() => composition.TypeLoader.GetDataEditors()); composition.RegisterUnique(); composition.RegisterUnique(); @@ -101,13 +101,13 @@ namespace Umbraco.Core.Runtime factory.GetInstance(), true, new DatabaseServerMessengerOptions())); - composition.WithCollectionBuilder() + composition.CacheRefreshers() .Add(() => composition.TypeLoader.GetCacheRefreshers()); - composition.WithCollectionBuilder() + composition.PackageActions() .Add(() => composition.TypeLoader.GetPackageActions()); - composition.WithCollectionBuilder() + composition.PropertyValueConverters() .Append(composition.TypeLoader.GetTypes()); composition.RegisterUnique(); @@ -115,7 +115,7 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(factory => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetInstance()))); - composition.WithCollectionBuilder() + composition.UrlSegmentProviders() .Append(); composition.RegisterUnique(factory => new MigrationBuilder(factory)); From 3ead51ff64c13e8ff066181fcd0d830e8bde1048 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 26 Nov 2019 11:05:30 +0000 Subject: [PATCH 121/610] Update WebInitComposer to use the easier to read extension method on composition as opposed to declaring WithCollectionBuilder --- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 4de5e8627a..27d0b5a1ce 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -150,19 +150,19 @@ namespace Umbraco.Web.Runtime .ComposeUmbracoControllers(GetType().Assembly) .SetDefaultRenderMvcController(); // default controller for template views - composition.WithCollectionBuilder() + composition.SearchableTrees() .Add(() => composition.TypeLoader.GetTypes()); composition.Register(Lifetime.Request); - composition.WithCollectionBuilder() + composition.EditorValidators() .Add(() => composition.TypeLoader.GetTypes()); - composition.WithCollectionBuilder(); + composition.TourFilters(); composition.RegisterUnique(); - composition.WithCollectionBuilder() + composition.Actions() .Add(() => composition.TypeLoader.GetTypes()); //we need to eagerly scan controller types since they will need to be routed @@ -177,26 +177,26 @@ namespace Umbraco.Web.Runtime // here because there cannot be two converters for one property editor - and we want the full // RteMacroRenderingValueConverter that converts macros, etc. So remove TinyMceValueConverter. // (the limited one, defined in Core, is there for tests) - same for others - composition.WithCollectionBuilder() + composition.PropertyValueConverters() .Remove() .Remove() .Remove(); // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax - composition.WithCollectionBuilder() + composition.FilteredControllerFactory() .Append(); - composition.WithCollectionBuilder() + composition.UrlProviders() .Append() .Append(); - composition.WithCollectionBuilder() + composition.MediaUrlProviders() .Append(); composition.RegisterUnique(); - composition.WithCollectionBuilder() + composition.ContentFinders() // all built-in finders in the correct order, // devs can then modify this list on application startup .Append() @@ -211,7 +211,7 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); // register *all* checks, except those marked [HideFromTypeFinder] of course - composition.WithCollectionBuilder() + composition.HealthChecks() .Add(() => composition.TypeLoader.GetTypes()); composition.WithCollectionBuilder() @@ -231,13 +231,13 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); // register known content apps - composition.WithCollectionBuilder() + composition.ContentApps() .Append() .Append() .Append(); // register back office sections in the order we want them rendered - composition.WithCollectionBuilder() + composition.Sections() .Append() .Append() .Append() @@ -248,18 +248,18 @@ namespace Umbraco.Web.Runtime .Append(); // register core CMS dashboards and 3rd party types - will be ordered by weight attribute & merged with package.manifest dashboards - composition.WithCollectionBuilder() + composition.Dashboards() .Add(composition.TypeLoader.GetTypes()); // register back office trees // the collection builder only accepts types inheriting from TreeControllerBase // and will filter out those that are not attributed with TreeAttribute - composition.WithCollectionBuilder() + composition.Trees() .AddTreeControllers(umbracoApiControllerTypes.Where(x => typeof(TreeControllerBase).IsAssignableFrom(x))); // register OEmbed providers - no type scanning - all explicit opt-in of adding types // note: IEmbedProvider is not IDiscoverable - think about it if going for type scanning - composition.WithCollectionBuilder() + composition.OEmbedProviders() .Append() .Append() .Append() From 7573d528074871e980341b171fcb61b42b02c54a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 26 Nov 2019 11:09:33 +0000 Subject: [PATCH 122/610] Initial plumbing, extension methods etc for collection to store IDataValueReferences --- src/Umbraco.Core/Composing/Current.cs | 3 +++ src/Umbraco.Core/CompositionExtensions.cs | 7 +++++++ .../PropertyEditors/DataValueReferenceCollection.cs | 12 ++++++++++++ .../DataValueReferenceCollectionBuilder.cs | 9 +++++++++ src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 4 ++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 ++ src/Umbraco.Web/Composing/Current.cs | 2 ++ 7 files changed, 39 insertions(+) create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index f12bf0dd63..1bd56ed727 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -154,6 +154,9 @@ namespace Umbraco.Core.Composing public static DataEditorCollection DataEditors => Factory.GetInstance(); + public static DataValueReferenceCollection DataValueReferences + => Factory.GetInstance(); + public static PropertyEditorCollection PropertyEditors => Factory.GetInstance(); diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index 5dd33c2a60..d29f251edc 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -49,6 +49,13 @@ namespace Umbraco.Core public static DataEditorCollectionBuilder DataEditors(this Composition composition) => composition.WithCollectionBuilder(); + /// + /// Gets the data value reference collection builder. + /// + /// The composition. + public static DataValueReferenceCollectionBuilder DataValueReferences(this Composition composition) + => composition.WithCollectionBuilder(); + /// /// Gets the property value converters collection builder. /// diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs new file mode 100644 index 0000000000..6d0e3e62df --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceCollection : BuilderCollectionBase + { + public DataValueReferenceCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs new file mode 100644 index 0000000000..0f8ee1bd8e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceCollectionBuilder : LazyCollectionBuilderBase + { + protected override DataValueReferenceCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index 86e61aeb90..db308d720c 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -75,6 +75,10 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + // TODO: WB Add our collection + // Manually register stuff in this collection + composition.DataValueReferences(); + // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 225317943b..c6545a4f8b 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -281,6 +281,8 @@ + + diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 5be5e45ecd..1363d60b8a 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -182,6 +182,8 @@ namespace Umbraco.Web.Composing public static DataEditorCollection DataEditors => CoreCurrent.DataEditors; + public static DataValueReferenceCollection DataValueReferences => CoreCurrent.DataValueReferences; + public static PropertyEditorCollection PropertyEditors => CoreCurrent.PropertyEditors; public static ParameterEditorCollection ParameterEditors => CoreCurrent.ParameterEditors; From bd2c3bdcb3aedd474c2232f8509f9515158c3852 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Nov 2019 14:28:53 +1100 Subject: [PATCH 123/610] Creates test to assert the problem --- .../PublishedContent/NuCacheChildrenTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 5d32606ee7..cc9ffdba3c 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -918,6 +918,14 @@ namespace Umbraco.Tests.PublishedContent var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); _snapshotAccessor.PublishedSnapshot = snapshot; + var snapshotService = (PublishedSnapshotService)_snapshotService; + var contentStore = snapshotService.GetContentStore(); + + var parentNodes = contentStore.Test.GetValues(1); + var parentNode = parentNodes[0]; + AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); + Assert.AreEqual(1, parentNode.gen); + var documents = snapshot.Content.GetAtRoot().ToArray(); AssertDocuments(documents, "N1", "N2", "N3"); @@ -934,6 +942,15 @@ namespace Umbraco.Tests.PublishedContent new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshNode), }, out _, out _); + parentNodes = contentStore.Test.GetValues(1); + Assert.AreEqual(2, parentNodes.Length); + parentNode = parentNodes[1]; // get the first gen + AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same + Assert.AreEqual(1, parentNode.gen); + parentNode = parentNodes[0]; // get the latest gen + AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same + Assert.AreEqual(2, parentNode.gen); + documents = snapshot.Content.GetAtRoot().ToArray(); AssertDocuments(documents, "N1", "N2", "N3"); @@ -942,6 +959,8 @@ namespace Umbraco.Tests.PublishedContent documents = snapshot.Content.GetById(2).Children().ToArray(); AssertDocuments(documents, "N9", "N8", "N7"); + + } [Test] From 645a30a8aa0027fd49b85698bd26f4e1ee47a5e0 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 27 Nov 2019 20:24:16 +0000 Subject: [PATCH 124/610] Adds new ValueReference classes and update to Interface for IsForEditor method to check if we should get references for a specific dataeditor based on its alias --- .../Implement/ContentRepositoryBase.cs | 18 +++++-- .../Implement/DocumentBlueprintRepository.cs | 4 +- .../Implement/DocumentRepository.cs | 4 +- .../Repositories/Implement/MediaRepository.cs | 4 +- .../Implement/MemberRepository.cs | 4 +- .../DataValueReferenceCollectionBuilder.cs | 4 +- .../PropertyEditors/IDataValueReference.cs | 7 +++ .../Runtime/CoreInitialComposer.cs | 3 -- .../ContentPickerPropertyEditor.cs | 12 +---- .../PropertyEditors/GridPropertyEditor.cs | 26 +--------- .../MediaPickerPropertyEditor.cs | 12 +---- .../MultiNodeTreePickerPropertyEditor.cs | 15 +----- .../MultiUrlPickerValueEditor.cs | 5 +- .../NestedContentPropertyEditor.cs | 28 +---------- .../PropertyEditors/RichTextPropertyEditor.cs | 20 +------- .../ContentPickerPropertyValueReferences.cs | 22 +++++++++ .../GridPropertyValueReferences.cs | 37 ++++++++++++++ .../MediaPickerPropertyValueReferences.cs | 22 +++++++++ ...tiNodeTreePickerPropertyValueReferences.cs | 23 +++++++++ .../MultiUrlPickerValueReferences.cs | 26 ++++++++++ .../NestedContentPropertyValueReferences.cs | 49 +++++++++++++++++++ .../RichTextPropertyValueReferences.cs | 40 +++++++++++++++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 19 ++++++- src/Umbraco.Web/Umbraco.Web.csproj | 7 +++ 24 files changed, 281 insertions(+), 130 deletions(-) create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 65f6dc0472..c91a7e20e1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -36,6 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TRepository : class, IRepository { private readonly Lazy _propertyEditors; + private readonly DataValueReferenceCollection _dataValueReferences; /// /// @@ -49,13 +50,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors) + Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; RelationRepository = relationRepository; RelationTypeRepository = relationTypeRepository; _propertyEditors = propertyEditors; + _dataValueReferences = dataValueReferences; } protected abstract TRepository This { get; } @@ -826,15 +828,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var p in entity.Properties) { if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - var valueEditor = editor.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here if (!p.PropertyType.VariesByNothing()) continue; var val = p.GetValue(); // get the invariant value - var refs = reference.GetReferences(val); - trackedRelations.AddRange(refs); + + // WB: Loop over our collection of references registered and add references + foreach (var item in _dataValueReferences) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media items + if (item.IsForEditor(editor)) + trackedRelations.AddRange(item.GetReferences(val)); + } } //First delete all auto-relations for this entity diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index 60d4026ad5..5c2e73de9d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -20,8 +20,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection) + Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 5e3e7f05b9..e1ea955972 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -46,8 +46,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors) + Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index a3f9e45485..2297bbed2c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -29,8 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection) + Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 2871bf1dd3..c20e990a2b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -28,8 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors) + Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs index 0f8ee1bd8e..c6442275b0 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs @@ -2,8 +2,8 @@ namespace Umbraco.Core.PropertyEditors { - public class DataValueReferenceCollectionBuilder : LazyCollectionBuilderBase + public class DataValueReferenceCollectionBuilder : OrderedCollectionBuilderBase { - protected override DataValueReferenceCollectionBuilder This => this; + protected override DataValueReferenceCollectionBuilder This => this; } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index 8c0806a4a4..7d46704938 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -14,5 +14,12 @@ namespace Umbraco.Core.PropertyEditors /// /// IEnumerable GetReferences(object value); + + /// + /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). + /// + /// The datatype. + /// A value indicating whether the converter supports a datatype. + bool IsForEditor(IDataEditor dataEditor); } } diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index db308d720c..94d2b68121 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -75,9 +75,6 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - // TODO: WB Add our collection - // Manually register stuff in this collection - composition.DataValueReferences(); // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 683f1a05c3..2f45d98fa1 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -29,21 +29,11 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new ContentPickerPropertyValueEditor(Attribute); - internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference + internal class ContentPickerPropertyValueEditor : DataValueEditor { public ContentPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } - - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 228e058ee7..16dffb3b10 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); - internal class GridPropertyValueEditor : DataValueEditor, IDataValueReference + internal class GridPropertyValueEditor : DataValueEditor { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -156,30 +156,6 @@ namespace Umbraco.Web.PropertyEditors return grid; } - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); - - foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => - _richTextPropertyValueEditor.GetReferences(x.Value))) - { - yield return umbracoEntityReference; - } - - foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) - { - yield return umbracoEntityReference; - } - } } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index ece210b9d1..6416fa3342 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -33,21 +33,11 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MediaPickerPropertyValueEditor(Attribute); - internal class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference + internal class MediaPickerPropertyValueEditor : DataValueEditor { public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } - - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index 1da665a622..d7a7a2ed59 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -23,25 +23,12 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MultiNodeTreePickerPropertyValueEditor(Attribute); - public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference + public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor { public MultiNodeTreePickerPropertyValueEditor(DataEditorAttribute attribute): base(attribute) { } - - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var udiPaths = asString.Split(','); - foreach (var udiPath in udiPaths) - { - if (Udi.TryParse(udiPath, out var udi)) - yield return new UmbracoEntityReference(udi); - } - - } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 9c42fe6cbe..de641f69a3 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -15,7 +15,7 @@ using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference + public class MultiUrlPickerValueEditor : DataValueEditor { private readonly IEntityService _entityService; private readonly ILogger _logger; @@ -190,9 +190,6 @@ namespace Umbraco.Web.PropertyEditors } } - - - } } } diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 3d0605c4f9..865b583624 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService); - internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference + internal class NestedContentPropertyValueEditor : DataValueEditor { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; @@ -227,32 +227,6 @@ namespace Umbraco.Web.PropertyEditors // return json return JsonConvert.SerializeObject(deserialized); } - - public IEnumerable GetReferences(object value) - { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - - foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) - { - if (row.PropType == null) continue; - - var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; - - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - var val = row.JsonRowValue[row.PropKey]?.ToString(); - - var refs = reference.GetReferences(val); - - result.AddRange(refs); - } - - return result; - } - #endregion } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 427e36b37a..96b1d87205 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.PropertyEditors /// /// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for display in the editor /// - internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference + internal class RichTextPropertyValueEditor : DataValueEditor { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -130,24 +130,6 @@ namespace Umbraco.Web.PropertyEditors return parsed; } - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) - yield return new UmbracoEntityReference(udi); - - foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - yield return new UmbracoEntityReference(udi); - - //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs - } } internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs new file mode 100644 index 0000000000..08405b6d66 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class ContentPickerPropertyValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.ContentPicker); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs new file mode 100644 index 0000000000..85acff7ca3 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class GridPropertyValueReferences : IDataValueReference + { + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + return Enumerable.Empty(); + + //var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + ////TODO FIX SQUIGLES + //GridPropertyEditor.GridPropertyValueEditor.DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); + + //foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => + // _richTextPropertyValueEditor.GetReferences(x.Value))) + // yield return umbracoEntityReference; + + //foreach (var umbracoEntityReference in mediaValues.SelectMany(x => + // _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + // yield return umbracoEntityReference; + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs new file mode 100644 index 0000000000..ecd46b7ea5 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class MediaPickerPropertyValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MediaPicker); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs new file mode 100644 index 0000000000..4413f75080 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class MultiNodeTreePickerPropertyValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var udiPaths = asString.Split(','); + foreach (var udiPath in udiPaths) + if (Udi.TryParse(udiPath, out var udi)) + yield return new UmbracoEntityReference(udi); + + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs new file mode 100644 index 0000000000..5d8dfd4f39 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class MultiUrlPickerValueReferences : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + if (link.Udi != null) // Links can be absolute links without a Udi + yield return new UmbracoEntityReference(link.Udi); + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiUrlPicker); + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs new file mode 100644 index 0000000000..a872dde1c7 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using static Umbraco.Web.PropertyEditors.NestedContentPropertyEditor; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class NestedContentPropertyValueReferences : IDataValueReference + { + private PropertyEditorCollection _propertyEditors; + private NestedContentValues _nestedContentValues; + + //ARGH LightInject moaning at me + public NestedContentPropertyValueReferences(PropertyEditorCollection propertyEditors, IContentTypeService contentTypeService) + { + _propertyEditors = propertyEditors; + _nestedContentValues = new NestedContentValues(contentTypeService); + } + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) + { + if (row.PropType == null) continue; + + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = row.JsonRowValue[row.PropKey]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + + return result; + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs new file mode 100644 index 0000000000..744b0dc27c --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Templates; + +namespace Umbraco.Web.PropertyEditors.ValueReferences +{ + public class RichTextPropertyValueReferences : IDataValueReference + { + private HtmlImageSourceParser _imageSourceParser; + private HtmlLocalLinkParser _localLinkParser; + + public RichTextPropertyValueReferences(HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) + { + _imageSourceParser = imageSourceParser; + _localLinkParser = localLinkParser; + } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) + yield return new UmbracoEntityReference(udi); + + foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) + yield return new UmbracoEntityReference(udi); + + //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs + } + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce); + } +} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 27d0b5a1ce..295a31c461 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -42,6 +42,8 @@ using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; +using static Umbraco.Web.PropertyEditors.GridPropertyEditor; +using Umbraco.Web.PropertyEditors.ValueReferences; namespace Umbraco.Web.Runtime { @@ -274,7 +276,22 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append(); - + + // Used to determine if a datatype/editor should be storing/tracking + // references to media item/s + composition.DataValueReferences() + .Append() + + // TODO: Unsure how to call other ValueReference in collection + // When looking at grid item cells for RTE or media found + //.Append() + .Append() + .Append() + .Append() + + // TODO: LightInject problem + //.Append() + .Append(); // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5eca5ad7fe..22680898af 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -235,6 +235,13 @@ + + + + + + + From 69faaa8797d282eb5bfc5bd33eed1db32ab89e61 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 27 Nov 2019 20:29:38 +0000 Subject: [PATCH 125/610] Update tests to pass in an empty collection for the repository layers expecting the new dataValueReferencesCollection --- .../Persistence/Repositories/ContentTypeRepositoryTest.cs | 3 ++- .../Persistence/Repositories/DocumentRepositoryTest.cs | 3 ++- .../Persistence/Repositories/DomainRepositoryTest.cs | 3 ++- .../Persistence/Repositories/MediaRepositoryTest.cs | 3 ++- .../Persistence/Repositories/MemberRepositoryTest.cs | 3 ++- .../Persistence/Repositories/PublicAccessRepositoryTest.cs | 3 ++- .../Persistence/Repositories/TagRepositoryTest.cs | 6 ++++-- .../Persistence/Repositories/TemplateRepositoryTest.cs | 3 ++- .../Persistence/Repositories/UserRepositoryTest.cs | 6 ++++-- src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs | 3 ++- src/Umbraco.Tests/Services/ContentServiceTests.cs | 3 ++- 11 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 8ed935795d..141331d4f4 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -40,7 +40,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 3a36a647f0..76d52fb844 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -73,7 +73,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index ad27aed309..99adcd63e3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -31,7 +31,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index ced0ee75e9..c7503671b7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -46,7 +46,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index 7531d92b49..b9d034bd12 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -40,7 +40,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 9a65bde41f..b69adcbb67 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -315,7 +315,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index fe0db4563a..7f356897af 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -964,7 +964,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -980,7 +981,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 4bf50c6ffd..14aa9d8cf2 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -246,7 +246,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(ScopeProvider); var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate); // else, FK violation on contentType! diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index f4ab387683..fead39b6cb 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -35,7 +35,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -57,7 +58,8 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index 763635c393..d0f4ff95b3 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -52,7 +52,8 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 88b5cb5c34..cb097af2ef 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3211,7 +3211,8 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors); + var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } From 1471bffeffd1a9a9f68fc8b92b741b35bc227491 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:00:56 +0000 Subject: [PATCH 126/610] Adds DataValueReferenceFor & its collection --- src/Umbraco.Core/Composing/Current.cs | 4 +- src/Umbraco.Core/CompositionExtensions.cs | 4 +- .../Implement/ContentRepositoryBase.cs | 20 +++++--- .../Implement/DocumentBlueprintRepository.cs | 4 +- .../Implement/DocumentRepository.cs | 4 +- .../Repositories/Implement/MediaRepository.cs | 4 +- .../Implement/MemberRepository.cs | 4 +- .../DataValueReferenceCollection.cs | 12 ----- .../DataValueReferenceCollectionBuilder.cs | 9 ---- .../DataValueReferenceForCollection.cs | 12 +++++ .../DataValueReferenceForCollectionBuilder.cs | 9 ++++ .../PropertyEditors/IDataValueReference.cs | 9 +--- .../PropertyEditors/IDataValueReferenceFor.cs | 18 +++++++ src/Umbraco.Core/Umbraco.Core.csproj | 5 +- .../Repositories/ContentTypeRepositoryTest.cs | 2 +- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Repositories/DomainRepositoryTest.cs | 2 +- .../Repositories/MediaRepositoryTest.cs | 2 +- .../Repositories/MemberRepositoryTest.cs | 2 +- .../PublicAccessRepositoryTest.cs | 2 +- .../Repositories/TagRepositoryTest.cs | 4 +- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Repositories/UserRepositoryTest.cs | 4 +- .../Services/ContentServicePerformanceTest.cs | 2 +- .../Services/ContentServiceTests.cs | 2 +- src/Umbraco.Web/Composing/Current.cs | 2 +- .../ContentPickerPropertyEditor.cs | 12 ++++- .../PropertyEditors/GridPropertyEditor.cs | 21 +++++++- .../MediaPickerPropertyEditor.cs | 12 ++++- .../MultiNodeTreePickerPropertyEditor.cs | 12 ++++- .../MultiUrlPickerPropertyEditor.cs | 17 ++++++- .../NestedContentPropertyEditor.cs | 27 +++++++++- .../PropertyEditors/RichTextPropertyEditor.cs | 20 +++++++- .../ContentPickerPropertyValueReferences.cs | 22 --------- .../GridPropertyValueReferences.cs | 37 -------------- .../MediaPickerPropertyValueReferences.cs | 22 --------- ...tiNodeTreePickerPropertyValueReferences.cs | 23 --------- .../MultiUrlPickerValueReferences.cs | 26 ---------- .../NestedContentPropertyValueReferences.cs | 49 ------------------- .../RichTextPropertyValueReferences.cs | 40 --------------- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 21 +------- src/Umbraco.Web/Umbraco.Web.csproj | 7 --- 42 files changed, 199 insertions(+), 315 deletions(-) delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs create mode 100644 src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs delete mode 100644 src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index 1bd56ed727..899c465ca7 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -154,8 +154,8 @@ namespace Umbraco.Core.Composing public static DataEditorCollection DataEditors => Factory.GetInstance(); - public static DataValueReferenceCollection DataValueReferences - => Factory.GetInstance(); + public static DataValueReferenceForCollection DataValueReferenceFors + => Factory.GetInstance(); public static PropertyEditorCollection PropertyEditors => Factory.GetInstance(); diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index d29f251edc..aee4b41be8 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -53,8 +53,8 @@ namespace Umbraco.Core /// Gets the data value reference collection builder. /// /// The composition. - public static DataValueReferenceCollectionBuilder DataValueReferences(this Composition composition) - => composition.WithCollectionBuilder(); + public static DataValueReferenceForCollectionBuilder DataValueReferenceFors(this Composition composition) + => composition.WithCollectionBuilder(); ///
/// Gets the property value converters collection builder. diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index c91a7e20e1..77d36b8d43 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TRepository : class, IRepository { private readonly Lazy _propertyEditors; - private readonly DataValueReferenceCollection _dataValueReferences; + private readonly DataValueReferenceForCollection _dataValueReferenceFors; /// /// @@ -50,14 +50,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) + Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; RelationRepository = relationRepository; RelationTypeRepository = relationTypeRepository; _propertyEditors = propertyEditors; - _dataValueReferences = dataValueReferences; + _dataValueReferenceFors = dataValueReferenceFors; } protected abstract TRepository This { get; } @@ -828,20 +828,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var p in entity.Properties) { if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + var valueEditor = editor.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here if (!p.PropertyType.VariesByNothing()) continue; var val = p.GetValue(); // get the invariant value + var refs = reference.GetReferences(val); + trackedRelations.AddRange(refs); - // WB: Loop over our collection of references registered and add references - foreach (var item in _dataValueReferences) + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in _dataValueReferenceFors) { // Check if this value reference is for this datatype/editor // Then call it's GetReferences method - to see if the value stored // in the dataeditor/property has referecnes to media items if (item.IsForEditor(editor)) - trackedRelations.AddRange(item.GetReferences(val)); + trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index 5c2e73de9d..e3fba3db01 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -20,8 +20,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) + Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index e1ea955972..8b63b93f16 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -46,8 +46,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) + Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 2297bbed2c..da124db77f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -29,8 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferences) + Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index c20e990a2b..06d93dfe50 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -28,8 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceCollection dataValueReferences) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences) + Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs deleted file mode 100644 index 6d0e3e62df..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceCollection : BuilderCollectionBase - { - public DataValueReferenceCollection(IEnumerable items) - : base(items) - { } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs deleted file mode 100644 index c6442275b0..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceCollectionBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceCollectionBuilder : OrderedCollectionBuilderBase - { - protected override DataValueReferenceCollectionBuilder This => this; - } -} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs new file mode 100644 index 0000000000..c5b9470868 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceForCollection : BuilderCollectionBase + { + public DataValueReferenceForCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs new file mode 100644 index 0000000000..a7b03ea482 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceForCollectionBuilder : OrderedCollectionBuilderBase + { + protected override DataValueReferenceForCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs index 7d46704938..6377098bfc 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReference.cs @@ -13,13 +13,6 @@ namespace Umbraco.Core.PropertyEditors /// /// /// - IEnumerable GetReferences(object value); - - /// - /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). - /// - /// The datatype. - /// A value indicating whether the converter supports a datatype. - bool IsForEditor(IDataEditor dataEditor); + IEnumerable GetReferences(object value); } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs new file mode 100644 index 0000000000..e0d5e4bad1 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.PropertyEditors +{ + public interface IDataValueReferenceFor + { + /// + /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). + /// + /// The datatype. + /// A value indicating whether the converter supports a datatype. + bool IsForEditor(IDataEditor dataEditor); + + /// + /// + /// + /// + IDataValueReference GetDataValueReference(); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c6545a4f8b..98871fbf5c 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -281,9 +281,10 @@ - - + + + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 141331d4f4..9b99aa9b4c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 76d52fb844..42d2d13c6e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -73,7 +73,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 99adcd63e3..6d78601bba 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -31,7 +31,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index c7503671b7..886d0c5ecf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -46,7 +46,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index b9d034bd12..d3150623c2 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index b69adcbb67..89f7c39abc 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -315,7 +315,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index 7f356897af..a8a263043d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -964,7 +964,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -981,7 +981,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 14aa9d8cf2..7caf0ef934 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -246,7 +246,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(ScopeProvider); var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index fead39b6cb..b632699bfd 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -35,7 +35,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -58,7 +58,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index d0f4ff95b3..dd561cb3a8 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -52,7 +52,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index cb097af2ef..70c51c5baa 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3211,7 +3211,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 1363d60b8a..2dd82d9a4a 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -182,7 +182,7 @@ namespace Umbraco.Web.Composing public static DataEditorCollection DataEditors => CoreCurrent.DataEditors; - public static DataValueReferenceCollection DataValueReferences => CoreCurrent.DataValueReferences; + public static DataValueReferenceForCollection DataValueReferenceFors => CoreCurrent.DataValueReferenceFors; public static PropertyEditorCollection PropertyEditors => CoreCurrent.PropertyEditors; diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 2f45d98fa1..683f1a05c3 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -29,11 +29,21 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new ContentPickerPropertyValueEditor(Attribute); - internal class ContentPickerPropertyValueEditor : DataValueEditor + internal class ContentPickerPropertyValueEditor : DataValueEditor, IDataValueReference { public ContentPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } + + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 16dffb3b10..da4264be28 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); - internal class GridPropertyValueEditor : DataValueEditor + internal class GridPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -156,6 +156,25 @@ namespace Umbraco.Web.PropertyEditors return grid; } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); + + foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => + _richTextPropertyValueEditor.GetReferences(x.Value))) + yield return umbracoEntityReference; + + foreach (var umbracoEntityReference in mediaValues.SelectMany(x => + _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + yield return umbracoEntityReference; + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 6416fa3342..ece210b9d1 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -33,11 +33,21 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MediaPickerPropertyValueEditor(Attribute); - internal class MediaPickerPropertyValueEditor : DataValueEditor + internal class MediaPickerPropertyValueEditor : DataValueEditor, IDataValueReference { public MediaPickerPropertyValueEditor(DataEditorAttribute attribute) : base(attribute) { } + + public IEnumerable GetReferences(object value) + { + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index d7a7a2ed59..fd7f735e68 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -23,12 +23,22 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new MultiNodeTreePickerPropertyValueEditor(Attribute); - public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor + public class MultiNodeTreePickerPropertyValueEditor : DataValueEditor, IDataValueReference { public MultiNodeTreePickerPropertyValueEditor(DataEditorAttribute attribute): base(attribute) { } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var udiPaths = asString.Split(','); + foreach (var udiPath in udiPaths) + if (Udi.TryParse(udiPath, out var udi)) + yield return new UmbracoEntityReference(udi); + } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index 95ac809576..e77ce3a993 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -4,6 +4,9 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; +using System.Collections.Generic; +using Umbraco.Core.Models.Editors; +using Newtonsoft.Json; namespace Umbraco.Web.PropertyEditors { @@ -15,7 +18,7 @@ namespace Umbraco.Web.PropertyEditors ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Pickers, Icon = "icon-link")] - public class MultiUrlPickerPropertyEditor : DataEditor + public class MultiUrlPickerPropertyEditor : DataEditor, IDataValueReference { private readonly IEntityService _entityService; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; @@ -25,6 +28,18 @@ namespace Umbraco.Web.PropertyEditors _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } + + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + if (link.Udi != null) // Links can be absolute links without a Udi + yield return new UmbracoEntityReference(link.Udi); + } protected override IConfigurationEditor CreateConfigurationEditor() => new MultiUrlPickerConfigurationEditor(); diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 865b583624..f3e391aeab 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService); - internal class NestedContentPropertyValueEditor : DataValueEditor + internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; @@ -228,6 +228,31 @@ namespace Umbraco.Web.PropertyEditors return JsonConvert.SerializeObject(deserialized); } #endregion + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) + { + if (row.PropType == null) continue; + + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = row.JsonRowValue[row.PropKey]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + + return result; + } } internal class NestedContentValidator : IValueValidator diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 96b1d87205..427e36b37a 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.PropertyEditors /// /// A custom value editor to ensure that macro syntax is parsed when being persisted and formatted correctly for display in the editor /// - internal class RichTextPropertyValueEditor : DataValueEditor + internal class RichTextPropertyValueEditor : DataValueEditor, IDataValueReference { private IUmbracoContextAccessor _umbracoContextAccessor; private readonly HtmlImageSourceParser _imageSourceParser; @@ -130,6 +130,24 @@ namespace Umbraco.Web.PropertyEditors return parsed; } + + /// + /// Resolve references from values + /// + /// + /// + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) + yield return new UmbracoEntityReference(udi); + + foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) + yield return new UmbracoEntityReference(udi); + + //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs + } } internal class RichTextPropertyIndexValueFactory : IPropertyIndexValueFactory diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs deleted file mode 100644 index 08405b6d66..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/ContentPickerPropertyValueReferences.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class ContentPickerPropertyValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.ContentPicker); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs deleted file mode 100644 index 85acff7ca3..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/GridPropertyValueReferences.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class GridPropertyValueReferences : IDataValueReference - { - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - return Enumerable.Empty(); - - //var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - ////TODO FIX SQUIGLES - //GridPropertyEditor.GridPropertyValueEditor.DeserializeGridValue(rawJson, out var richTextEditorValues, out var mediaValues); - - //foreach (var umbracoEntityReference in richTextEditorValues.SelectMany(x => - // _richTextPropertyValueEditor.GetReferences(x.Value))) - // yield return umbracoEntityReference; - - //foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - // _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) - // yield return umbracoEntityReference; - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs deleted file mode 100644 index ecd46b7ea5..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/MediaPickerPropertyValueReferences.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class MediaPickerPropertyValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value is string str ? str : value?.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - if (Udi.TryParse(asString, out var udi)) - yield return new UmbracoEntityReference(udi); - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MediaPicker); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs deleted file mode 100644 index 4413f75080..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiNodeTreePickerPropertyValueReferences.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class MultiNodeTreePickerPropertyValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var udiPaths = asString.Split(','); - foreach (var udiPath in udiPaths) - if (Udi.TryParse(udiPath, out var udi)) - yield return new UmbracoEntityReference(udi); - - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs deleted file mode 100644 index 5d8dfd4f39..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/MultiUrlPickerValueReferences.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class MultiUrlPickerValueReferences : IDataValueReference - { - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - var links = JsonConvert.DeserializeObject>(asString); - foreach (var link in links) - if (link.Udi != null) // Links can be absolute links without a Udi - yield return new UmbracoEntityReference(link.Udi); - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.MultiUrlPicker); - - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs deleted file mode 100644 index a872dde1c7..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/NestedContentPropertyValueReferences.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using static Umbraco.Web.PropertyEditors.NestedContentPropertyEditor; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class NestedContentPropertyValueReferences : IDataValueReference - { - private PropertyEditorCollection _propertyEditors; - private NestedContentValues _nestedContentValues; - - //ARGH LightInject moaning at me - public NestedContentPropertyValueReferences(PropertyEditorCollection propertyEditors, IContentTypeService contentTypeService) - { - _propertyEditors = propertyEditors; - _nestedContentValues = new NestedContentValues(contentTypeService); - } - - public IEnumerable GetReferences(object value) - { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - - foreach (var row in _nestedContentValues.GetPropertyValues(rawJson, out _)) - { - if (row.PropType == null) continue; - - var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; - - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - var val = row.JsonRowValue[row.PropKey]?.ToString(); - - var refs = reference.GetReferences(val); - - result.AddRange(refs); - } - - return result; - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs b/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs deleted file mode 100644 index 744b0dc27c..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueReferences/RichTextPropertyValueReferences.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Models.Editors; -using Umbraco.Core.PropertyEditors; -using Umbraco.Web.Templates; - -namespace Umbraco.Web.PropertyEditors.ValueReferences -{ - public class RichTextPropertyValueReferences : IDataValueReference - { - private HtmlImageSourceParser _imageSourceParser; - private HtmlLocalLinkParser _localLinkParser; - - public RichTextPropertyValueReferences(HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser) - { - _imageSourceParser = imageSourceParser; - _localLinkParser = localLinkParser; - } - - /// - /// Resolve references from values - /// - /// - /// - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - foreach (var udi in _imageSourceParser.FindUdisFromDataAttributes(asString)) - yield return new UmbracoEntityReference(udi); - - foreach (var udi in _localLinkParser.FindUdisFromLocalLinks(asString)) - yield return new UmbracoEntityReference(udi); - - //TODO: Detect Macros too ... but we can save that for a later date, right now need to do media refs - } - - public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias.InvariantEquals(Constants.PropertyEditors.Aliases.TinyMce); - } -} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 295a31c461..c1eea60517 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -9,9 +9,7 @@ using Umbraco.Core.Dashboards; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Migrations.PostMigrations; -using Umbraco.Web.Migrations.PostMigrations; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Runtime; using Umbraco.Core.Services; @@ -19,7 +17,6 @@ using Umbraco.Web.Actions; using Umbraco.Web.Cache; using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.ContentApps; -using Umbraco.Web.Dashboards; using Umbraco.Web.Dictionary; using Umbraco.Web.Editors; using Umbraco.Web.Features; @@ -37,13 +34,10 @@ using Umbraco.Web.Security.Providers; using Umbraco.Web.Services; using Umbraco.Web.SignalR; using Umbraco.Web.Templates; -using Umbraco.Web.Tour; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; -using static Umbraco.Web.PropertyEditors.GridPropertyEditor; -using Umbraco.Web.PropertyEditors.ValueReferences; namespace Umbraco.Web.Runtime { @@ -279,19 +273,8 @@ namespace Umbraco.Web.Runtime // Used to determine if a datatype/editor should be storing/tracking // references to media item/s - composition.DataValueReferences() - .Append() - - // TODO: Unsure how to call other ValueReference in collection - // When looking at grid item cells for RTE or media found - //.Append() - .Append() - .Append() - .Append() - - // TODO: LightInject problem - //.Append() - .Append(); + composition.DataValueReferenceFors(); + //WB:TODO Try me out // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 22680898af..5eca5ad7fe 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -235,13 +235,6 @@ - - - - - - - From 958eb822137df9752a0ff49ed46a74310b66ee53 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:23:56 +0000 Subject: [PATCH 127/610] Move the GetReferences about (back to where it was) --- .../MultiUrlPickerPropertyEditor.cs | 14 +------------- .../PropertyEditors/MultiUrlPickerValueEditor.cs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index e77ce3a993..8af2d98018 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.PropertyEditors ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Pickers, Icon = "icon-link")] - public class MultiUrlPickerPropertyEditor : DataEditor, IDataValueReference + public class MultiUrlPickerPropertyEditor : DataEditor { private readonly IEntityService _entityService; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; @@ -29,18 +29,6 @@ namespace Umbraco.Web.PropertyEditors _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - var links = JsonConvert.DeserializeObject>(asString); - foreach (var link in links) - if (link.Udi != null) // Links can be absolute links without a Udi - yield return new UmbracoEntityReference(link.Udi); - } - protected override IConfigurationEditor CreateConfigurationEditor() => new MultiUrlPickerConfigurationEditor(); protected override IDataValueEditor CreateValueEditor() => new MultiUrlPickerValueEditor(_entityService, _publishedSnapshotAccessor, Logger, Attribute); diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index de641f69a3..ff2ac28986 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -15,7 +15,7 @@ using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors { - public class MultiUrlPickerValueEditor : DataValueEditor + public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference { private readonly IEntityService _entityService; private readonly ILogger _logger; @@ -156,6 +156,18 @@ namespace Umbraco.Web.PropertyEditors return base.FromEditor(editorValue, currentValue); } + public IEnumerable GetReferences(object value) + { + var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + var links = JsonConvert.DeserializeObject>(asString); + foreach (var link in links) + if (link.Udi != null) // Links can be absolute links without a Udi + yield return new UmbracoEntityReference(link.Udi); + } + [DataContract] internal class LinkDto { From 1b84051e8f19bc0135a2d74f8c12ebee534114aa Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:24:50 +0000 Subject: [PATCH 128/610] Move DataValueReferceFors collection from Umbraco.Web to Umbraco.Core composer in order to see if that helps the Unit Test LightInject issue --- src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 4 ++++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 6 +----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index 94d2b68121..be8a920988 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -75,6 +75,10 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); + // Used to determine if a datatype/editor should be storing/tracking + // references to media item/s + composition.DataValueReferenceFors(); + //WB:TODO Try me out & overide/add to an existing list // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index c1eea60517..28f365fc60 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -177,7 +177,7 @@ namespace Umbraco.Web.Runtime .Remove() .Remove() .Remove(); - + // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax composition.FilteredControllerFactory() @@ -271,10 +271,6 @@ namespace Umbraco.Web.Runtime .Append() .Append(); - // Used to determine if a datatype/editor should be storing/tracking - // references to media item/s - composition.DataValueReferenceFors(); - //WB:TODO Try me out // replace with web implementation composition.RegisterUnique(); From e1e5ac44cc8d2e147d971da7176b514dd70bd38f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 2 Dec 2019 15:35:13 +0000 Subject: [PATCH 129/610] Something gone funky/awol with my commits - remove dupe code :S --- .../PropertyEditors/MultiUrlPickerValueEditor.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index ff2ac28986..853e995ed8 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -156,18 +156,6 @@ namespace Umbraco.Web.PropertyEditors return base.FromEditor(editorValue, currentValue); } - public IEnumerable GetReferences(object value) - { - var asString = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (string.IsNullOrEmpty(asString)) yield break; - - var links = JsonConvert.DeserializeObject>(asString); - foreach (var link in links) - if (link.Udi != null) // Links can be absolute links without a Udi - yield return new UmbracoEntityReference(link.Udi); - } - [DataContract] internal class LinkDto { From 19497ee7857d544552d57068d4431328f882d4f8 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 3 Dec 2019 11:52:46 +0000 Subject: [PATCH 130/610] Fix up unit test - needed to add the collection to the Compositon for all tests inheriting from UmbracoTestBase --- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index ffc49b47ac..a444e4b81d 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -217,6 +217,9 @@ namespace Umbraco.Tests.Testing Composition.RegisterUnique(_ => Umbraco.Web.Composing.Current.UmbracoContextAccessor); Composition.RegisterUnique(); Composition.WithCollectionBuilder(); + + Composition.DataValueReferenceFors(); + Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); From 9e1a56eba59a2650905e955fbbb5f9d976ebacf6 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 4 Dec 2019 16:14:33 +0000 Subject: [PATCH 131/610] Rename from DataValueReferenceFor to DataValyeReferenceFactory & Factories for the plural collection --- src/Umbraco.Core/Composing/Current.cs | 4 ++-- src/Umbraco.Core/CompositionExtensions.cs | 6 +++--- .../Repositories/Implement/ContentRepositoryBase.cs | 8 ++++---- .../Implement/DocumentBlueprintRepository.cs | 4 ++-- .../Repositories/Implement/DocumentRepository.cs | 4 ++-- .../Repositories/Implement/MediaRepository.cs | 4 ++-- .../Repositories/Implement/MemberRepository.cs | 4 ++-- .../DataValueReferenceFactoryCollection.cs | 12 ++++++++++++ .../DataValueReferenceFactoryCollectionBuilder.cs | 9 +++++++++ .../DataValueReferenceForCollection.cs | 12 ------------ .../DataValueReferenceForCollectionBuilder.cs | 9 --------- ...ReferenceFor.cs => IDataValueReferenceFactory.cs} | 2 +- src/Umbraco.Core/Runtime/CoreInitialComposer.cs | 3 +-- src/Umbraco.Core/Umbraco.Core.csproj | 6 +++--- .../Repositories/ContentTypeRepositoryTest.cs | 2 +- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Persistence/Repositories/DomainRepositoryTest.cs | 2 +- .../Persistence/Repositories/MediaRepositoryTest.cs | 2 +- .../Persistence/Repositories/MemberRepositoryTest.cs | 2 +- .../Repositories/PublicAccessRepositoryTest.cs | 2 +- .../Persistence/Repositories/TagRepositoryTest.cs | 4 ++-- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Persistence/Repositories/UserRepositoryTest.cs | 4 ++-- .../Services/ContentServicePerformanceTest.cs | 2 +- src/Umbraco.Tests/Services/ContentServiceTests.cs | 2 +- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 +- src/Umbraco.Web/Composing/Current.cs | 2 +- 27 files changed, 58 insertions(+), 59 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs create mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs rename src/Umbraco.Core/PropertyEditors/{IDataValueReferenceFor.cs => IDataValueReferenceFactory.cs} (92%) diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index 899c465ca7..846331a16e 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -154,8 +154,8 @@ namespace Umbraco.Core.Composing public static DataEditorCollection DataEditors => Factory.GetInstance(); - public static DataValueReferenceForCollection DataValueReferenceFors - => Factory.GetInstance(); + public static DataValueReferenceFactoryCollection DataValueReferenceFactories + => Factory.GetInstance(); public static PropertyEditorCollection PropertyEditors => Factory.GetInstance(); diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index aee4b41be8..ced9a9386a 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -50,11 +50,11 @@ namespace Umbraco.Core => composition.WithCollectionBuilder(); /// - /// Gets the data value reference collection builder. + /// Gets the data value reference factory collection builder. /// /// The composition. - public static DataValueReferenceForCollectionBuilder DataValueReferenceFors(this Composition composition) - => composition.WithCollectionBuilder(); + public static DataValueReferenceFactoryCollectionBuilder DataValueReferenceFactories(this Composition composition) + => composition.WithCollectionBuilder(); /// /// Gets the property value converters collection builder. diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 77d36b8d43..9bd1520b51 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement where TRepository : class, IRepository { private readonly Lazy _propertyEditors; - private readonly DataValueReferenceForCollection _dataValueReferenceFors; + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; /// /// @@ -50,14 +50,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// protected ContentRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) + Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories) : base(scopeAccessor, cache, logger) { LanguageRepository = languageRepository; RelationRepository = relationRepository; RelationTypeRepository = relationTypeRepository; _propertyEditors = propertyEditors; - _dataValueReferenceFors = dataValueReferenceFors; + _dataValueReferenceFactories = dataValueReferenceFactories; } protected abstract TRepository This { get; } @@ -843,7 +843,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // implementation of GetReferences in IDataValueReference. // Allows developers to add support for references by a // package /property editor that did not implement IDataValueReference themselves - foreach (var item in _dataValueReferenceFors) + foreach (var item in _dataValueReferenceFactories) { // Check if this value reference is for this datatype/editor // Then call it's GetReferences method - to see if the value stored diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index e3fba3db01..e150b2e632 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -20,8 +20,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public DocumentBlueprintRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) + Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, appCaches, logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 8b63b93f16..35e54a39e0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -46,8 +46,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// public DocumentRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) + Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories) { _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index da124db77f..ad47c7af6b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -29,8 +29,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditorCollection, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFors) + Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories) { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 06d93dfe50..42e7d1c32f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -28,8 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public MemberRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, ILanguageRepository languageRepository, IRelationRepository relationRepository, IRelationTypeRepository relationTypeRepository, - Lazy propertyEditors, DataValueReferenceForCollection dataValueReferenceFors) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFors) + Lazy propertyEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories) { _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs new file mode 100644 index 0000000000..6b6607e1f9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceFactoryCollection : BuilderCollectionBase + { + public DataValueReferenceFactoryCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs new file mode 100644 index 0000000000..f12071c492 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.PropertyEditors +{ + public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase + { + protected override DataValueReferenceFactoryCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs deleted file mode 100644 index c5b9470868..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceForCollection : BuilderCollectionBase - { - public DataValueReferenceForCollection(IEnumerable items) - : base(items) - { } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs deleted file mode 100644 index a7b03ea482..0000000000 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceForCollectionBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Core.PropertyEditors -{ - public class DataValueReferenceForCollectionBuilder : OrderedCollectionBuilderBase - { - protected override DataValueReferenceForCollectionBuilder This => this; - } -} diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs similarity index 92% rename from src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs rename to src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs index e0d5e4bad1..6587e071bf 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueReferenceFactory.cs @@ -1,6 +1,6 @@ namespace Umbraco.Core.PropertyEditors { - public interface IDataValueReferenceFor + public interface IDataValueReferenceFactory { /// /// Gets a value indicating whether the DataValueReference lookup supports a datatype (data editor). diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index be8a920988..d95ada499b 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -77,8 +77,7 @@ namespace Umbraco.Core.Runtime // Used to determine if a datatype/editor should be storing/tracking // references to media item/s - composition.DataValueReferenceFors(); - //WB:TODO Try me out & overide/add to an existing list + composition.DataValueReferenceFactories(); // register a server registrar, by default it's the db registrar composition.RegisterUnique(f => diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 98871fbf5c..306b764787 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -281,10 +281,10 @@ - - + + - + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 9b99aa9b4c..f7f3adf0a0 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 42d2d13c6e..291525ba13 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -73,7 +73,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index 6d78601bba..56138faea9 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -31,7 +31,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 886d0c5ecf..ef436a3489 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -46,7 +46,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(scopeAccessor); var relationRepository = new RelationRepository(scopeAccessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index d3150623c2..060478d64b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 89f7c39abc..4cf440c369 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -315,7 +315,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index a8a263043d..2c15e91e61 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -964,7 +964,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -981,7 +981,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 7caf0ef934..200447e30a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -246,7 +246,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(ScopeProvider); var relationRepository = new RelationRepository(ScopeProvider, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var contentRepo = new DocumentRepository(ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index b632699bfd..3ba00e54cf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -35,7 +35,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of(), relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } @@ -58,7 +58,7 @@ namespace Umbraco.Tests.Persistence.Repositories var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index dd561cb3a8..9f4304ebee 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -52,7 +52,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 70c51c5baa..92d2a68472 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3211,7 +3211,7 @@ namespace Umbraco.Tests.Services var entityRepository = new EntityRepository(accessor); var relationRepository = new RelationRepository(accessor, Logger, relationTypeRepository, entityRepository); var propertyEditors = new Lazy(() => new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty()))); - var dataValueReferences = new DataValueReferenceForCollection(Enumerable.Empty()); + var dataValueReferences = new DataValueReferenceFactoryCollection(Enumerable.Empty()); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferences); return repository; } diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index a444e4b81d..4f7de2d230 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -218,7 +218,7 @@ namespace Umbraco.Tests.Testing Composition.RegisterUnique(); Composition.WithCollectionBuilder(); - Composition.DataValueReferenceFors(); + Composition.DataValueReferenceFactories(); Composition.RegisterUnique(); Composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 2dd82d9a4a..ec65046e84 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -182,7 +182,7 @@ namespace Umbraco.Web.Composing public static DataEditorCollection DataEditors => CoreCurrent.DataEditors; - public static DataValueReferenceForCollection DataValueReferenceFors => CoreCurrent.DataValueReferenceFors; + public static DataValueReferenceFactoryCollection DataValueReferenceFactories => CoreCurrent.DataValueReferenceFactories; public static PropertyEditorCollection PropertyEditors => CoreCurrent.PropertyEditors; From cad384a0a6b5e1c8e0bb967d3560663ddc0f43c9 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 5 Dec 2019 11:18:18 +0000 Subject: [PATCH 132/610] Moves logic out from ContentRepositoryBase & into a method on the Collection itself --- .../Implement/ContentRepositoryBase.cs | 31 ++------------ .../DataValueReferenceFactoryCollection.cs | 40 +++++++++++++++++++ ...aValueReferenceFactoryCollectionBuilder.cs | 2 +- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 9bd1520b51..13b687eb4e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -823,35 +823,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistRelations(TEntity entity) { + // Get all references from our core built in DataEditors/Property Editors + // Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection var trackedRelations = new List(); - - foreach (var p in entity.Properties) - { - if (!PropertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - var valueEditor = editor.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here - if (!p.PropertyType.VariesByNothing()) continue; - - var val = p.GetValue(); // get the invariant value - var refs = reference.GetReferences(val); - trackedRelations.AddRange(refs); - - - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (var item in _dataValueReferenceFactories) - { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media items - if (item.IsForEditor(editor)) - trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); - } - } + trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors)); //First delete all auto-relations for this entity RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 6b6607e1f9..c1dbfbaca4 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; namespace Umbraco.Core.PropertyEditors { @@ -8,5 +10,43 @@ namespace Umbraco.Core.PropertyEditors public DataValueReferenceFactoryCollection(IEnumerable items) : base(items) { } + + public IEnumerable GetAllReferences(PropertyCollection properties, PropertyEditorCollection propertyEditors) + { + var trackedRelations = new List(); + + foreach (var p in properties) + { + // Injecting propertyEditorCollection is causing LightInject to throw a Recursive Dependancy error + // Hence using Current.PropertyEditors + if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; + + //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here + if (!p.PropertyType.VariesByNothing()) continue; + var val = p.GetValue(); // get the invariant value + + var valueEditor = editor.GetValueEditor(); + if (valueEditor is IDataValueReference reference) + { + var refs = reference.GetReferences(val); + trackedRelations.AddRange(refs); + } + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) + trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); + } + } + + return trackedRelations; + } } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs index f12071c492..2cf76712c8 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionBuilder.cs @@ -4,6 +4,6 @@ namespace Umbraco.Core.PropertyEditors { public class DataValueReferenceFactoryCollectionBuilder : OrderedCollectionBuilderBase { - protected override DataValueReferenceFactoryCollectionBuilder This => this; + protected override DataValueReferenceFactoryCollectionBuilder This => this; } } From 4d78a2c848d7399ddaa13257a7d13526205f5b31 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 5 Dec 2019 12:48:41 +0000 Subject: [PATCH 133/610] Remove old & unecessary comment --- .../PropertyEditors/DataValueReferenceFactoryCollection.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index c1dbfbaca4..386ab6a8f3 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -17,8 +17,6 @@ namespace Umbraco.Core.PropertyEditors foreach (var p in properties) { - // Injecting propertyEditorCollection is causing LightInject to throw a Recursive Dependancy error - // Hence using Current.PropertyEditors if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here From 1513a1254902dd755a2876321b8d50bbcf5ced48 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:33:59 +0100 Subject: [PATCH 134/610] Introduce a new IMainDomLock and both default and sql implementations --- .../Migrations/Install/DatabaseDataCreator.cs | 2 + .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_8_6_0/AddMainDomLock.cs | 16 ++ ...AddPropertyTypeValidationMessageColumns.cs | 1 + .../Persistence/Constants-Locks.cs | 5 + .../SqlSyntax/SqlServerSyntaxProvider.cs | 7 +- src/Umbraco.Core/Runtime/CoreRuntime.cs | 2 +- src/Umbraco.Core/{ => Runtime}/IMainDom.cs | 1 + src/Umbraco.Core/Runtime/IMainDomLock.cs | 30 +++ src/Umbraco.Core/{ => Runtime}/MainDom.cs | 97 ++++------ .../Runtime/MainDomSemaphoreLock.cs | 92 +++++++++ src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 178 ++++++++++++++++++ src/Umbraco.Core/Scoping/IScopeProvider.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 8 +- .../XmlPublishedSnapshotService.cs | 1 + .../LegacyXmlPublishedCache/XmlStore.cs | 1 + 16 files changed, 375 insertions(+), 69 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs rename src/Umbraco.Core/{ => Runtime}/IMainDom.cs (96%) create mode 100644 src/Umbraco.Core/Runtime/IMainDomLock.cs rename src/Umbraco.Core/{ => Runtime}/MainDom.cs (77%) create mode 100644 src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs create mode 100644 src/Umbraco.Core/Runtime/SqlMainDomLock.cs diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 94d8cfbc62..f6daf180b7 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -151,6 +151,8 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" }); _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" }); + + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); } private void CreateContentTypeData() diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 223603be14..6164f828f0 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -185,6 +185,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.6.0 To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); + To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs new file mode 100644 index 0000000000..6ca493ac7e --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs @@ -0,0 +1,16 @@ +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 +{ + public class AddMainDomLock : MigrationBase + { + public AddMainDomLock(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs index 30eb30109e..f44695da69 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { + public class AddPropertyTypeValidationMessageColumns : MigrationBase { public AddPropertyTypeValidationMessageColumns(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 1dcd2408e7..e64f40ced7 100644 --- a/src/Umbraco.Core/Persistence/Constants-Locks.cs +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -8,6 +8,11 @@ namespace Umbraco.Core /// public static class Locks { + /// + /// The lock + /// + public const int MainDom = -1000; + /// /// All servers. /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 3d0adf175e..6dda49cd5e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -250,6 +250,11 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) } public override void WriteLock(IDatabase db, params int[] lockIds) + { + WriteLock(db, 1800, lockIds); + } + + public void WriteLock(IDatabase db, int millisecondsTimeout, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -260,7 +265,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - db.Execute(@"SET LOCK_TIMEOUT 1800;"); + db.Execute($"SET LOCK_TIMEOUT {millisecondsTimeout};"); var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); if (i == 0) // ensure we are actually locking! throw new ArgumentException($"LockObject with id={lockId} does not exist."); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 50653edc7c..49d7658647 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -147,7 +147,7 @@ namespace Umbraco.Core.Runtime // TODO: remove this in netcore, this is purely backwards compat hacks with the empty ctor if (MainDom == null) { - MainDom = new MainDom(Logger); + MainDom = new MainDom(Logger, new MainDomSemaphoreLock()); } diff --git a/src/Umbraco.Core/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs similarity index 96% rename from src/Umbraco.Core/IMainDom.cs rename to src/Umbraco.Core/Runtime/IMainDom.cs index 31b2e2eee0..444fc1c7d0 100644 --- a/src/Umbraco.Core/IMainDom.cs +++ b/src/Umbraco.Core/Runtime/IMainDom.cs @@ -1,5 +1,6 @@ using System; +// TODO: Can't change namespace due to breaking changes, change in netcore namespace Umbraco.Core { /// diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs new file mode 100644 index 0000000000..c32e990114 --- /dev/null +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; + +namespace Umbraco.Core.Runtime +{ + /// + /// An application-wide distributed lock + /// + /// + /// Disposing releases the lock + /// + public interface IMainDomLock : IDisposable + { + /// + /// Acquires an application-wide distributed lock + /// + /// + /// + /// A disposable object which will be disposed in order to release the lock + /// + /// Throws a if the elapsed millsecondsTimeout value is exceeded + Task AcquireLockAsync(int millisecondsTimeout); + + /// + /// Wait on a background thread to receive a signal from another AppDomain + /// + /// + Task ListenAsync(); + } +} diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs similarity index 77% rename from src/Umbraco.Core/MainDom.cs rename to src/Umbraco.Core/Runtime/MainDom.cs index e2049c0190..406fb9b731 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -4,10 +4,13 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Web.Hosting; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; -namespace Umbraco.Core +namespace Umbraco.Core.Runtime { + /// /// Provides the full implementation of . /// @@ -20,18 +23,11 @@ namespace Umbraco.Core #region Vars private readonly ILogger _logger; + private readonly IMainDomLock _mainDomLock; // our own lock for local consistency private object _locko = new object(); - // async lock representing the main domain lock - private readonly SystemLock _systemLock; - private IDisposable _systemLocker; - - // event wait handle used to notify current main domain that it should - // release the lock because a new domain wants to be the main domain - private readonly EventWaitHandle _signal; - private bool _isInitialized; // indicates whether... private bool _isMainDom; // we are the main domain @@ -47,32 +43,12 @@ namespace Umbraco.Core #region Ctor // initializes a new instance of MainDom - public MainDom(ILogger logger) + public MainDom(ILogger logger, IMainDomLock systemLock) { HostingEnvironment.RegisterObject(this); _logger = logger; - - // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail - var appId = HostingEnvironment.ApplicationID?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty; - - // combining with the physical path because if running on eg IIS Express, - // two sites could have the same appId even though they are different. - // - // now what could still collide is... two sites, running in two different processes - // and having the same appId, and running on the same app physical path - // - // we *cannot* use the process ID here because when an AppPool restarts it is - // a new process for the same application path - - var appPath = HostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty; - var hash = (appId + ":::" + appPath).GenerateHash(); - - var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK"; - _systemLock = new SystemLock(lockName); - - var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT"; - _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _mainDomLock = systemLock; } #endregion @@ -130,7 +106,6 @@ namespace Umbraco.Core { _logger.Info("Stopping ({SignalSource})", source); foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) - { try { callback(); // no timeout on callbacks @@ -140,14 +115,13 @@ namespace Umbraco.Core _logger.Error(e, "Error while running callback"); continue; } - } _logger.Debug("Stopped ({SignalSource})", source); } finally { // in any case... _isMainDom = false; - _systemLocker?.Dispose(); + _mainDomLock.Dispose(); _logger.Info("Released ({SignalSource})", source); } @@ -167,35 +141,11 @@ namespace Umbraco.Core _logger.Info("Acquiring."); - // signal other instances that we want the lock, then wait one the lock, - // which may timeout, and this is accepted - see comments below + // Get the lock + _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); - // signal, then wait for the lock, then make sure the event is - // reset (maybe there was noone listening..) - _signal.Set(); - - // if more than 1 instance reach that point, one will get the lock - // and the other one will timeout, which is accepted - - //This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. - try - { - _systemLocker = _systemLock.Lock(LockTimeoutMilliseconds); - } - finally - { - // we need to reset the event, because otherwise we would end up - // signaling ourselves and committing suicide immediately. - // only 1 instance can reach that point, but other instances may - // have started and be trying to get the lock - they will timeout, - // which is accepted - - _signal.Reset(); - } - - //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread - - _signal.WaitOneAsync() + // Listen for the signal from another AppDomain coming online to release the lock + _mainDomLock.ListenAsync() .ContinueWith(_ => OnSignal("signal")); _logger.Info("Acquired."); @@ -230,8 +180,7 @@ namespace Umbraco.Core { if (disposing) { - _signal?.Close(); - _signal?.Dispose(); + _mainDomLock.Dispose(); } disposedValue = true; @@ -244,5 +193,25 @@ namespace Umbraco.Core } #endregion + + public static string GetMainDomId() + { + // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail + var appId = HostingEnvironment.ApplicationID?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty; + + // combining with the physical path because if running on eg IIS Express, + // two sites could have the same appId even though they are different. + // + // now what could still collide is... two sites, running in two different processes + // and having the same appId, and running on the same app physical path + // + // we *cannot* use the process ID here because when an AppPool restarts it is + // a new process for the same application path + + var appPath = HostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty; + var hash = (appId + ":::" + appPath).GenerateHash(); + + return hash; + } } } diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs new file mode 100644 index 0000000000..89eef8a658 --- /dev/null +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Runtime +{ + /// + /// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain + /// + internal class MainDomSemaphoreLock : IMainDomLock + { + private readonly SystemLock _systemLock; + + // event wait handle used to notify current main domain that it should + // release the lock because a new domain wants to be the main domain + private readonly EventWaitHandle _signal; + + private IDisposable _lockRelease; + + public MainDomSemaphoreLock() + { + var lockName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-LCK"; + _systemLock = new SystemLock(lockName); + + var eventName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + } + + //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread + public Task ListenAsync() => _signal.WaitOneAsync(); + + public Task AcquireLockAsync(int millisecondsTimeout) + { + // signal other instances that we want the lock, then wait on the lock, + // which may timeout, and this is accepted - see comments below + + // signal, then wait for the lock, then make sure the event is + // reset (maybe there was noone listening..) + _signal.Set(); + + // if more than 1 instance reach that point, one will get the lock + // and the other one will timeout, which is accepted + + //This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset. + try + { + _lockRelease = _systemLock.Lock(millisecondsTimeout); + return Task.FromResult(true); + } + catch (TimeoutException) + { + return Task.FromResult(false); + } + finally + { + // we need to reset the event, because otherwise we would end up + // signaling ourselves and committing suicide immediately. + // only 1 instance can reach that point, but other instances may + // have started and be trying to get the lock - they will timeout, + // which is accepted + + _signal.Reset(); + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _lockRelease?.Dispose(); + _signal.Close(); + _signal.Dispose(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + } +} diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs new file mode 100644 index 0000000000..28449fe889 --- /dev/null +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -0,0 +1,178 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Runtime +{ + internal class SqlMainDomLock : IMainDomLock + { + private string _appDomainId; + private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private readonly ILogger _logger; + private IUmbracoDatabase _db; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + + public SqlMainDomLock(ILogger logger) + { + _appDomainId = AppDomain.CurrentDomain.Id.ToString(); + _logger = logger; + } + + + + public Task AcquireLockAsync(int millisecondsTimeout) + { + var factory = new UmbracoDatabaseFactory( + Constants.System.UmbracoConnectionName, + _logger, + new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); + + _db = factory.CreateDatabase(); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + // wait to get a write lock + _sqlServerSyntax.WriteLock(_db, millisecondsTimeout, Constants.Locks.MainDom); + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex)) + { + return Task.FromResult(false); + } + + // unexpected + throw; + } + + InsertLockRecord(); + + return Task.FromResult(true); + } + catch(Exception) + { + _db.AbortTransaction(); + + // unexpected + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + + public Task ListenAsync() + { + // Create a long running task (dedicated thread) + // to poll to check if we are still the MainDom registered in the DB + return Task.Factory.StartNew(() => + { + while(true) + { + if (_cancellationTokenSource.IsCancellationRequested) + break; + + // poll every 1 second + Thread.Sleep(1000); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + + if (!IsStillMainDom()) + { + // we are no longer main dom, exit + return; + } + } + catch (Exception) + { + _db.AbortTransaction(); + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + + + }, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + } + + /// + /// Inserts or updates the key/value row to check if the current appdomain is registerd as the maindom + /// + private void InsertLockRecord() + { + _db.InsertOrUpdate(new KeyValueDto + { + Key = MainDomKey, + Value = _appDomainId, + Updated = DateTime.Now + }); + } + + /// + /// Checks if the DB row value is our current appdomain value + /// + /// + private bool IsStillMainDom() + { + return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", + new { key = MainDomKey, val = _appDomainId }) == 1; + } + + /// + /// Checks if the exception is an SQL timeout + /// + /// + /// + private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _db.Dispose(); + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + + disposedValue = true; + } + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + } + #endregion + + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 6c9eb63ba0..96bb939f8e 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Scoping /// Provides scopes. /// public interface IScopeProvider - { + { /// /// Creates an ambient scope. /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6f9ff1e783..8d278f5630 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,10 @@ --> + + + + @@ -398,7 +402,7 @@ - + @@ -729,7 +733,7 @@ - + diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs index ec6b854a46..97fe9057bb 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Runtime; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Web; diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs index c452c4792a..803b86aec5 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs @@ -14,6 +14,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Runtime; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; From 654511a6564af56e3d50c527cd7b35921ce120cc Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:42:53 +0100 Subject: [PATCH 135/610] adds appSetting to switch maindom lock --- src/Umbraco.Core/Constants-AppSettings.cs | 2 ++ src/Umbraco.Web/UmbracoApplication.cs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 509be46b61..85d6b24ae0 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -9,6 +9,8 @@ namespace Umbraco.Core /// public static class AppSettings { + public const string MainDomLock = "Umbraco.Core.MainDom.Lock"; + // TODO: Kill me - still used in Umbraco.Core.IO.SystemFiles:27 [Obsolete("We need to kill this appsetting as we do not use XML content cache umbraco.config anymore due to NuCache")] public const string ContentXML = "Umbraco.Core.ContentXML"; //umbracoContentXML diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index 94403bc1be..ad8dcb7e8b 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -1,7 +1,9 @@ -using System.Threading; +using System.Configuration; +using System.Threading; using System.Web; using Umbraco.Core; using Umbraco.Core.Logging.Serilog; +using Umbraco.Core.Runtime; using Umbraco.Web.Runtime; namespace Umbraco.Web @@ -14,7 +16,17 @@ namespace Umbraco.Web protected override IRuntime GetRuntime() { var logger = SerilogLogger.CreateWithDefaultConfiguration(); - return new WebRuntime(this, logger, new MainDom(logger)); + + // Determine if we should use the sql main dom or the default + var appSettingMainDomLock = ConfigurationManager.AppSettings[Constants.AppSettings.MainDomLock]; + + var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" + ? (IMainDomLock)new SqlMainDomLock(logger) + : new MainDomSemaphoreLock(); + + var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); + + return runtime; } /// From e919af14d33827de2415f332907cdfc12766ed91 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:50:10 +0100 Subject: [PATCH 136/610] adds comments --- src/Umbraco.Core/Runtime/MainDom.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 406fb9b731..c6ee819cda 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -155,6 +155,10 @@ namespace Umbraco.Core.Runtime /// /// Gets a value indicating whether the current domain is the main domain. /// + /// + /// The lazy initializer call will only call the Acquire callback when it's not been initialized, else it will just return + /// the value from _isMainDom which means when we set _isMainDom to false again after being signaled, this will return false; + /// public bool IsMainDom => LazyInitializer.EnsureInitialized(ref _isMainDom, ref _isInitialized, ref _locko, () => Acquire()); // IRegisteredObject From aae8dbdc15ec0f10efaa98c87384d2c2a069b936 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 15:44:47 +0100 Subject: [PATCH 137/610] Updates to allow the sql main dom lock to be able to wait until the previous maindom has fully unwound before it can be taken over. --- src/Umbraco.Core/Runtime/MainDom.cs | 13 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 204 +++++++++++++++++++-- 2 files changed, 195 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index c6ee819cda..85cf5ad6a9 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -144,9 +144,16 @@ namespace Umbraco.Core.Runtime // Get the lock _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); - // Listen for the signal from another AppDomain coming online to release the lock - _mainDomLock.ListenAsync() - .ContinueWith(_ => OnSignal("signal")); + try + { + // Listen for the signal from another AppDomain coming online to release the lock + _mainDomLock.ListenAsync() + .ContinueWith(_ => OnSignal("signal")); + } + catch (OperationCanceledException) + { + // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception + } _logger.Info("Acquired."); return true; diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 28449fe889..c077572f85 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.SqlClient; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,22 +15,22 @@ namespace Umbraco.Core.Runtime { internal class SqlMainDomLock : IMainDomLock { - private string _appDomainId; + private string _lockId; private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; private readonly ILogger _logger; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + private bool _mainDomChanging = false; public SqlMainDomLock(ILogger logger) { - _appDomainId = AppDomain.CurrentDomain.Id.ToString(); + // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer + _lockId = Guid.NewGuid().ToString(); _logger = logger; } - - - public Task AcquireLockAsync(int millisecondsTimeout) + public async Task AcquireLockAsync(int millisecondsTimeout) { var factory = new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, @@ -38,6 +39,8 @@ namespace Umbraco.Core.Runtime _db = factory.CreateDatabase(); + var tempId = Guid.NewGuid().ToString(); + try { _db.BeginTransaction(IsolationLevel.ReadCommitted); @@ -51,18 +54,29 @@ namespace Umbraco.Core.Runtime { if (IsLockTimeoutException(ex)) { - return Task.FromResult(false); + return false; } // unexpected throw; } - InsertLockRecord(); + var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown + if (result == RecordPersistenceType.Insert) + { + // if we've inserted, then there was no MainDom so we can instantly acquire - return Task.FromResult(true); + // TODO: see the other TODO, could we just delete the row and that would indicate that we + // are MainDom? then we don't leave any orphan rows behind. + + InsertLockRecord(_lockId); // so update with our appdomain id + return true; + } + + // if we've updated, this means there is an active MainDom, now we need to wait to + // for the current MainDom to shutdown which also requires releasing our write lock } - catch(Exception) + catch (Exception) { _db.AbortTransaction(); @@ -73,6 +87,8 @@ namespace Umbraco.Core.Runtime { _db.CompleteTransaction(); } + + return await WaitForExistingAsync(tempId, millisecondsTimeout); } public Task ListenAsync() @@ -96,9 +112,14 @@ namespace Umbraco.Core.Runtime // get a read lock _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); - if (!IsStillMainDom()) + // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that + // we are still the maindom. An empty value might be better because then we won't have any orphan rows + // if the app is terminated. Could that work? + + if (!IsMainDomValue(_lockId)) { - // we are no longer main dom, exit + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; return; } } @@ -119,26 +140,134 @@ namespace Umbraco.Core.Runtime } /// - /// Inserts or updates the key/value row to check if the current appdomain is registerd as the maindom + /// Wait for any existing MainDom to release so we can continue booting /// - private void InsertLockRecord() + /// + /// + /// + private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { - _db.InsertOrUpdate(new KeyValueDto + var updatedTempId = tempId + "_updated"; + + return Task.Run(() => + { + var watch = new Stopwatch(); + watch.Start(); + while(true) + { + // poll very often, we need to take over as fast as we can + Thread.Sleep(100); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + + var mainDomRows = _db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + + if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + { + // the other main dom has updated our record + // Or the other maindom shutdown super fast and just deleted the record + // which indicates that we + // can acquire it and it has shutdown. + + _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId); + + return true; + } + else if (mainDomRows.Count == 1 && mainDomRows[0].Value.EndsWith("_updated")) + { + // in this case, there is a suffixed _updated value but it's not for our ID which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + return false; + } + } + catch (Exception ex) + { + _db.AbortTransaction(); + + if (IsLockTimeoutException(ex)) + { + return false; + } + + throw; + } + finally + { + _db.CompleteTransaction(); + } + + if (watch.ElapsedMilliseconds >= millisecondsTimeout) + { + // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, + // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row + // and it's just been left as an orphan row. + // There's really know way of knowing unless we are constantly updating the row for the current maindom + // which isn't ideal. + // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId); + return true; + } + catch (Exception ex) + { + _db.AbortTransaction(); + + if (IsLockTimeoutException(ex)) + { + // something is wrong, we cannot acquire, not much we can do + return false; + } + + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + } + }, _cancellationTokenSource.Token); + } + + /// + /// Inserts or updates the key/value row + /// + private RecordPersistenceType InsertLockRecord(string id) + { + return _db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, - Value = _appDomainId, + Value = id, Updated = DateTime.Now }); } /// - /// Checks if the DB row value is our current appdomain value + /// Checks if the DB row value is equals the value /// /// - private bool IsStillMainDom() + private bool IsMainDomValue(string val) { return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", - new { key = MainDomKey, val = _appDomainId }) == 1; + new { key = MainDomKey, val = val }) == 1; } /// @@ -157,9 +286,46 @@ namespace Umbraco.Core.Runtime { if (disposing) { - _db.Dispose(); + // capture locally just in case in some strange way a sub task somehow updates this + var mainDomChanging = _mainDomChanging; + + // immediately cancel all sub-tasks, we don't want them to keep querying _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (mainDomChanging) + { + _db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } + } + catch (Exception) + { + _db.AbortTransaction(); + throw; + } + finally + { + _db.CompleteTransaction(); + + _db.Dispose(); + } } disposedValue = true; From a63de7672bd891a306d5c1a8bc2cc682fd3cc784 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 15:53:57 +0100 Subject: [PATCH 138/610] updates a bool check --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index c077572f85..edd5af7ea8 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -181,9 +181,9 @@ namespace Umbraco.Core.Runtime return true; } - else if (mainDomRows.Count == 1 && mainDomRows[0].Value.EndsWith("_updated")) + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) { - // in this case, there is a suffixed _updated value but it's not for our ID which means + // in this case, the prefixed ID is different which means // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. From 0ef08db7658f1eae04c1d3602200b0c8420e4a88 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 11 Dec 2019 08:55:07 +0100 Subject: [PATCH 139/610] adds error logging and ensure we don't throw exceptions on a background task which would destroy the app domain --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 138 ++++++++++++++------- 1 file changed, 91 insertions(+), 47 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index edd5af7ea8..c567b76403 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -22,42 +22,45 @@ namespace Umbraco.Core.Runtime private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); private bool _mainDomChanging = false; + private readonly UmbracoDatabaseFactory _dbFactory; + private bool _hasError; public SqlMainDomLock(ILogger logger) { // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; + _dbFactory = new UmbracoDatabaseFactory( + Constants.System.UmbracoConnectionName, + _logger, + new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); } public async Task AcquireLockAsync(int millisecondsTimeout) { - var factory = new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, - _logger, - new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); - - _db = factory.CreateDatabase(); + var db = GetDatabase(); var tempId = Guid.NewGuid().ToString(); try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); try { // wait to get a write lock - _sqlServerSyntax.WriteLock(_db, millisecondsTimeout, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, millisecondsTimeout, Constants.Locks.MainDom); } catch (Exception ex) { if (IsLockTimeoutException(ex)) { + _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); + _hasError = true; return false; } - // unexpected + // unexpected (will be caught below) throw; } @@ -76,16 +79,17 @@ namespace Umbraco.Core.Runtime // if we've updated, this means there is an active MainDom, now we need to wait to // for the current MainDom to shutdown which also requires releasing our write lock } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - + ResetDatabase(); // unexpected - throw; + _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } return await WaitForExistingAsync(tempId, millisecondsTimeout); @@ -93,6 +97,12 @@ namespace Umbraco.Core.Runtime public Task ListenAsync() { + if (_hasError) + { + _logger.Warn("Could not acquire MainDom, listening is canceled."); + return Task.CompletedTask; + } + // Create a long running task (dedicated thread) // to poll to check if we are still the MainDom registered in the DB return Task.Factory.StartNew(() => @@ -105,12 +115,14 @@ namespace Umbraco.Core.Runtime // poll every 1 second Thread.Sleep(1000); + var db = GetDatabase(); + try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that // we are still the maindom. An empty value might be better because then we won't have any orphan rows @@ -123,14 +135,17 @@ namespace Umbraco.Core.Runtime return; } } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - throw; + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; + return; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } } @@ -139,6 +154,23 @@ namespace Umbraco.Core.Runtime } + private void ResetDatabase() + { + if (_db.InTransaction) + _db.AbortTransaction(); + _db.Dispose(); + _db = null; + } + + private IUmbracoDatabase GetDatabase() + { + if (_db != null) + return _db; + + _db = _dbFactory.CreateDatabase(); + return _db; + } + /// /// Wait for any existing MainDom to release so we can continue booting /// @@ -151,6 +183,7 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { + var db = GetDatabase(); var watch = new Stopwatch(); watch.Start(); while(true) @@ -160,12 +193,13 @@ namespace Umbraco.Core.Runtime try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - var mainDomRows = _db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) { @@ -174,7 +208,7 @@ namespace Umbraco.Core.Runtime // which indicates that we // can acquire it and it has shutdown. - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId); @@ -192,18 +226,22 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - _db.AbortTransaction(); + ResetDatabase(); if (IsLockTimeoutException(ex)) { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _hasError = true; return false; } - - throw; + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } if (watch.ElapsedMilliseconds >= millisecondsTimeout) @@ -218,9 +256,9 @@ namespace Umbraco.Core.Runtime try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId); @@ -228,19 +266,22 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - _db.AbortTransaction(); + ResetDatabase(); if (IsLockTimeoutException(ex)) { - // something is wrong, we cannot acquire, not much we can do + // something is wrong, we cannot acquire, not much we can do + _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); + _hasError = true; return false; } - - throw; + _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } } } @@ -252,7 +293,8 @@ namespace Umbraco.Core.Runtime /// private RecordPersistenceType InsertLockRecord(string id) { - return _db.InsertOrUpdate(new KeyValueDto + var db = GetDatabase(); + return db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, Value = id, @@ -266,7 +308,8 @@ namespace Umbraco.Core.Runtime /// private bool IsMainDomValue(string val) { - return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", + var db = GetDatabase(); + return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", new { key = MainDomKey, val = val }) == 1; } @@ -293,12 +336,13 @@ namespace Umbraco.Core.Runtime _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); + var db = GetDatabase(); try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a write lock - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // When we are disposed, it means we have released the MainDom lock // and called all MainDom release callbacks, in this case @@ -308,23 +352,23 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (mainDomChanging) { - _db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { - _db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - throw; + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; } finally { - _db.CompleteTransaction(); - - _db.Dispose(); + db?.CompleteTransaction(); + ResetDatabase(); } } From 6f67105645e616a8b36dcddb1db7cabd8c9a3c2d Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:25:30 +0100 Subject: [PATCH 140/610] Undo removing the MiniProfiler routes if solution is not in debug mode --- .../Profiling/WebProfilerProvider.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/Umbraco.Web/Profiling/WebProfilerProvider.cs b/src/Umbraco.Web/Profiling/WebProfilerProvider.cs index e0dcfcf9b1..ffd1871ecc 100644 --- a/src/Umbraco.Web/Profiling/WebProfilerProvider.cs +++ b/src/Umbraco.Web/Profiling/WebProfilerProvider.cs @@ -1,10 +1,7 @@ using System; -using System.Linq; using System.Threading; using System.Web; -using System.Web.Routing; using StackExchange.Profiling; -using Umbraco.Core.Configuration; namespace Umbraco.Web.Profiling { @@ -27,20 +24,6 @@ namespace Umbraco.Web.Profiling { // booting... _bootPhase = BootPhase.Boot; - - // Remove Mini Profiler routes when not in debug mode - if (GlobalSettings.DebugMode == false) - { - //NOTE: Keep the global fully qualified name, for some reason without it I was getting null refs - var prefix = global::StackExchange.Profiling.MiniProfiler.Settings.RouteBasePath.Replace("~/", string.Empty); - - using (RouteTable.Routes.GetWriteLock()) - { - var routes = RouteTable.Routes.Where(x => x is Route r && r.Url.StartsWith(prefix)).ToList(); - foreach(var r in routes) - RouteTable.Routes.Remove(r); - } - } } /// @@ -135,4 +118,4 @@ namespace Umbraco.Web.Profiling } } } -} +} \ No newline at end of file From c845f5d8c578e6bdf12bc20000b485dd9933bf10 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:26:31 +0100 Subject: [PATCH 141/610] Discard MiniProfiler results when not in debug mode --- src/Umbraco.Web/Profiling/WebProfiler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Profiling/WebProfiler.cs b/src/Umbraco.Web/Profiling/WebProfiler.cs index fd980db2d1..f44d4876e2 100644 --- a/src/Umbraco.Web/Profiling/WebProfiler.cs +++ b/src/Umbraco.Web/Profiling/WebProfiler.cs @@ -82,7 +82,7 @@ namespace Umbraco.Web.Profiling if (isBootRequest) _provider.EndBootRequest(); if (isBootRequest || ShouldProfile(sender)) - Stop(); + Stop(!GlobalSettings.DebugMode); } private bool ShouldProfile(object sender) From 262a4cba36fee93cff531ab84ad26b4f88d2ea9c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:30:25 +0100 Subject: [PATCH 142/610] Explicitly set MiniProfiler storage to HttpRuntimeCacheStorage --- src/Umbraco.Web/Profiling/WebProfiler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/Profiling/WebProfiler.cs b/src/Umbraco.Web/Profiling/WebProfiler.cs index f44d4876e2..2fa5639fa7 100644 --- a/src/Umbraco.Web/Profiling/WebProfiler.cs +++ b/src/Umbraco.Web/Profiling/WebProfiler.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Web; using StackExchange.Profiling; using StackExchange.Profiling.SqlFormatters; +using StackExchange.Profiling.Storage; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; @@ -32,6 +33,7 @@ namespace Umbraco.Web.Profiling MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); MiniProfiler.Settings.StackMaxLength = 5000; MiniProfiler.Settings.ProfilerProvider = _provider; + MiniProfiler.Settings.Storage = new HttpRuntimeCacheStorage(TimeSpan.FromMinutes(30)); //Binds to application events to enable the MiniProfiler with a real HttpRequest UmbracoApplicationBase.ApplicationInit += UmbracoApplicationApplicationInit; From 492fb01ad9c3c56d7e1fcdd2cb22f977fdecd835 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 12 Dec 2019 13:41:40 +0100 Subject: [PATCH 143/610] Don't load languages in treepicker unless they're needed --- .../treepicker/treepicker.controller.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) 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 0ff6403761..edb344b0a5 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 @@ -75,18 +75,20 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", */ function onInit () { - // load languages - languageResource.getAll().then(function (languages) { - vm.languages = languages; + if (vm.showLanguageSelector) { + // load languages + languageResource.getAll().then(function (languages) { + vm.languages = languages; - // set the default language - vm.languages.forEach(function (language) { - if (language.isDefault) { - vm.selectedLanguage = language; - vm.languageSelectorIsOpen = false; - } + // set the default language + vm.languages.forEach(function (language) { + if (language.isDefault) { + vm.selectedLanguage = language; + vm.languageSelectorIsOpen = false; + } + }); }); - }); + } if (vm.treeAlias === "content") { vm.entityType = "Document"; @@ -211,7 +213,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", if (vm.dataTypeKey) { queryParams["dataTypeKey"] = vm.dataTypeKey; } - + var queryString = $.param(queryParams); //create the query string from the params object if (!queryString) { From 9e9a2d8379877961e2c0563b91286a4ca4f3f3d3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Dec 2019 10:38:03 +1100 Subject: [PATCH 144/610] Adds TestData project with an endpoint to create content/media hieararchy for testing --- .../Properties/AssemblyInfo.cs | 36 +++ src/Umbraco.TestData/Umbraco.TestData.csproj | 69 +++++ .../UmbracoTestDataController.cs | 269 ++++++++++++++++++ src/Umbraco.TestData/readme.md | 4 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 + src/umbraco.sln | 7 + 6 files changed, 391 insertions(+) create mode 100644 src/Umbraco.TestData/Properties/AssemblyInfo.cs create mode 100644 src/Umbraco.TestData/Umbraco.TestData.csproj create mode 100644 src/Umbraco.TestData/UmbracoTestDataController.cs create mode 100644 src/Umbraco.TestData/readme.md diff --git a/src/Umbraco.TestData/Properties/AssemblyInfo.cs b/src/Umbraco.TestData/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3c4251cdf6 --- /dev/null +++ b/src/Umbraco.TestData/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Umbraco.TestData")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.TestData")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fb5676ed-7a69-492c-b802-e7b24144c0fc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj new file mode 100644 index 0000000000..b0d6b5677b --- /dev/null +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC} + Library + Properties + Umbraco.TestData + Umbraco.TestData + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + 28.4.4 + + + 5.2.7 + + + + \ No newline at end of file diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs new file mode 100644 index 0000000000..8912a28940 --- /dev/null +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using System.Web.Mvc; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Mvc; +using System.Configuration; +using Bogus; +using Umbraco.Core.Scoping; + +namespace Umbraco.TestData +{ + /// + /// Creates test data + /// + public class UmbracoTestDataController : SurfaceController + { + private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE"; + private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; + private const string TextDataTypeName = "UmbracoTestDataContent.Text"; + private const string TestDataContentTypeAlias = "umbTestDataContent"; + private const int IdealPerBranch = 5; + private readonly IScopeProvider _scopeProvider; + private readonly PropertyEditorCollection _propertyEditors; + + public UmbracoTestDataController(IScopeProvider scopeProvider, PropertyEditorCollection propertyEditors, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, UmbracoHelper umbracoHelper) : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger, umbracoHelper) + { + _scopeProvider = scopeProvider; + _propertyEditors = propertyEditors; + } + + /// + /// Creates a content and associated media tree (hierarchy) + /// + /// + /// + /// + /// + /// + /// Each content item created is associated to a media item via a media picker and therefore a relation is created between the two + /// + public ActionResult CreateTree(int count, int depth, string locale = "en") + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + if (!Validate(count, depth, out var message, out var perLevel)) + throw new InvalidOperationException(message); + + var faker = new Faker(locale); + var company = faker.Company.CompanyName(); + + using (var scope = _scopeProvider.CreateScope()) + { + var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); + var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); + + Services.ContentService.SaveAndPublishBranch(root, true); + + scope.Complete(); + } + + + return Content("Done"); + } + + private bool Validate(int count, int depth, out string message, out int perLevel) + { + perLevel = 0; + message = null; + + if (count <= 0) + { + message = "Count must be more than 0"; + return false; + } + + perLevel = count / depth; + if (perLevel < 1) + { + message = "Count not high enough for specified for number of levels required"; + return false; + } + + return true; + } + + /// + /// Utility to create a tree hierarchy + /// + /// + /// + /// + /// + /// + /// A callback that returns a tuple of Content and another callback to produce a Container. + /// For media, a container will be another folder, for content the container will be the Content itself. + /// + /// + private IEnumerable CreateHierarchy( + T parent, int count, int depth, + Func container)> creator) + where T: class, IContentBase + { + yield return parent.GetUdi(); + + var totalDescendants = count - 1; + int perLevel = totalDescendants / depth; + var branchCount = perLevel / IdealPerBranch; + var perBranch = perLevel / branchCount; + + var currentPerBranchCount = 0; + + T lastParent = null; + + for (int i = 0; i < count; i++) + { + var created = creator(parent); + var contentItem = created.content; + + yield return contentItem.GetUdi(); + + currentPerBranchCount++; + + if (contentItem.Level < depth) + { + // not at max depth, create below + lastParent = parent; + parent = created.container(); + currentPerBranchCount = 0; + } + else if (currentPerBranchCount == perBranch) + { + parent = lastParent; + currentPerBranchCount = 0; + } + } + } + + /// + /// Creates the media tree hiearachy + /// + /// + /// + /// + /// + /// + private IEnumerable CreateMediaTree(string company, Faker faker, int count, int depth) + { + var parent = Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder); + + return CreateHierarchy(parent, count, depth, currParent => + { + var imageUrl = faker.Image.PicsumUrl(); + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); + media.SetValue(Constants.Conventions.Media.File, imageUrl); + Services.MediaService.Save(media); + return (media, () => + { + // create a folder to contain child media + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), parent, Constants.Conventions.MediaTypes.Folder); + return container; + }); + }); + } + + /// + /// Creates the content tree hiearachy + /// + /// + /// + /// + /// + /// + /// + private IEnumerable CreateContentTree(string company, Faker faker, int count, int depth, List imageIds, out IContent root) + { + var random = new Random(company.GetHashCode()); + + var docType = GetOrCreateContentType(); + + var parent = Services.ContentService.Create(company, -1, docType.Alias); + parent.SetValue("review", faker.Rant.Review()); + parent.SetValue("desc", company); + parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + Services.ContentService.Save(parent); + + root = parent; + + return CreateHierarchy(parent, count, depth, currParent => + { + var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); + content.SetValue("review", faker.Rant.Review()); + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); ; + content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + + Services.ContentService.Save(content); + return (content, () => content); + }); + + } + + private IContentType GetOrCreateContentType() + { + var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias); + if (docType != null) + return docType; + + docType = new ContentType(-1) + { + Alias = TestDataContentTypeAlias, + Name = "Umbraco Test Data Content", + Icon = "icon-science color-green" + }; + docType.AddPropertyGroup("Content"); + docType.AddPropertyType(new PropertyType(GetOrCreateRichText(), "review") + { + Name = "Review" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateMediaPicker(), "media") + { + Name = "Media" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateText(), "desc") + { + Name = "Description" + }); + Services.ContentTypeService.Save(docType); + docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) }; + Services.ContentTypeService.Save(docType); + return docType; + } + + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + + private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); + + private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox); + + private IDataType GetOrCreateDataType(string name, string editorAlias) + { + var dt = Services.DataTypeService.GetDataType(name); + if (dt != null) return dt; + + var editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias); + if (editor == null) + throw new InvalidOperationException($"No {editorAlias} editor found"); + + dt = new DataType(editor) + { + Name = name, + Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject, + DatabaseType = ValueStorageType.Ntext + }; + + Services.DataTypeService.Save(dt); + return dt; + } + } +} diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md new file mode 100644 index 0000000000..78c08e0957 --- /dev/null +++ b/src/Umbraco.TestData/readme.md @@ -0,0 +1,4 @@ +## Umbraco Test Data + +This project is a utility to be able to generate large amounts of content and media in an +Umbraco installation for testing. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 43130f34a3..b18856ad2d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -115,6 +115,12 @@ + + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} diff --git a/src/umbraco.sln b/src/umbraco.sln index 938532beb0..0c5f02c7bd 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -102,6 +102,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp ..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +136,10 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,6 +152,7 @@ Global {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} + {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 292a40194fccd8e3cfef04213f8cf49321013cea Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Dec 2019 16:46:02 +1100 Subject: [PATCH 145/610] updates readme, adjusts controller --- .../UmbracoTestDataController.cs | 48 +++++++++++-------- src/Umbraco.TestData/readme.md | 45 +++++++++++++++++ 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 8912a28940..f5ded2a754 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -28,7 +28,6 @@ namespace Umbraco.TestData private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; private const string TextDataTypeName = "UmbracoTestDataContent.Text"; private const string TestDataContentTypeAlias = "umbTestDataContent"; - private const int IdealPerBranch = 5; private readonly IScopeProvider _scopeProvider; private readonly PropertyEditorCollection _propertyEditors; @@ -101,48 +100,59 @@ namespace Umbraco.TestData /// /// /// - /// + /// /// A callback that returns a tuple of Content and another callback to produce a Container. /// For media, a container will be another folder, for content the container will be the Content itself. /// /// private IEnumerable CreateHierarchy( T parent, int count, int depth, - Func container)> creator) + Func container)> create) where T: class, IContentBase { yield return parent.GetUdi(); + // This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree. var totalDescendants = count - 1; - int perLevel = totalDescendants / depth; - var branchCount = perLevel / IdealPerBranch; - var perBranch = perLevel / branchCount; + var perLevel = Math.Ceiling(totalDescendants / (double)depth); + var perBranch = Math.Ceiling(perLevel / depth); - var currentPerBranchCount = 0; + var tracked = new Stack<(T parent, int childCount)>(); - T lastParent = null; + var currChildCount = 0; for (int i = 0; i < count; i++) { - var created = creator(parent); + var created = create(parent); var contentItem = created.content; yield return contentItem.GetUdi(); - currentPerBranchCount++; + currChildCount++; - if (contentItem.Level < depth) + if (currChildCount == perBranch) { + // move back up... + + var prev = tracked.Pop(); + + // restore child count + currChildCount = prev.childCount; + // restore the parent + parent = prev.parent; + + } + else if (contentItem.Level < depth) + { + // track the current parent and it's current child count + tracked.Push((parent, currChildCount)); + // not at max depth, create below - lastParent = parent; parent = created.container(); - currentPerBranchCount = 0; - } - else if (currentPerBranchCount == perBranch) - { - parent = lastParent; - currentPerBranchCount = 0; + + currChildCount = 0; } + } } @@ -167,7 +177,7 @@ namespace Umbraco.TestData return (media, () => { // create a folder to contain child media - var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), parent, Constants.Conventions.MediaTypes.Folder); + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder); return container; }); }); diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 78c08e0957..717b6aac57 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -2,3 +2,48 @@ This project is a utility to be able to generate large amounts of content and media in an Umbraco installation for testing. + +Currently this project is referenced in the Umbraco.Web.UI project but only when it's being built +in Debug mode (i.e. when testing within Visual Studio). + +## Usage + +It has to be enabled by an appSetting: + +```xml + +``` + +Once this is enabled this endpoint can be executed: + +`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5` + +The query string options are: + +* `count` = the number of content and media nodes to create +* `depth` = how deep the trees created will be +* `locale` (optional, default = "en") = the language that the data will be generated in + +This creates a content and associated media tree (hierarchy). Each content item created is associated +to a media item via a media picker and therefore a relation is created between the two. Each content and +media tree created have the same root node name so it's easy to know which content branch relates to +which media branch. + +All values are generated using the very handy `Bogus` package. + +## Schema + +This will install some schema items: + +* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type +* `UmbracoTestDataContent.RTE` Data Type +* `UmbracoTestDataContent.MediaPicker` Data Type +* `UmbracoTestDataContent.Text` Data Type + +For media, the normal folder and image is used + +## Media + +This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. +This works when viewing the media item in the media section and the image will show up, however when viewing a content item +that has these media items picked, the thumbnail will not show up (which is due to a bug in the CMS that will be fixed in 8.6). From 25d1c454dc10ce6bb6e9e3e9b0b5aad386492f04 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Tue, 17 Dec 2019 16:50:54 +1100 Subject: [PATCH 146/610] Update readme.md --- src/Umbraco.TestData/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 717b6aac57..5475da1ff5 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -8,6 +8,8 @@ in Debug mode (i.e. when testing within Visual Studio). ## Usage +You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data. + It has to be enabled by an appSetting: ```xml From 000a8d2c946ef64e7e9b025b4f0e1efa7f3cc6b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 15:37:00 +1100 Subject: [PATCH 147/610] manually merges changes for the NC validation stuff since there was conflicts --- .../NestedContentPropertyEditor.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index f3e391aeab..564630c574 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -293,9 +293,19 @@ namespace Umbraco.Web.PropertyEditors if (row.PropType.Mandatory) { if (row.JsonRowValue[row.PropKey] == null) - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be null", new[] { row.PropKey })); + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be null" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be empty", new[] { row.PropKey })); + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be empty" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } } // Check regex @@ -305,7 +315,10 @@ namespace Umbraco.Web.PropertyEditors var regex = new Regex(row.PropType.ValidationRegExp); if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) { - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' is invalid, it does not match the correct pattern", new[] { row.PropKey })); + var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage) + ? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern" + : row.PropType.ValidationRegExpMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); } } } From 1e35aeb9cf3ef100eb7a2e25d5aca84f9e5bb439 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:01:27 +1100 Subject: [PATCH 148/610] ensures .jpg ext is added to test data --- src/Umbraco.TestData/UmbracoTestDataController.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index f5ded2a754..402c05cc1c 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -171,6 +171,15 @@ namespace Umbraco.TestData return CreateHierarchy(parent, count, depth, currParent => { var imageUrl = faker.Image.PicsumUrl(); + + // we are appending a &ext=.jpg to the end of this for a reason. The result of this url will be something like: + // https://picsum.photos/640/480/?image=106 + // and due to the way that we detect images there must be an extension so we'll change it to + // https://picsum.photos/640/480/?image=106&ext=.jpg + // which will trick our app into parsing this and thinking it's an image ... which it is so that's good. + // if we don't do this we don't get thumbnails in the back office. + imageUrl += "&ext=.jpg"; + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); media.SetValue(Constants.Conventions.Media.File, imageUrl); Services.MediaService.Save(media); From e0531f1429cc2e1f7c812c7737653f2bc9ed58d0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:10:33 +1100 Subject: [PATCH 149/610] allows ImagesController to still return valid urls even if the uri isn't resolved to a local on-disk file --- src/Umbraco.Web/Editors/ImagesController.cs | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index b29c166765..db6706d1bb 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -60,8 +60,27 @@ namespace Umbraco.Web.Editors //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file var response = Request.CreateResponse(HttpStatusCode.Found); - var imageLastModified = _mediaFileSystem.GetLastModified(imagePath); - response.Headers.Location = new Uri($"{imagePath}?rnd={imageLastModified:yyyyMMddHHmmss}&upscale=false&width={width}&animationprocessmode=first&mode=max", UriKind.Relative); + + DateTimeOffset? imageLastModified = null; + try + { + imageLastModified = _mediaFileSystem.GetLastModified(imagePath); + + } + catch (Exception) + { + // if we get an exception here it's probably because the image path being requested is an image that doesn't exist + // in the local media file system. This can happen if someone is storing an absolute path to an image online, which + // is perfectly legal but in that case the media file system isn't going to resolve it. + // so ignore and we won't set a last modified date. + } + + // TODO: When we abstract imaging for netcore, we are actually just going to be abstracting a URI builder for images, this + // is one of those places where this can be used. + + var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : string.Empty; + + response.Headers.Location = new Uri($"{imagePath}?upscale=false&width={width}&animationprocessmode=first&mode=max{rnd}", UriKind.RelativeOrAbsolute); return response; } From ddaafa2abe1c0fb1f65daf7aa692835c604eea2c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:11:31 +1100 Subject: [PATCH 150/610] updates readme --- src/Umbraco.TestData/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 5475da1ff5..f943326303 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -47,5 +47,5 @@ For media, the normal folder and image is used ## Media This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. -This works when viewing the media item in the media section and the image will show up, however when viewing a content item -that has these media items picked, the thumbnail will not show up (which is due to a bug in the CMS that will be fixed in 8.6). +This works when viewing the media item in the media section and the image will show up and with recent changes this will also work +when editing content to view the thumbnail for the picked media. From 7501629c541633c8bb33077b473514bbe288f359 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 17:15:57 +1100 Subject: [PATCH 151/610] Adds logging to SqlMainDomLock --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index c567b76403..a2560b71a3 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -38,6 +38,8 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + _logger.Info("Acquiring lock..."); + var db = GetDatabase(); var tempId = Guid.NewGuid().ToString(); @@ -73,6 +75,7 @@ namespace Umbraco.Core.Runtime // are MainDom? then we don't leave any orphan rows behind. InsertLockRecord(_lockId); // so update with our appdomain id + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } @@ -212,7 +215,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -221,6 +224,8 @@ namespace Umbraco.Core.Runtime // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. + _logger.Info("Cannot acquire, another booting application detected."); + return false; } } @@ -253,6 +258,7 @@ namespace Umbraco.Core.Runtime // which isn't ideal. // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + _logger.Info("Timeout elapsed, assuming orphan row, acquiring MainDom."); try { @@ -262,6 +268,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } catch (Exception ex) From 0d1101eaa748180d350662beee6040d203bda37f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:14:18 +1100 Subject: [PATCH 152/610] adds notes/logging --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index a2560b71a3..d7b800c364 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -112,6 +112,10 @@ namespace Umbraco.Core.Runtime { while(true) { + // If cancellation has been requested we will just exit. Depending on timing of the shutdown, + // we will have already flagged _mainDomChanging = true, or we're shutting down faster than + // the other MainDom is taking to startup. In this case the db row will just be deleted and the + // new MainDom will just take over. if (_cancellationTokenSource.IsCancellationRequested) break; @@ -135,6 +139,7 @@ namespace Umbraco.Core.Runtime { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; + _logger.Info("Detected new booting application, releasing MainDom."); return; } } @@ -359,10 +364,12 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (mainDomChanging) { + _logger.Info("Releasing MainDom, updating row, new application is booting."); db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { + _logger.Info("Releasing MainDom, deleting row, application is shutting down."); db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } From 5476388e787c778852802042267f1f20ceb3aae6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:49:33 +1100 Subject: [PATCH 153/610] adds lock else we end up detecting when not needed --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 143 +++++++++++---------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index d7b800c364..bb44bbbfbf 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Runtime private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; private bool _hasError; + private object _locker = new object(); public SqlMainDomLock(ILogger logger) { @@ -112,49 +113,53 @@ namespace Umbraco.Core.Runtime { while(true) { - // If cancellation has been requested we will just exit. Depending on timing of the shutdown, - // we will have already flagged _mainDomChanging = true, or we're shutting down faster than - // the other MainDom is taking to startup. In this case the db row will just be deleted and the - // new MainDom will just take over. - if (_cancellationTokenSource.IsCancellationRequested) - break; - // poll every 1 second Thread.Sleep(1000); - var db = GetDatabase(); - - try + lock(_locker) { - db.BeginTransaction(IsolationLevel.ReadCommitted); + // If cancellation has been requested we will just exit. Depending on timing of the shutdown, + // we will have already flagged _mainDomChanging = true, or we're shutting down faster than + // the other MainDom is taking to startup. In this case the db row will just be deleted and the + // new MainDom will just take over. + if (_cancellationTokenSource.IsCancellationRequested) + break; - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + var db = GetDatabase(); - // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that - // we are still the maindom. An empty value might be better because then we won't have any orphan rows - // if the app is terminated. Could that work? - - if (!IsMainDomValue(_lockId)) + try { - // we are no longer main dom, another one has come online, exit - _mainDomChanging = true; - _logger.Info("Detected new booting application, releasing MainDom."); + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + + // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that + // we are still the maindom. An empty value might be better because then we won't have any orphan rows + // if the app is terminated. Could that work? + + if (!IsMainDomValue(_lockId)) + { + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; + _logger.Info("Detected new booting application, releasing MainDom."); + return; + } + } + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; return; } + finally + { + db?.CompleteTransaction(); + } } - catch (Exception ex) - { - ResetDatabase(); - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; - return; - } - finally - { - db?.CompleteTransaction(); - } + } @@ -341,48 +346,48 @@ namespace Umbraco.Core.Runtime { if (disposing) { - // capture locally just in case in some strange way a sub task somehow updates this - var mainDomChanging = _mainDomChanging; - - // immediately cancel all sub-tasks, we don't want them to keep querying - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - - var db = GetDatabase(); - try + lock (_locker) { - db.BeginTransaction(IsolationLevel.ReadCommitted); + // immediately cancel all sub-tasks, we don't want them to keep querying + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); - // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // When we are disposed, it means we have released the MainDom lock - // and called all MainDom release callbacks, in this case - // if another maindom is actually coming online we need - // to signal to the MainDom coming online that we have shutdown. - // To do that, we update the existing main dom DB record with a suffixed "_updated" string. - // Otherwise, if we are just shutting down, we want to just delete the row. - if (mainDomChanging) + var db = GetDatabase(); + try { - _logger.Info("Releasing MainDom, updating row, new application is booting."); - db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (_mainDomChanging) + { + _logger.Info("Releasing MainDom, updating row, new application is booting."); + db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _logger.Info("Releasing MainDom, deleting row, application is shutting down."); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } } - else + catch (Exception ex) { - _logger.Info("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; + } + finally + { + db?.CompleteTransaction(); + ResetDatabase(); } - } - catch (Exception ex) - { - ResetDatabase(); - _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; - } - finally - { - db?.CompleteTransaction(); - ResetDatabase(); } } From b24a87d9868d70bcbc164559f74caab744785b5c Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:51:38 +1100 Subject: [PATCH 154/610] Changes logging to debug --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index bb44bbbfbf..847a0c25df 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -39,7 +39,7 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { - _logger.Info("Acquiring lock..."); + _logger.Debug("Acquiring lock..."); var db = GetDatabase(); @@ -76,7 +76,7 @@ namespace Umbraco.Core.Runtime // are MainDom? then we don't leave any orphan rows behind. InsertLockRecord(_lockId); // so update with our appdomain id - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } @@ -142,7 +142,7 @@ namespace Umbraco.Core.Runtime { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; - _logger.Info("Detected new booting application, releasing MainDom."); + _logger.Debug("Detected new booting application, releasing MainDom."); return; } } @@ -225,7 +225,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -234,7 +234,7 @@ namespace Umbraco.Core.Runtime // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. - _logger.Info("Cannot acquire, another booting application detected."); + _logger.Debug("Cannot acquire, another booting application detected."); return false; } @@ -268,7 +268,7 @@ namespace Umbraco.Core.Runtime // which isn't ideal. // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok - _logger.Info("Timeout elapsed, assuming orphan row, acquiring MainDom."); + _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); try { @@ -278,7 +278,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } catch (Exception ex) @@ -368,12 +368,12 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (_mainDomChanging) { - _logger.Info("Releasing MainDom, updating row, new application is booting."); + _logger.Debug("Releasing MainDom, updating row, new application is booting."); db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { - _logger.Info("Releasing MainDom, deleting row, application is shutting down."); + _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } From 00c913d6217ecd9dfe16454d0f883cf034e328f4 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 12:02:48 +0000 Subject: [PATCH 155/610] add InternalSearchConstants --- src/Umbraco.Web/InternalSearchConstants.cs | 32 +++++++++++++++++++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 2 +- .../Search/IInternalSearchConstants.cs | 13 ++++++++ src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 12 ++++--- src/Umbraco.Web/Umbraco.Web.csproj | 2 ++ 5 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web/InternalSearchConstants.cs create mode 100644 src/Umbraco.Web/Search/IInternalSearchConstants.cs diff --git a/src/Umbraco.Web/InternalSearchConstants.cs b/src/Umbraco.Web/InternalSearchConstants.cs new file mode 100644 index 0000000000..012cd98f1d --- /dev/null +++ b/src/Umbraco.Web/InternalSearchConstants.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Umbraco.Examine; +using Umbraco.Web.Search; + +namespace Umbraco.Web +{ + public class InternalSearchConstants : IInternalSearchConstants + { + private List _backOfficeFields = new List {"id", "__NodeId", "__Key"}; + public List GetBackOfficeFields() + { + return _backOfficeFields; + } + + + private List _backOfficeMembersFields = new List {"email", "loginName"}; + public List GetBackOfficeMembersFields() + { + return _backOfficeMembersFields; + } + private List _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; + public List GetBackOfficeMediaFields() + { + return _backOfficeMediaFields; + } + private List _backOfficeDocumentFields = new List (); + public List GetBackOfficeDocumentFields() + { + return _backOfficeDocumentFields; + } + } +} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 87c0f46fba..b8be2319d8 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -95,7 +95,7 @@ namespace Umbraco.Web.Runtime // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context // in it's own way but we don't want that to happen, we manage its lifetime ourselves. composition.Register(factory => factory.GetInstance().UmbracoContext); - + composition.RegisterUnique(); composition.Register(factory => { var umbCtx = factory.GetInstance(); diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IInternalSearchConstants.cs new file mode 100644 index 0000000000..72a37deaa1 --- /dev/null +++ b/src/Umbraco.Web/Search/IInternalSearchConstants.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Umbraco.Web.Search +{ + public interface IInternalSearchConstants + { + List GetBackOfficeFields(); + List GetBackOfficeMembersFields(); + + List GetBackOfficeMediaFields(); + List GetBackOfficeDocumentFields(); + } +} diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 463f4b09df..1345ffd4cc 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -28,13 +28,15 @@ namespace Umbraco.Web.Search private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; + private readonly IInternalSearchConstants _internalSearchConstants; + public UmbracoTreeSearcher(IExamineManager examineManager, UmbracoContext umbracoContext, ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext) + ISqlContext sqlContext,IInternalSearchConstants internalSearchConstants) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -42,6 +44,7 @@ namespace Umbraco.Web.Search _entityService = entityService; _mapper = mapper; _sqlContext = sqlContext; + _internalSearchConstants = internalSearchConstants; } /// @@ -67,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = new List { "id", "__NodeId", "__Key" }; + var fields = _internalSearchConstants.GetBackOfficeFields(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -87,7 +90,7 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields.AddRange(new[]{ "email", "loginName"}); + fields.AddRange(_internalSearchConstants.GetBackOfficeMembersFields()); if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -97,12 +100,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; - fields.AddRange(new[] { UmbracoExamineIndex.UmbracoFileFieldName }); + fields.AddRange(_internalSearchConstants.GetBackOfficeMediaFields()); var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; + fields.AddRange(_internalSearchConstants.GetBackOfficeDocumentFields()); var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5d29e53d4a..159abd435b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -155,6 +155,7 @@ + @@ -249,6 +250,7 @@ + From 9614dcf0b08bcca001e423733e2dc7f2826a2de8 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 13:20:32 +0000 Subject: [PATCH 156/610] move InternalSearchConstants to correct place in source code --- src/Umbraco.Web/{ => Search}/InternalSearchConstants.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Umbraco.Web/{ => Search}/InternalSearchConstants.cs (100%) diff --git a/src/Umbraco.Web/InternalSearchConstants.cs b/src/Umbraco.Web/Search/InternalSearchConstants.cs similarity index 100% rename from src/Umbraco.Web/InternalSearchConstants.cs rename to src/Umbraco.Web/Search/InternalSearchConstants.cs From 575ec6fec4e5c0f2aa94a2d4efb216fddef54c18 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 13:39:23 +0000 Subject: [PATCH 157/610] Correct project references --- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 159abd435b..0980b1b3d6 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -155,7 +155,6 @@ - @@ -251,6 +250,7 @@ + From 12f51dec4e6ee3c4b64719799066749feae04cd5 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:00:53 +0000 Subject: [PATCH 158/610] Use immutable types --- .../Search/IInternalSearchConstants.cs | 10 +++++----- .../Search/InternalSearchConstants.cs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IInternalSearchConstants.cs index 72a37deaa1..06222e2ec7 100644 --- a/src/Umbraco.Web/Search/IInternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/IInternalSearchConstants.cs @@ -4,10 +4,10 @@ namespace Umbraco.Web.Search { public interface IInternalSearchConstants { - List GetBackOfficeFields(); - List GetBackOfficeMembersFields(); - - List GetBackOfficeMediaFields(); - List GetBackOfficeDocumentFields(); + IEnumerable GetBackOfficeFields(); + IEnumerable GetBackOfficeMembersFields(); + + IEnumerable GetBackOfficeMediaFields(); + IEnumerable GetBackOfficeDocumentFields(); } } diff --git a/src/Umbraco.Web/Search/InternalSearchConstants.cs b/src/Umbraco.Web/Search/InternalSearchConstants.cs index 012cd98f1d..51e5df4c4e 100644 --- a/src/Umbraco.Web/Search/InternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/InternalSearchConstants.cs @@ -6,25 +6,25 @@ namespace Umbraco.Web { public class InternalSearchConstants : IInternalSearchConstants { - private List _backOfficeFields = new List {"id", "__NodeId", "__Key"}; - public List GetBackOfficeFields() + private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; + public IEnumerable GetBackOfficeFields() { return _backOfficeFields; } - private List _backOfficeMembersFields = new List {"email", "loginName"}; - public List GetBackOfficeMembersFields() + private IReadOnlyList _backOfficeMembersFields = new List {"email", "loginName"}; + public IEnumerable GetBackOfficeMembersFields() { return _backOfficeMembersFields; } - private List _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; - public List GetBackOfficeMediaFields() + private IReadOnlyList _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; + public IEnumerable GetBackOfficeMediaFields() { return _backOfficeMediaFields; } - private List _backOfficeDocumentFields = new List (); - public List GetBackOfficeDocumentFields() + private IReadOnlyList _backOfficeDocumentFields = new List (); + public IEnumerable GetBackOfficeDocumentFields() { return _backOfficeDocumentFields; } From 3b8e6d3cc05621c0c402b807902cf0e549e2d6e9 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:05:08 +0000 Subject: [PATCH 159/610] rename to UmbracoTreeSearcherFields --- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 6 +++--- ...hConstants.cs => IUmbracoTreeSearcherFields.cs} | 2 +- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 14 +++++++------- ...chConstants.cs => UmbracoTreeSearcherFields.cs} | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/Umbraco.Web/Search/{IInternalSearchConstants.cs => IUmbracoTreeSearcherFields.cs} (86%) rename src/Umbraco.Web/Search/{InternalSearchConstants.cs => UmbracoTreeSearcherFields.cs} (93%) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index b8be2319d8..cc6cdc1f60 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web.Runtime // register accessors for cultures composition.RegisterUnique(); composition.RegisterUnique(); - + // register the http context and umbraco context accessors // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when // we have no http context, eg when booting Umbraco or in background threads, so instead @@ -95,7 +95,7 @@ namespace Umbraco.Web.Runtime // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context // in it's own way but we don't want that to happen, we manage its lifetime ourselves. composition.Register(factory => factory.GetInstance().UmbracoContext); - composition.RegisterUnique(); + composition.RegisterUnique(); composition.Register(factory => { var umbCtx = factory.GetInstance(); @@ -268,7 +268,7 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append(); - + // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs similarity index 86% rename from src/Umbraco.Web/Search/IInternalSearchConstants.cs rename to src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs index 06222e2ec7..eaa5d743a1 100644 --- a/src/Umbraco.Web/Search/IInternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace Umbraco.Web.Search { - public interface IInternalSearchConstants + public interface IUmbracoTreeSearcherFields { IEnumerable GetBackOfficeFields(); IEnumerable GetBackOfficeMembersFields(); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 1345ffd4cc..56c7c6fbf3 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Search private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; - private readonly IInternalSearchConstants _internalSearchConstants; + private readonly IUmbracoTreeSearcherFields _umbracoTreeSearcherFields; public UmbracoTreeSearcher(IExamineManager examineManager, @@ -36,7 +36,7 @@ namespace Umbraco.Web.Search ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext,IInternalSearchConstants internalSearchConstants) + ISqlContext sqlContext,IUmbracoTreeSearcherFields umbracoTreeSearcherFields) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -44,7 +44,7 @@ namespace Umbraco.Web.Search _entityService = entityService; _mapper = mapper; _sqlContext = sqlContext; - _internalSearchConstants = internalSearchConstants; + _umbracoTreeSearcherFields = umbracoTreeSearcherFields; } /// @@ -70,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _internalSearchConstants.GetBackOfficeFields(); + var fields = _umbracoTreeSearcherFields.GetBackOfficeFields(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -90,7 +90,7 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields.AddRange(_internalSearchConstants.GetBackOfficeMembersFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMembersFields()); if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -100,13 +100,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; - fields.AddRange(_internalSearchConstants.GetBackOfficeMediaFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; - fields.AddRange(_internalSearchConstants.GetBackOfficeDocumentFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; diff --git a/src/Umbraco.Web/Search/InternalSearchConstants.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs similarity index 93% rename from src/Umbraco.Web/Search/InternalSearchConstants.cs rename to src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index 51e5df4c4e..d1c663024b 100644 --- a/src/Umbraco.Web/Search/InternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -4,7 +4,7 @@ using Umbraco.Web.Search; namespace Umbraco.Web { - public class InternalSearchConstants : IInternalSearchConstants + public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields { private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; public IEnumerable GetBackOfficeFields() From 358b4c9133ac2f4a2cc45cff21cfbe325c3a57fd Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:08:53 +0000 Subject: [PATCH 160/610] Make fields list to allow to add fields --- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 56c7c6fbf3..146177f86f 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -70,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _umbracoTreeSearcherFields.GetBackOfficeFields(); + var fields = _umbracoTreeSearcherFields.GetBackOfficeFields().ToList(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... From a46e9124d28799067377da5d969d2730425f57bb Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 10:38:48 +1100 Subject: [PATCH 161/610] First commit in fixing deadlock - committing my notes, etc... --- src/Umbraco.Core/Runtime/CoreRuntime.cs | 2 +- src/Umbraco.Core/Runtime/MainDom.cs | 27 ++++- .../Runtime/MainDomSemaphoreLock.cs | 9 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 88 +++++++-------- .../PublishedCache/NuCache/ContentStore.cs | 101 ++++++++++++++---- .../NuCache/PublishedSnapshot.cs | 8 ++ .../NuCache/PublishedSnapshotService.cs | 25 ++++- .../NuCache/{notes.txt => readme.md} | 44 ++++---- src/Umbraco.Web/Umbraco.Web.csproj | 4 +- src/Umbraco.Web/UmbracoApplication.cs | 2 +- 10 files changed, 207 insertions(+), 103 deletions(-) rename src/Umbraco.Web/PublishedCache/NuCache/{notes.txt => readme.md} (70%) diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index b46058fa82..b852aff2ff 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -147,7 +147,7 @@ namespace Umbraco.Core.Runtime // TODO: remove this in netcore, this is purely backwards compat hacks with the empty ctor if (MainDom == null) { - MainDom = new MainDom(Logger, new MainDomSemaphoreLock()); + MainDom = new MainDom(Logger, new MainDomSemaphoreLock(Logger)); } diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 85cf5ad6a9..883b69b2a7 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Runtime // actions to run before releasing the main domain private readonly List> _callbacks = new List>(); - private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds + private const int LockTimeoutMilliseconds = 10000; // 10 seconds #endregion @@ -106,6 +106,7 @@ namespace Umbraco.Core.Runtime { _logger.Info("Stopping ({SignalSource})", source); foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) + { try { callback(); // no timeout on callbacks @@ -115,6 +116,8 @@ namespace Umbraco.Core.Runtime _logger.Error(e, "Error while running callback"); continue; } + } + _logger.Debug("Stopped ({SignalSource})", source); } finally @@ -142,17 +145,33 @@ namespace Umbraco.Core.Runtime _logger.Info("Acquiring."); // Get the lock - _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); + var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Result; + + if (!acquired) + { + _logger.Info("Cannot acquire (timeout)."); + + // TODO: Previously we'd throw an exception and the appdomain would not start, what do we want to do? + + // return false; + + throw new TimeoutException("Cannot acquire MainDom"); + } try { // Listen for the signal from another AppDomain coming online to release the lock _mainDomLock.ListenAsync() - .ContinueWith(_ => OnSignal("signal")); + .ContinueWith(_ => + { + _logger.Debug("Signal heard from other appdomain."); + OnSignal("signal"); + }); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception + _logger.Warn(ex, ex.Message); } _logger.Info("Acquired."); diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs index 89eef8a658..2dcc37e25f 100644 --- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Logging; namespace Umbraco.Core.Runtime { @@ -14,16 +15,17 @@ namespace Umbraco.Core.Runtime // event wait handle used to notify current main domain that it should // release the lock because a new domain wants to be the main domain private readonly EventWaitHandle _signal; - + private readonly ILogger _logger; private IDisposable _lockRelease; - public MainDomSemaphoreLock() + public MainDomSemaphoreLock(ILogger logger) { var lockName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-LCK"; _systemLock = new SystemLock(lockName); var eventName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-EVT"; _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + _logger = logger; } //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread @@ -47,8 +49,9 @@ namespace Umbraco.Core.Runtime _lockRelease = _systemLock.Lock(millisecondsTimeout); return Task.FromResult(true); } - catch (TimeoutException) + catch (TimeoutException ex) { + _logger.Error(ex); return Task.FromResult(false); } finally diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 847a0c25df..31f8b36ed0 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -109,62 +109,62 @@ namespace Umbraco.Core.Runtime // Create a long running task (dedicated thread) // to poll to check if we are still the MainDom registered in the DB - return Task.Factory.StartNew(() => + return Task.Factory.StartNew(ListeningLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + } + + private void ListeningLoop() + { + while (true) { - while(true) + // poll every 1 second + Thread.Sleep(1000); + + lock (_locker) { - // poll every 1 second - Thread.Sleep(1000); + // If cancellation has been requested we will just exit. Depending on timing of the shutdown, + // we will have already flagged _mainDomChanging = true, or we're shutting down faster than + // the other MainDom is taking to startup. In this case the db row will just be deleted and the + // new MainDom will just take over. + if (_cancellationTokenSource.IsCancellationRequested) + return; - lock(_locker) + var db = GetDatabase(); + + try { - // If cancellation has been requested we will just exit. Depending on timing of the shutdown, - // we will have already flagged _mainDomChanging = true, or we're shutting down faster than - // the other MainDom is taking to startup. In this case the db row will just be deleted and the - // new MainDom will just take over. - if (_cancellationTokenSource.IsCancellationRequested) - break; + db.BeginTransaction(IsolationLevel.ReadCommitted); - var db = GetDatabase(); + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - try + // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that + // we are still the maindom. An empty value might be better because then we won't have any orphan rows + // if the app is terminated. Could that work? + + if (!IsMainDomValue(_lockId)) { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - - // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that - // we are still the maindom. An empty value might be better because then we won't have any orphan rows - // if the app is terminated. Could that work? - - if (!IsMainDomValue(_lockId)) - { - // we are no longer main dom, another one has come online, exit - _mainDomChanging = true; - _logger.Debug("Detected new booting application, releasing MainDom."); - return; - } - } - catch (Exception ex) - { - ResetDatabase(); - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; + _logger.Debug("Detected new booting application, releasing MainDom lock."); return; } - finally - { - db?.CompleteTransaction(); - } } - + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; + return; + } + finally + { + db?.CompleteTransaction(); + } } - - - }, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } } private void ResetDatabase() diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 550fd507d5..81229af9e1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -115,11 +115,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { + _logger.Debug("GetScopedWriteLock"); return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + // TODO: We are in a deadlock here somehow?? + Monitor.Enter(_wlocko, ref lockInfo.Taken); var rtaken = false; @@ -193,8 +196,15 @@ namespace Umbraco.Web.PublishedCache.NuCache _localDb.Commit(); } - if (lockInfo.Count) _wlocked--; - if (lockInfo.Taken) Monitor.Exit(_wlocko); + // TODO: Shouldn't this be in a finally block? + // TODO: Shouldn't this be decremented after we exit?? + // TODO: Shouldn't the locked flag never exceed 1? + if (lockInfo.Count) + _wlocked--; + + // TODO: Shouldn't this be in a finally block? + if (lockInfo.Taken) + Monitor.Exit(_wlocko); } private void Release(ReadLockInfo lockInfo) @@ -232,21 +242,29 @@ namespace Umbraco.Web.PublishedCache.NuCache public void ReleaseLocalDb() { + _logger.Info("Releasing DB..."); var lockInfo = new WriteLockInfo(); try { try { // Trying to lock could throw exceptions so always make sure to clean up. + _logger.Info("Trying to lock before releasing DB (lock count = {LockCount}) ...", _wlocked); + Lock(lockInfo); } finally { try { + _logger.Info("Disposing local DB..."); _localDb?.Dispose(); } - catch { /* TBD: May already be throwing so don't throw again */} + catch (Exception ex) + { + /* TBD: May already be throwing so don't throw again */ + _logger.Error(ex, "Error trying to release DB"); + } finally { _localDb = null; @@ -254,8 +272,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + catch (Exception ex) + { + _logger.Error(ex, "Error trying to lock"); + throw; + } finally { + _logger.Info("Releasing ContentStore..."); + + // TODO: I don't understand this, we would have already closed the BPlusTree store above?? + // I guess it's 'safe' and will just decrement the write lock counter? Release(lockInfo); } } @@ -275,6 +302,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("NewContentTypes"); Lock(lockInfo); foreach (var type in types) @@ -297,6 +325,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateContentTypes"); Lock(lockInfo); var index = types.ToDictionary(x => x.Id, x => x); @@ -324,10 +353,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public void SetAllContentTypes(IEnumerable types) { - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO lock here! All calls made to this are wrapped in GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + _logger.Debug("SetAllContentTypes"); + //Lock(lockInfo); // clear all existing content types ClearLocked(_contentTypesById); @@ -345,7 +377,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } } @@ -362,6 +394,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateContentTypes"); Lock(lockInfo); var removedContentTypeNodes = new List(); @@ -435,6 +468,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateDataTypes"); Lock(lockInfo); var contentTypes = _contentTypesById @@ -545,10 +579,11 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO locks here, all calls made to this are done within GetScopedWriteLock + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + //Lock(lockInfo); // get existing _contentNodes.TryGetValue(kit.Node.Id, out var link); @@ -594,7 +629,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return true; @@ -623,11 +658,16 @@ namespace Umbraco.Web.PublishedCache.NuCache /// internal bool SetAllFastSorted(IEnumerable kits, bool fromDb) { - var lockInfo = new WriteLockInfo(); + + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetAllFastSorted"); + + //Lock(lockInfo); ClearLocked(_contentNodes); ClearRootLocked(); @@ -665,7 +705,7 @@ namespace Umbraco.Web.PublishedCache.NuCache previousNode = null; // there is no previous sibling } - _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); SetValueLocked(_contentNodes, thisNode.Id, thisNode); // if we are initializing from the database source ensure the local db is updated @@ -689,7 +729,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -697,11 +737,15 @@ namespace Umbraco.Web.PublishedCache.NuCache public bool SetAll(IEnumerable kits) { - var lockInfo = new WriteLockInfo(); + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetAll"); + + //Lock(lockInfo); ClearLocked(_contentNodes); ClearRootLocked(); @@ -717,7 +761,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ok = false; continue; // skip that one } - _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); @@ -728,7 +772,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -737,11 +781,15 @@ namespace Umbraco.Web.PublishedCache.NuCache // IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER public bool SetBranch(int rootContentId, IEnumerable kits) { - var lockInfo = new WriteLockInfo(); + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetBranch"); + + //Lock(lockInfo); // get existing _contentNodes.TryGetValue(rootContentId, out var link); @@ -773,7 +821,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -781,10 +829,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public bool Clear(int id) { - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO locks here! All calls to this are made within GetScopedWriteLock + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + _logger.Debug("Clear"); + + //Lock(lockInfo); // try to find the content // if it is not there, nothing to do @@ -804,7 +855,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } } @@ -1200,6 +1251,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new ReadLockInfo(); try { + // TODO: This would be much simpler with just a lock(_rlocko) { } + // in this case I see no reason why we are using this syntax?! Lock(lockInfo); // if no next generation is required, and we already have one, @@ -1374,6 +1427,8 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // TODO: This is never used? Should it be? + public async Task WaitForPendingCollect() { Task task; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs index 3f5c1aa4d5..bbb84fc650 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs @@ -39,7 +39,15 @@ namespace Umbraco.Web.PublishedCache.NuCache public void Resync() { + // This is annoying since this can cause deadlocks. Since this will be cleared if something happens in the same + // thread after that calls Elements, it will re-lock the _storesLock but that might already be locked again + // and since we're most likely in a ContentStore write lock, the other thread is probably wanting that one too. + // no lock - published snapshots are single-thread + + // TODO: Instead of clearing, we could hold this value in another var in case the above call to GetElements() Or potentially + // a new TryGetElements() fails (since we might be shutting down). In that case we can use the old value. + _elements?.Dispose(); _elements = null; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a866297d72..2439633005 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -190,10 +190,15 @@ namespace Umbraco.Web.PublishedCache.NuCache /// private void MainDomRelease() { + _logger.Debug("Releasing from MainDom..."); + lock (_storesLock) { + _logger.Debug("Releasing content store..."); _contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localContentDb = null; + + _logger.Debug("Releasing media store..."); _mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localMediaDb = null; @@ -663,10 +668,20 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } + // TODO: These resync's are a problem, they cause deadlocks because when this is called, it's generally called within a writelock + // and then we clear out the snapshot and then if there's some event handler that needs the content cache, it tries to re-get it + // which first tries locking on the _storesLock which may have already been acquired and in this case we deadlock because + // we're still holding the write lock. + // We resync at the end of a ScopedWriteLock + // BUT if we don't resync here then the current snapshot is out of date for any event handlers that wish to use the most up to date + // data... + // so we need to figure out how to deal with the _storesLock + if (draftChanged || publishedChanged) ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } + // Calling this method means we have a lock on the contentStore (i.e. GetScopedWriteLock) private void NotifyLocked(IEnumerable payloads, out bool draftChanged, out bool publishedChanged) { publishedChanged = false; @@ -676,7 +691,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // content (and content types) are read-locked while reading content // contentStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - // contentStore is write-locked during changes + // contentStore is write-locked during changes - see note above, calls to this method are wrapped in contentStore.GetScopedWriteLock foreach (var payload in payloads) { @@ -1131,6 +1146,12 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentStore.Snapshot contentSnap, mediaSnap; SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; + + // TODO: Idea... TryGetElements? Might check if we are shutting down and return false and callers to this could handle it? + // Else does a readerwriterlockslim work here? (i don't think so) + // Else we have 2x locks, one for startup/shutdown, the other for getting elements and then we can maybe do a Monitor.Try? + // That is sort of the same as a TryGetElements + lock (_storesLock) { var scopeContext = _scopeProvider.Context; @@ -1156,6 +1177,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // elements // just need to make sure nothing gets elements in another enlisted action... so using // a MaxValue to make sure this one runs last, and it should be ok + // ... else there is potential to deadlock since this would recursively go back into trying + // lock on _storesLock but another thread may have already tried that scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt b/src/Umbraco.Web/PublishedCache/NuCache/readme.md similarity index 70% rename from src/Umbraco.Web/PublishedCache/NuCache/notes.txt rename to src/Umbraco.Web/PublishedCache/NuCache/readme.md index ff2d8dd48b..c8e8a363e9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt +++ b/src/Umbraco.Web/PublishedCache/NuCache/readme.md @@ -1,10 +1,6 @@ NuCache Documentation ====================== -NOTE - RENAMING -Facade = PublishedSnapshot -and everything needs to be renamed accordingly - HOW IT WORKS ------------- @@ -22,12 +18,12 @@ When reading the cache, we read views up the chain until we find a value (which null) for the given id, and finally we read the store itself. -The FacadeService manages a ContentStore for content, and another for media. -When a Facade is created, the FacadeService gets ContentView objects from the stores. +The PublishedSnapshotService manages a ContentStore for content, and another for media. +When a PublishedSnapshot is created, the PublishedSnapshotService gets ContentView objects from the stores. Views provide an immutable snapshot of the content and media. -When the FacadeService is notified of changes, it notifies the stores. -Then it resync the current Facade, so that it requires new views, etc. +When the PublishedSnapshotService is notified of changes, it notifies the stores. +Then it resync the current PublishedSnapshot, so that it requires new views, etc. Whenever a content, media or member is modified or removed, the cmsContentNu table is updated with a json dictionary of alias => property value, so that a content, @@ -50,32 +46,32 @@ Each ContentStore has a _freezeLock object used to protect the 'Frozen' state of the store. It's a disposable object that releases the lock when disposed, so usage would be: using (store.Frozen) { ... }. -The FacadeService has a _storesLock object used to guarantee atomic access to the +The PublishedSnapshotService has a _storesLock object used to guarantee atomic access to the set of content, media stores. CACHE ------ -For each set of views, the FacadeService creates a SnapshotCache. So a SnapshotCache +For each set of views, the PublishedSnapshotService creates a SnapshotCache. So a SnapshotCache is valid until anything changes in the content or media trees. In other words, things that go in the SnapshotCache stay until a content or media is modified. -For each Facade, the FacadeService creates a FacadeCache. So a FacadeCache is valid -for the duration of the Facade (usually, the request). In other words, things that go -in the FacadeCache stay (and are visible to) for the duration of the request only. +For each PublishedSnapshot, the PublishedSnapshotService creates a PublishedSnapshotCache. So a PublishedSnapshotCache is valid +for the duration of the PublishedSnapshot (usually, the request). In other words, things that go +in the PublishedSnapshotCache stay (and are visible to) for the duration of the request only. -The FacadeService defines a static constant FullCacheWhenPreviewing, that defines +The PublishedSnapshotService defines a static constant FullCacheWhenPreviewing, that defines how caches operate when previewing: - when true, the caches in preview mode work normally. -- when false, everything that would go to the SnapshotCache goes to the FacadeCache. +- when false, everything that would go to the SnapshotCache goes to the PublishedSnapshotCache. At the moment it is true in the code, which means that eg converted values for previewed content will go in the SnapshotCache. Makes for faster preview, but uses more memory on the long term... would need some benchmarking to figure out what is best. -Members only live for the duration of the Facade. So, for members SnapshotCache is -never used, and everything goes to the FacadeCache. +Members only live for the duration of the PublishedSnapshot. So, for members SnapshotCache is +never used, and everything goes to the PublishedSnapshotCache. All cache keys are computed in the CacheKeys static class. @@ -85,15 +81,15 @@ TESTS For testing purposes the following mechanisms exist: -The Facade type has a static Current property that is used to obtain the 'current' -facade in many places, going through the PublishedCachesServiceResolver to get the -current service, and asking the current service for the current facade, which by +The PublishedSnapshot type has a static Current property that is used to obtain the 'current' +PublishedSnapshot in many places, going through the PublishedCachesServiceResolver to get the +current service, and asking the current service for the current PublishedSnapshot, which by default relies on UmbracoContext. For test purposes, it is possible to override the -entire mechanism by defining Facade.GetCurrentFacadeFunc which should return a facade. +entire mechanism by defining PublishedSnapshot.GetCurrentPublishedSnapshotFunc which should return a PublishedSnapshot. A PublishedContent keeps only id-references to its parent and children, and needs a way to retrieve the actual objects from the cache - which depends on the current -facade. It is possible to override the entire mechanism by defining PublishedContent. +PublishedSnapshot. It is possible to override the entire mechanism by defining PublishedContent. GetContentByIdFunc or .GetMediaByIdFunc. Setting these functions must be done before Resolution is frozen. @@ -110,7 +106,7 @@ possible to support detached contents & properties, even those that do not have int id, but again this should be refactored entirely anyway. Not doing any row-version checks (see XmlStore) when reloading from database, though it -is maintained in the database. Two FIXME in FacadeService. Should we do it? +is maintained in the database. Two FIXME in PublishedSnapshotService. Should we do it? There is no on-disk cache at all so everything is reloaded from the cmsContentNu table when the site restarts. This is pretty fast, but we should experiment with solutions to @@ -121,4 +117,4 @@ PublishedMember exposes properties that IPublishedContent does not, and that are to be lost soon as the member is wrapped (content set, model...) - so we probably need some sort of IPublishedMember. -/ \ No newline at end of file +/ diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5d29e53d4a..861b95691b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1216,7 +1216,7 @@ - + @@ -1279,4 +1279,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index ad8dcb7e8b..f8ee238da7 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" ? (IMainDomLock)new SqlMainDomLock(logger) - : new MainDomSemaphoreLock(); + : new MainDomSemaphoreLock(logger); var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); From d33928f4259b01e4045a46adf92c2356d7297406 Mon Sep 17 00:00:00 2001 From: abi Date: Fri, 3 Jan 2020 01:43:37 +0100 Subject: [PATCH 162/610] Correct project references --- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0980b1b3d6..99eadef2bd 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -249,8 +249,8 @@ - - + + From 3d8e9a78e3da9bd8d64abd993f69c1374991c20f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 12:39:17 +1100 Subject: [PATCH 163/610] Fixes deadlock --- .../NuCache/PublishedSnapshotService.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 2439633005..97e546e603 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; @@ -54,6 +55,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore _mediaStore; private readonly SnapDictionary _domainStore; private readonly object _storesLock = new object(); + private readonly object _elementsLock = new object(); private BPlusTree _localContentDb; private BPlusTree _localMediaDb; @@ -143,7 +145,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _domainStore = new SnapDictionary(); - LoadCachesOnStartup(); + LoadCachesOnStartup(); } Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; @@ -171,7 +173,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var path = GetLocalFilesPath(); var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); - + _localContentDbExists = File.Exists(localContentDbPath); _localMediaDbExists = File.Exists(localMediaDbPath); @@ -221,7 +223,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); if (!okContent) - _logger.Warn("Loading content from local db raised warnings, will reload from database."); + _logger.Warn("Loading content from local db raised warnings, will reload from database."); } if (_localMediaDbExists) @@ -1147,12 +1149,12 @@ namespace Umbraco.Web.PublishedCache.NuCache SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; - // TODO: Idea... TryGetElements? Might check if we are shutting down and return false and callers to this could handle it? - // Else does a readerwriterlockslim work here? (i don't think so) - // Else we have 2x locks, one for startup/shutdown, the other for getting elements and then we can maybe do a Monitor.Try? - // That is sort of the same as a TryGetElements + // Here we are reading/writing to shared objects so we need to lock (can't be _storesLock which manages the actual nucache files + // and would result in a deadlock). Even though we are locking around underlying readlocks (within CreateSnapshot) it's because + // we need to ensure that the result of contentSnap.Gen (etc) and the re-assignment of these values and _elements cache + // are done atomically. - lock (_storesLock) + lock (_elementsLock) { var scopeContext = _scopeProvider.Context; @@ -1177,14 +1179,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // elements // just need to make sure nothing gets elements in another enlisted action... so using // a MaxValue to make sure this one runs last, and it should be ok - // ... else there is potential to deadlock since this would recursively go back into trying - // lock on _storesLock but another thread may have already tried that + scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); }, int.MaxValue); } + // create a new snapshot cache if snapshots are different gens if (contentSnap.Gen != _contentGen || mediaSnap.Gen != _mediaGen || domainSnap.Gen != _domainGen || _elementsCache == null) { @@ -1351,7 +1353,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { //culture changed on an existing language var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); - if(cultureChanged) + if (cultureChanged) { RebuildContentDbCache(); } From e6b333a3ec2f5234a081bb7f1ab821e42f332a3e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 12:39:56 +1100 Subject: [PATCH 164/610] Changes readlocks to normal locks, no need to have extra objects allocated and complex code. --- .../PublishedCache/NuCache/ContentStore.cs | 72 +++++-------------- .../PublishedCache/NuCache/SnapDictionary.cs | 58 ++++----------- 2 files changed, 32 insertions(+), 98 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 81229af9e1..450dc2f283 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -20,6 +20,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this class is an extended version of SnapDictionary // most of the snapshots management code, etc is an exact copy // SnapDictionary has unit tests to ensure it all works correctly + // For locking information, see SnapDictionary private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IVariationContextAccessor _variationContextAccessor; @@ -79,11 +80,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; @@ -121,15 +117,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { - // TODO: We are in a deadlock here somehow?? - Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // see SnapDictionary try { } finally @@ -146,26 +137,16 @@ namespace Umbraco.Web.PublishedCache.NuCache _nextGen = true; } } - } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); + } } private void Release(WriteLockInfo lockInfo, bool commit = true) { if (commit == false) { - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); + // see SnapDictionary try { } finally { @@ -173,10 +154,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _liveGen -= 1; } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } Rollback(_contentNodes); RollbackRoot(); @@ -207,11 +184,6 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) - { - if (lockInfo.Taken) Monitor.Exit(_rlocko); - } - private void RollbackRoot() { if (_root.Gen <= _liveGen) return; @@ -1248,13 +1220,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - // TODO: This would be much simpler with just a lock(_rlocko) { } - // in this case I see no reason why we are using this syntax?! - Lock(lockInfo); - // if no next generation is required, and we already have one, // use it and create a new snapshot if (_nextGen == false && _genObj != null) @@ -1306,10 +1273,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Snapshot LiveSnapshot => new Snapshot(this, _liveGen @@ -1427,18 +1390,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // TODO: This is never used? Should it be? - - public async Task WaitForPendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - if (task != null) - await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public async Task WaitForPendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // if (task != null) + // await task; + //} public long GenCount => _genObjs.Count; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index 9671949ff0..e0ad26eb81 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -22,6 +22,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // This class is optimized for many readers, few writers // Readers are lock-free + // NOTE - we used to lock _rlocko the long hand way with Monitor.Enter(_rlocko, ref lockTaken) but this has + // been replaced with a normal c# lock because that's exactly how the normal c# lock works, + // see https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/ + // for the readlock, there's no reason here to use the long hand way. + private readonly ConcurrentDictionary> _items; private readonly ConcurrentQueue _genObjs; private GenObj _genObj; @@ -71,16 +76,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // are all ignored - Release is private and meant to be invoked with 'commit' being false only // only on the outermost lock (by SnapDictionaryWriter) - // using (...) {} for locking is prone to nasty leaks in case of weird exceptions - // such as thread-abort or out-of-memory, but let's not worry about it now + // side note - using (...) {} for locking is prone to nasty leaks in case of weird exceptions + // such as thread-abort or out-of-memory, which is why we've moved away from the old using wrapper we had on locking. private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; @@ -121,18 +121,16 @@ namespace Umbraco.Web.PublishedCache.NuCache { Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // assume everything in finally runs atomically // http://stackoverflow.com/questions/18501678/can-this-unexpected-behavior-of-prepareconstrainedregions-and-thread-abort-be-ex // http://joeduffyblog.com/2005/03/18/atomicity-and-asynchronous-exception-failures/ // http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/ // http://chabster.blogspot.fr/2013/12/readerwriterlockslim-fails-on-dual.html //RuntimeHelpers.PrepareConstrainedRegions(); - try { } finally + try { } + finally { // increment the lock count, and register that this lock is counting _wlocked++; @@ -149,15 +147,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); } private void Release(WriteLockInfo lockInfo, bool commit = true) @@ -168,22 +157,17 @@ namespace Umbraco.Web.PublishedCache.NuCache if (commit == false) { - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - try { } finally + try { } + finally { // forget about the temp. liveGen _nextGen = false; _liveGen -= 1; } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - + foreach (var item in _items) { var link = item.Value; @@ -202,11 +186,6 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) - { - if (lockInfo.Taken) Monitor.Exit(_rlocko); - } - #endregion #region Set, Clear, Get, Has @@ -347,11 +326,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - Lock(lockInfo); - // if no next generation is required, and we already have a gen object, // use it to create a new snapshot if (_nextGen == false && _genObj != null) @@ -398,10 +374,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Task CollectAsync() From 8e3b3c83261e5272fefb219aaceaa645775ad164 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 13:21:49 +1100 Subject: [PATCH 165/610] Changes methods that should already be locked to check that they are and changes their names/adds docs --- .../PublishedCache/NuCache/ContentStore.cs | 488 +++++++++--------- .../NuCache/PublishedSnapshotService.cs | 30 +- 2 files changed, 270 insertions(+), 248 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 450dc2f283..82fae0d32a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -115,6 +115,12 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { Monitor.Enter(_wlocko, ref lockInfo.Taken); @@ -323,34 +329,36 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public void SetAllContentTypes(IEnumerable types) + /// + /// Updates/sets data for all content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void SetAllContentTypesLocked(IEnumerable types) { - // TODO: There should be NO lock here! All calls made to this are wrapped in GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); - try + _logger.Debug("SetAllContentTypes"); + + // clear all existing content types + ClearLocked(_contentTypesById); + ClearLocked(_contentTypesByAlias); + + // set all new content types + foreach (var type in types) { - _logger.Debug("SetAllContentTypes"); - //Lock(lockInfo); - - // clear all existing content types - ClearLocked(_contentTypesById); - ClearLocked(_contentTypesByAlias); - - // set all new content types - foreach (var type in types) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - } - finally - { - //Release(lockInfo); + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! } public void UpdateContentTypes(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) @@ -540,8 +548,22 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - public bool Set(ContentNodeKit kit) + /// + /// Sets the data for a + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetLocked(ContentNodeKit kit) { + EnsureLocked(); + // ReSharper disable LocalizableElement if (kit.IsEmpty) throw new ArgumentException("Kit is empty.", nameof(kit)); @@ -551,58 +573,47 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); - // TODO: There should be NO locks here, all calls made to this are done within GetScopedWriteLock - //var lockInfo = new WriteLockInfo(); - try + // get existing + _contentNodes.TryGetValue(kit.Node.Id, out var link); + var existing = link?.Value; + + if (!BuildKit(kit, out var parent)) + return false; + + // moving? + var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; + + // manage children + if (existing != null) { - //Lock(lockInfo); - - // get existing - _contentNodes.TryGetValue(kit.Node.Id, out var link); - var existing = link?.Value; - - if (!BuildKit(kit, out var parent)) - return false; - - // moving? - var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; - - // manage children - if (existing != null) - { - kit.Node.FirstChildContentId = existing.FirstChildContentId; - kit.Node.LastChildContentId = existing.LastChildContentId; - } - - // set - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - - // manage the tree - if (existing == null) - { - // new, add to parent - AddTreeNodeLocked(kit.Node, parent); - } - else if (moving || existing.SortOrder != kit.Node.SortOrder) - { - // moved, remove existing from its parent, add content to its parent - RemoveTreeNodeLocked(existing); - AddTreeNodeLocked(kit.Node); - } - else - { - // replacing existing, handle siblings - kit.Node.NextSiblingContentId = existing.NextSiblingContentId; - kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; - } - - _xmap[kit.Node.Uid] = kit.Node.Id; + kit.Node.FirstChildContentId = existing.FirstChildContentId; + kit.Node.LastChildContentId = existing.LastChildContentId; } - finally + + // set + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + + // manage the tree + if (existing == null) { - //Release(lockInfo); + // new, add to parent + AddTreeNodeLocked(kit.Node, parent); } + else if (moving || existing.SortOrder != kit.Node.SortOrder) + { + // moved, remove existing from its parent, add content to its parent + RemoveTreeNodeLocked(existing); + AddTreeNodeLocked(kit.Node); + } + else + { + // replacing existing, handle siblings + kit.Node.NextSiblingContentId = existing.NextSiblingContentId; + kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; + } + + _xmap[kit.Node.Uid] = kit.Node.Id; return true; } @@ -624,211 +635,222 @@ namespace Umbraco.Web.PublishedCache.NuCache /// True if the data is coming from the database (not the local cache db) /// /// + /// /// This requires that the collection is sorted by Level + ParentId + Sort Order. /// This should be used only on a site startup as the first generations. /// This CANNOT be used after startup since it bypasses all checks for Generations. + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// /// - internal bool SetAllFastSorted(IEnumerable kits, bool fromDb) + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllFastSortedLocked(IEnumerable kits, bool fromDb) { + EnsureLocked(); - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + _logger.Debug("SetAllFastSorted"); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // The name of the game here is to populate each kit's + // FirstChildContentId + // LastChildContentId + // NextSiblingContentId + // PreviousSiblingContentId + + ContentNode previousNode = null; + ContentNode parent = null; + + foreach (var kit in kits) { - _logger.Debug("SetAllFastSorted"); - - //Lock(lockInfo); - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // The name of the game here is to populate each kit's - // FirstChildContentId - // LastChildContentId - // NextSiblingContentId - // PreviousSiblingContentId - - ContentNode previousNode = null; - ContentNode parent = null; - - foreach (var kit in kits) + if (!BuildKit(kit, out var parentLink)) { - if (!BuildKit(kit, out var parentLink)) - { - ok = false; - continue; // skip that one - } - - var thisNode = kit.Node; - - if (parent == null) - { - // first parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - } - else if (parent.Id != parentLink.Value.Id) - { - // new parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - previousNode = null; // there is no previous sibling - } - - _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); - SetValueLocked(_contentNodes, thisNode.Id, thisNode); - - // if we are initializing from the database source ensure the local db is updated - if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); - - // this node is always the last child - parent.LastChildContentId = thisNode.Id; - - // wire previous node as previous sibling - if (previousNode != null) - { - previousNode.NextSiblingContentId = thisNode.Id; - thisNode.PreviousSiblingContentId = previousNode.Id; - } - - // this node becomes the previous node - previousNode = thisNode; - - _xmap[kit.Node.Uid] = kit.Node.Id; + ok = false; + continue; // skip that one } - } - finally - { - //Release(lockInfo); + + var thisNode = kit.Node; + + if (parent == null) + { + // first parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + } + else if (parent.Id != parentLink.Value.Id) + { + // new parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + previousNode = null; // there is no previous sibling + } + + _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + SetValueLocked(_contentNodes, thisNode.Id, thisNode); + + // if we are initializing from the database source ensure the local db is updated + if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); + + // this node is always the last child + parent.LastChildContentId = thisNode.Id; + + // wire previous node as previous sibling + if (previousNode != null) + { + previousNode.NextSiblingContentId = thisNode.Id; + thisNode.PreviousSiblingContentId = previousNode.Id; + } + + // this node becomes the previous node + previousNode = thisNode; + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - public bool SetAll(IEnumerable kits) + /// + /// Set all data for a collection of + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllLocked(IEnumerable kits) { - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + _logger.Debug("SetAll"); + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // do NOT clear types else they are gone! + //ClearLocked(_contentTypesById); + //ClearLocked(_contentTypesByAlias); + + foreach (var kit in kits) { - _logger.Debug("SetAll"); - - //Lock(lockInfo); - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // do NOT clear types else they are gone! - //ClearLocked(_contentTypesById); - //ClearLocked(_contentTypesByAlias); - - foreach (var kit in kits) + if (!BuildKit(kit, out var parent)) { - if (!BuildKit(kit, out var parent)) - { - ok = false; - continue; // skip that one - } - _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _xmap[kit.Node.Uid] = kit.Node.Id; + ok = false; + continue; // skip that one } - } - finally - { - //Release(lockInfo); + _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - // IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER - public bool SetBranch(int rootContentId, IEnumerable kits) + /// + /// Sets data for a branch of + /// + /// + /// + /// + /// + /// + /// IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetBranchLocked(int rootContentId, IEnumerable kits) { - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + _logger.Debug("SetBranch"); + + // get existing + _contentNodes.TryGetValue(rootContentId, out var link); + var existing = link?.Value; + + // clear + if (existing != null) { - _logger.Debug("SetBranch"); - - //Lock(lockInfo); - - // get existing - _contentNodes.TryGetValue(rootContentId, out var link); - var existing = link?.Value; - - // clear - if (existing != null) - { - //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen - ClearBranchLocked(existing); - //TODO: This removes the current GEN from the tree - do we really want to do that? - RemoveTreeNodeLocked(existing); - } - - // now add them all back - foreach (var kit in kits) - { - if (!BuildKit(kit, out var parent)) - { - ok = false; - continue; // skip that one - } - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _xmap[kit.Node.Uid] = kit.Node.Id; - } + //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen + ClearBranchLocked(existing); + //TODO: This removes the current GEN from the tree - do we really want to do that? + RemoveTreeNodeLocked(existing); } - finally + + // now add them all back + foreach (var kit in kits) { - //Release(lockInfo); + if (!BuildKit(kit, out var parent)) + { + ok = false; + continue; // skip that one + } + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - public bool Clear(int id) + /// + /// Clears data for a given node id + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool ClearLocked(int id) { - // TODO: There should be NO locks here! All calls to this are made within GetScopedWriteLock - //var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("Clear"); + EnsureLocked(); - //Lock(lockInfo); + _logger.Debug("Clear"); - // try to find the content - // if it is not there, nothing to do - _contentNodes.TryGetValue(id, out var link); // else null - if (link?.Value == null) return false; + // try to find the content + // if it is not there, nothing to do + _contentNodes.TryGetValue(id, out var link); // else null + if (link?.Value == null) return false; - var content = link.Value; - _logger.Debug("Clear content ID: {ContentId}", content.Id); + var content = link.Value; + _logger.Debug("Clear content ID: {ContentId}", content.Id); - // clear the entire branch - ClearBranchLocked(content); + // clear the entire branch + ClearBranchLocked(content); - // manage the tree - RemoveTreeNodeLocked(content); + // manage the tree + RemoveTreeNodeLocked(content); - return true; - } - finally - { - //Release(lockInfo); - } + return true; } private void ClearBranchLocked(int id) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 97e546e603..b761fe0fa4 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -384,7 +384,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.SetAllContentTypes(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes); using (_logger.TraceDuration("Loading content from database")) { @@ -395,7 +395,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder var kits = _dataSource.GetAllContentSources(scope); - return onStartup ? _contentStore.SetAllFastSorted(kits, true) : _contentStore.SetAll(kits); + return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits); } } @@ -403,7 +403,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.SetAllContentTypes(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes); using (_logger.TraceDuration("Loading content from local cache file")) { @@ -455,7 +455,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypes(mediaTypes); + _mediaStore.SetAllContentTypesLocked(mediaTypes); using (_logger.TraceDuration("Loading media from database")) { @@ -467,7 +467,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading media from database..."); // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder var kits = _dataSource.GetAllMediaSources(scope); - return onStartup ? _mediaStore.SetAllFastSorted(kits, true) : _mediaStore.SetAll(kits); + return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits); } } @@ -475,7 +475,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypes(mediaTypes); + _mediaStore.SetAllContentTypesLocked(mediaTypes); using (_logger.TraceDuration("Loading media from local cache file")) { @@ -516,7 +516,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return false; } - return onStartup ? store.SetAllFastSorted(kits, false) : store.SetAll(kits); + return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits); } // keep these around - might be useful @@ -713,7 +713,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_contentStore.Clear(payload.Id)) + if (_contentStore.ClearLocked(payload.Id)) draftChanged = publishedChanged = true; continue; } @@ -736,7 +736,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchContentSources(scope, capture.Id); - _contentStore.SetBranch(capture.Id, kits); + _contentStore.SetBranchLocked(capture.Id, kits); } else { @@ -744,11 +744,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetContentSource(scope, capture.Id); if (kit.IsEmpty) { - _contentStore.Clear(capture.Id); + _contentStore.ClearLocked(capture.Id); } else { - _contentStore.Set(kit); + _contentStore.SetLocked(kit); } } @@ -806,7 +806,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_mediaStore.Clear(payload.Id)) + if (_mediaStore.ClearLocked(payload.Id)) anythingChanged = true; continue; } @@ -829,7 +829,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); - _mediaStore.SetBranch(capture.Id, kits); + _mediaStore.SetBranchLocked(capture.Id, kits); } else { @@ -837,11 +837,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetMediaSource(scope, capture.Id); if (kit.IsEmpty) { - _mediaStore.Clear(capture.Id); + _mediaStore.ClearLocked(capture.Id); } else { - _mediaStore.Set(kit); + _mediaStore.SetLocked(kit); } } From 243e76b3cc3e5fccdf9c021354d4a2d88b6bb707 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 15:04:39 +1100 Subject: [PATCH 166/610] Removes ability to have recursive locks in SnapDictionary, changes logic to require locking around the methods just like ContentStore, updates tests --- .../Cache/SnapDictionaryTests.cs | 126 ++++++++------ .../PublishedCache/NuCache/ContentStore.cs | 104 ++++++------ .../NuCache/PublishedSnapshotService.cs | 14 +- .../PublishedCache/NuCache/SnapDictionary.cs | 154 ++++++++---------- 4 files changed, 204 insertions(+), 194 deletions(-) diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index b435af9e77..86426d196c 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -9,6 +9,47 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.Cache { + /// + /// Used for tests + /// + public static class SnapDictionaryExtensions + { + internal static void Set(this SnapDictionary d, TKey key, TValue value) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.SetLocked(key, value); + } + } + + internal static void Clear(this SnapDictionary d) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(); + } + } + + internal static void Clear(this SnapDictionary d, TKey key) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(key); + } + } + + private static IScopeProvider GetScopeProvider() + { + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.Context).Returns(() => null); + return scopeProvider; + } + } + [TestFixture] public class SnapDictionaryTests { @@ -223,7 +264,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + // gen 1 d.Set(1, "one"); Assert.AreEqual(1, d.Test.GetValues(1).Length); @@ -321,7 +362,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + Assert.AreEqual(0, d.Test.GetValues(1).Length); // gen 1 @@ -416,7 +457,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + // gen 1 d.Set(1, "one"); Assert.AreEqual(1, d.Test.GetValues(1).Length); @@ -578,7 +619,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + d.Set(1, "one"); d.Set(2, "two"); @@ -689,7 +730,7 @@ namespace Umbraco.Tests.Cache { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.AreEqual(3, d.Test.GetValues(1).Length); Assert.AreEqual(3, d.Test.LiveGen); @@ -727,31 +768,25 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetScopedWriteLock(scopeProvider)) + Assert.Throws(() => { - Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(2, t.WLocked); - Assert.IsTrue(t.NextGen); - - Assert.AreNotSame(w1, w2); // get a new writer each time - - d.Set(1, "one"); - - Assert.AreEqual(0, d.CreateSnapshot().Gen); - } + using (var w2 = d.GetScopedWriteLock(scopeProvider)) + { + } + }); Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); Assert.AreEqual(0, d.CreateSnapshot().Gen); } Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(0, t.WLocked); + Assert.IsFalse(t.IsLocked); Assert.IsTrue(t.NextGen); Assert.AreEqual(1, d.CreateSnapshot().Gen); @@ -772,11 +807,14 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider)) { + // This one is interesting, although we don't allow recursive locks, since this is + // using the same ScopeContext/key, the lock acquisition is only done once + using (var w2 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreSame(w1, w2); - d.Set(1, "one"); + d.SetLocked(1, "one"); } } } @@ -797,19 +835,16 @@ namespace Umbraco.Tests.Cache using (var w1 = d.GetScopedWriteLock(scopeProvider1)) { Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetScopedWriteLock(scopeProvider2)) + Assert.Throws(() => { - Assert.AreEqual(1, t.LiveGen); - Assert.AreEqual(2, t.WLocked); - Assert.IsTrue(t.NextGen); + using (var w2 = d.GetScopedWriteLock(scopeProvider2)) + { + } + }); - Assert.AreNotSame(w1, w2); - - d.Set(1, "one"); - } } } @@ -848,13 +883,13 @@ namespace Umbraco.Tests.Cache Assert.IsFalse(d.Test.NextGen); Assert.AreEqual("uno", s2.Get(1)); - var scopeProvider = GetScopeProvider(); + var scopeProvider = GetScopeProvider(); using (d.GetScopedWriteLock(scopeProvider)) { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.AreEqual(3, d.Test.GetValues(1).Length); Assert.AreEqual(3, d.Test.LiveGen); @@ -881,6 +916,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; + // gen 1 d.Set(1, "one"); @@ -894,12 +930,11 @@ namespace Umbraco.Tests.Cache Assert.AreEqual("uno", s2.Get(1)); var scopeProvider = GetScopeProvider(); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -934,12 +969,11 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -967,7 +1001,7 @@ namespace Umbraco.Tests.Cache var d = new SnapDictionary(); var t = d.Test; t.CollectAuto = false; - + // gen 1 d.Set(1, "one"); var s1 = d.CreateSnapshot(); @@ -984,12 +1018,11 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release - d.Set(1, "ein"); + d.SetLocked(1, "ein"); var s3 = d.CreateSnapshot(); Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); @@ -997,7 +1030,7 @@ namespace Umbraco.Tests.Cache // we made some changes, so a next gen is required Assert.AreEqual(3, t.LiveGen); Assert.IsTrue(t.NextGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); // but live snapshot contains changes var ls = t.LiveSnapshot; @@ -1008,7 +1041,7 @@ namespace Umbraco.Tests.Cache // nothing is committed until scope exits Assert.AreEqual(3, t.LiveGen); Assert.IsTrue(t.NextGen); - Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.IsLocked); // no changes until exit var s4 = d.CreateSnapshot(); @@ -1020,7 +1053,7 @@ namespace Umbraco.Tests.Cache // now things have changed Assert.AreEqual(2, t.LiveGen); Assert.IsFalse(t.NextGen); - Assert.AreEqual(0, t.WLocked); + Assert.IsFalse(t.IsLocked); // no changes since not completed var s5 = d.CreateSnapshot(); @@ -1097,9 +1130,10 @@ namespace Umbraco.Tests.Cache // writer is scope contextual and scoped // when disposed, nothing happens // when the context exists, the writer is released + using (d.GetScopedWriteLock(scopeProvider)) { - d.Set(1, "ein"); + d.SetLocked(1, "ein"); Assert.IsTrue(d.Test.NextGen); Assert.AreEqual(3, d.Test.LiveGen); Assert.IsNotNull(d.Test.GenObj); @@ -1107,7 +1141,7 @@ namespace Umbraco.Tests.Cache } // writer has not released - Assert.AreEqual(1, d.Test.WLocked); + Assert.IsTrue(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); @@ -1118,7 +1152,7 @@ namespace Umbraco.Tests.Cache // panic! var s2 = d.CreateSnapshot(); - Assert.AreEqual(1, d.Test.WLocked); + Assert.IsTrue(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); @@ -1127,7 +1161,7 @@ namespace Umbraco.Tests.Cache // release writer scopeContext.ScopeExit(true); - Assert.AreEqual(0, d.Test.WLocked); + Assert.IsFalse(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(2, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); @@ -1135,7 +1169,7 @@ namespace Umbraco.Tests.Cache var s3 = d.CreateSnapshot(); - Assert.AreEqual(0, d.Test.WLocked); + Assert.IsFalse(d.Test.IsLocked); Assert.IsNotNull(d.Test.GenObj); Assert.AreEqual(3, d.Test.GenObj.Gen); Assert.AreEqual(3, d.Test.LiveGen); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 82fae0d32a..17eb2b6457 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -39,7 +39,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; private List> _wchanges; // TODO: collection trigger (ok for now) @@ -83,7 +82,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -111,7 +109,6 @@ namespace Umbraco.Web.PublishedCache.NuCache // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { - _logger.Debug("GetScopedWriteLock"); return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } @@ -123,6 +120,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); lock(_rlocko) @@ -131,9 +131,7 @@ namespace Umbraco.Web.PublishedCache.NuCache try { } finally { - _wlocked++; - lockInfo.Count = true; - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -179,12 +177,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _localDb.Commit(); } - // TODO: Shouldn't this be in a finally block? - // TODO: Shouldn't this be decremented after we exit?? - // TODO: Shouldn't the locked flag never exceed 1? - if (lockInfo.Count) - _wlocked--; - // TODO: Shouldn't this be in a finally block? if (lockInfo.Taken) Monitor.Exit(_wlocko); @@ -220,22 +212,18 @@ namespace Umbraco.Web.PublishedCache.NuCache public void ReleaseLocalDb() { - _logger.Info("Releasing DB..."); var lockInfo = new WriteLockInfo(); try { try { - // Trying to lock could throw exceptions so always make sure to clean up. - _logger.Info("Trying to lock before releasing DB (lock count = {LockCount}) ...", _wlocked); - + // Trying to lock could throw exceptions so always make sure to clean up. Lock(lockInfo); } finally { try { - _logger.Info("Disposing local DB..."); _localDb?.Dispose(); } catch (Exception ex) @@ -275,57 +263,61 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public void NewContentTypes(IEnumerable types) + /// + /// Sets data for new content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void NewContentTypesLocked(IEnumerable types) { - var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("NewContentTypes"); - Lock(lockInfo); + EnsureLocked(); - foreach (var type in types) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - } - finally + foreach (var type in types) { - Release(lockInfo); + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } } - public void UpdateContentTypes(IEnumerable types) + /// + /// Sets data for updated content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IEnumerable types) { //nothing to do if this is empty, no need to lock/allocate/iterate/etc... if (!types.Any()) return; - var lockInfo = new WriteLockInfo(); - try + EnsureLocked(); + + var index = types.ToDictionary(x => x.Id, x => x); + + foreach (var type in index.Values) { - _logger.Debug("UpdateContentTypes"); - Lock(lockInfo); - - var index = types.ToDictionary(x => x.Id, x => x); - - foreach (var type in index.Values) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); - } + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } - finally + + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); } } @@ -1256,7 +1248,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen ref // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index b761fe0fa4..249eb882f9 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -622,7 +622,7 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard))) { - _domainStore.Set(domain.Id, domain); + _domainStore.SetLocked(domain.Id, domain); } } @@ -980,7 +980,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } break; case DomainChangeTypes.Remove: - _domainStore.Clear(payload.Id); + _domainStore.ClearLocked(payload.Id); break; case DomainChangeTypes.Refresh: var domain = _serviceContext.DomainService.GetById(payload.Id); @@ -988,7 +988,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (domain.RootContentId.HasValue == false) continue; // anomaly if (domain.LanguageIsoCode.IsNullOrWhiteSpace()) continue; // anomaly var culture = CultureInfo.GetCultureInfo(domain.LanguageIsoCode); - _domainStore.Set(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); + _domainStore.SetLocked(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); break; } } @@ -1075,9 +1075,9 @@ namespace Umbraco.Web.PublishedCache.NuCache _contentStore.UpdateContentTypes(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) - _contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); + _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); if (!newIds.IsCollectionEmpty()) - _contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); + _contentStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); scope.Complete(); } } @@ -1106,9 +1106,9 @@ namespace Umbraco.Web.PublishedCache.NuCache _mediaStore.UpdateContentTypes(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) - _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); + _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); if (!newIds.IsCollectionEmpty()) - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + _mediaStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); scope.Complete(); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index e0ad26eb81..c38940da25 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -35,7 +35,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; // minGenDelta to be adjusted // we may want to throttle collects even if delta is reached @@ -84,7 +83,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -92,8 +90,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); private readonly SnapDictionary _dictionary; - private int _released; - + public ScopedWriteLock(SnapDictionary dictionary, bool scoped) { _dictionary = dictionary; @@ -102,8 +99,6 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Release(bool completed) { - if (Interlocked.CompareExchange(ref _released, 1, 0) != 0) - return; _dictionary.Release(_lockinfo, completed); } } @@ -117,8 +112,17 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); lock(_rlocko) @@ -132,11 +136,7 @@ namespace Umbraco.Web.PublishedCache.NuCache try { } finally { - // increment the lock count, and register that this lock is counting - _wlocked++; - lockInfo.Count = true; - - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -181,8 +181,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // decrement the lock count, if counting, then exit the lock - if (lockInfo.Count) _wlocked--; + // TODO: Shouldn't this be in a finally block? Monitor.Exit(_wlocko); } @@ -198,75 +197,59 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - public void Set(TKey key, TValue value) + public void SetLocked(TKey key, TValue value) { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); + EnsureLocked(); - // this is safe only because we're write-locked - var link = GetHead(key); - if (link != null) + // this is safe only because we're write-locked + var link = GetHead(key); + if (link != null) + { + // already in the dict + if (link.Gen != _liveGen) { - // already in the dict - if (link.Gen != _liveGen) - { - // for an older gen - if value is different then insert a new - // link for the new gen, with the new value - if (link.Value != value) - _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); - } - else - { - // for the live gen - we can fix the live gen - and remove it - // if value is null and there's no next gen - if (value == null && link.Next == null) - _items.TryRemove(key, out link); - else - link.Value = value; - } + // for an older gen - if value is different then insert a new + // link for the new gen, with the new value + if (link.Value != value) + _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); } else { - _items.TryAdd(key, new LinkedNode(value, _liveGen)); - } - } - finally - { - Release(lockInfo); - } - } - - public void Clear(TKey key) - { - Set(key, null); - } - - public void Clear() - { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); - - // this is safe only because we're write-locked - foreach (var kvp in _items.Where(x => x.Value != null)) - { - if (kvp.Value.Gen < _liveGen) - { - var link = new LinkedNode(null, _liveGen, kvp.Value); - _items.TryUpdate(kvp.Key, link, kvp.Value); - } + // for the live gen - we can fix the live gen - and remove it + // if value is null and there's no next gen + if (value == null && link.Next == null) + _items.TryRemove(key, out link); else - { - kvp.Value.Value = null; - } + link.Value = value; } } - finally + else { - Release(lockInfo); + _items.TryAdd(key, new LinkedNode(value, _liveGen)); + } + } + + public void ClearLocked(TKey key) + { + SetLocked(key, null); + } + + public void ClearLocked() + { + EnsureLocked(); + + // this is safe only because we're write-locked + foreach (var kvp in _items.Where(x => x.Value != null)) + { + if (kvp.Value.Gen < _liveGen) + { + var link = new LinkedNode(null, _liveGen, kvp.Value); + _items.TryUpdate(kvp.Key, link, kvp.Value); + } + else + { + kvp.Value.Value = null; + } } } @@ -336,7 +319,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen object // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; @@ -468,17 +451,18 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public /*async*/ Task PendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - return task ?? Task.CompletedTask; - //if (task != null) - // await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public /*async*/ Task PendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // return task ?? Task.CompletedTask; + // //if (task != null) + // // await task; + //} public long GenCount => _genObjs.Count; @@ -503,7 +487,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public long LiveGen => _dict._liveGen; public long FloorGen => _dict._floorGen; public bool NextGen => _dict._nextGen; - public int WLocked => _dict._wlocked; + public bool IsLocked => Monitor.IsEntered(_dict._wlocko); public bool CollectAuto { From 7a129f890dc5b87094901d2ac3bbe953486be04b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 15:07:21 +1100 Subject: [PATCH 167/610] Changes MainDom testing timeout to be larger... but not 1.5 mins! --- src/Umbraco.Core/Runtime/MainDom.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 883b69b2a7..d7bd407015 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Runtime // actions to run before releasing the main domain private readonly List> _callbacks = new List>(); - private const int LockTimeoutMilliseconds = 10000; // 10 seconds + private const int LockTimeoutMilliseconds = 40000; // 40 seconds #endregion @@ -151,22 +151,20 @@ namespace Umbraco.Core.Runtime { _logger.Info("Cannot acquire (timeout)."); - // TODO: Previously we'd throw an exception and the appdomain would not start, what do we want to do? - - // return false; + // TODO: In previous versions we'd let a TimeoutException be thrown + // and the appdomain would not start. We have the opportunity to allow it to + // start without having MainDom? This would mean that it couldn't write + // to nucache/examine and would only be ok if this was a super short lived appdomain. + // maybe safer to just keep throwing in this case. throw new TimeoutException("Cannot acquire MainDom"); + // return false; } try { // Listen for the signal from another AppDomain coming online to release the lock - _mainDomLock.ListenAsync() - .ContinueWith(_ => - { - _logger.Debug("Signal heard from other appdomain."); - OnSignal("signal"); - }); + _mainDomLock.ListenAsync().ContinueWith(_ => OnSignal("signal")); } catch (OperationCanceledException ex) { From c2ac5e85311b39ee0d9c8734659a39fb6be55ab9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jan 2020 18:34:04 +1100 Subject: [PATCH 168/610] Fixes a couple more issues with recursive locks --- .../PublishedCache/NuCache/ContentStore.cs | 244 +++++++++--------- .../NuCache/PublishedSnapshotService.cs | 8 +- 2 files changed, 130 insertions(+), 122 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 17eb2b6457..8ce299932c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -14,7 +14,18 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Web.PublishedCache.NuCache { - // stores content + /// + /// Stores content in memory and persists it back to disk + /// + /// + /// + /// Methods in this class suffixed with the term "Locked" means that those methods can only be called within a WriteLock. A WriteLock + /// is acquired by the GetScopedWriteLock method. Locks are not allowed to be recursive. + /// + /// + /// This class's logic is based on the class but has been slightly modified to suit these purposes. + /// + /// internal class ContentStore { // this class is an extended version of SnapDictionary @@ -336,8 +347,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("SetAllContentTypes"); - // clear all existing content types ClearLocked(_contentTypesById); ClearLocked(_contentTypesByAlias); @@ -353,8 +362,23 @@ namespace Umbraco.Web.PublishedCache.NuCache // assuming we are going to SetAll content items! } - public void UpdateContentTypes(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) + /// + /// Updates/sets/removes data for content types + /// + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) { + EnsureLocked(); + var removedIdsA = removedIds ?? Array.Empty(); var refreshedTypesA = refreshedTypes ?? Array.Empty(); var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToList(); @@ -363,87 +387,82 @@ namespace Umbraco.Web.PublishedCache.NuCache if (kits.Count == 0 && refreshedIdsA.Count == 0 && removedIdsA.Count == 0) return; //exit - there is nothing to do here - var lockInfo = new WriteLockInfo(); - try + 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) { - _logger.Debug("UpdateContentTypes"); - Lock(lockInfo); - - 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; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); - if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); - } - - // 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 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); - } - - // perform update of content with refreshed content type - from the kits - // skip missing type, skip missing parents & un-buildable kits - what else could we do? - // kits are ordered by level, so ParentExists is ok here - var visited = new List(); - foreach (var kit in kits.Where(x => - refreshedIdsA.Contains(x.ContentTypeId) && - BuildKit(x, out _))) - { - // replacing the node: must preserve the parents - var node = GetHead(_contentNodes, kit.Node.Id)?.Value; - if (node != null) - kit.Node.FirstChildContentId = node.FirstChildContentId; - - 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); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); + if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); } - finally + + // 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) { - Release(lockInfo); + 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); + } + + // perform update of content with refreshed content type - from the kits + // skip missing type, skip missing parents & un-buildable kits - what else could we do? + // kits are ordered by level, so ParentExists is ok here + var visited = new List(); + foreach (var kit in kits.Where(x => + refreshedIdsA.Contains(x.ContentTypeId) && + BuildKit(x, out _))) + { + // replacing the node: must preserve the parents + var node = GetHead(_contentNodes, kit.Node.Id)?.Value; + if (node != null) + kit.Node.FirstChildContentId = node.FirstChildContentId; + + 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); } - public void UpdateDataTypes(IEnumerable dataTypeIds, Func getContentType) + /// + /// Updates data types + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) { - var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("UpdateDataTypes"); - Lock(lockInfo); - - var contentTypes = _contentTypesById + var contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && kvp.Value.Value.PropertyTypes.Any(p => dataTypeIds.Contains(p.DataType.Id))) @@ -452,37 +471,32 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x != null) // poof, gone, very unlikely and probably an anomaly .ToArray(); - var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); - var contentTypeNodes = new Dictionary>(); - foreach (var id in contentTypeIdsA) - contentTypeNodes[id] = new List(); - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) - contentTypeNodes[node.ContentType.Id].Add(node.Id); - } - - foreach (var contentType in contentTypes) - { - // again, weird situation - if (contentTypeNodes.ContainsKey(contentType.Id) == false) - continue; - - foreach (var id in contentTypeNodes[contentType.Id]) - { - _contentNodes.TryGetValue(id, out var link); - if (link?.Value == null) - continue; - var node = new ContentNode(link.Value, contentType); - SetValueLocked(_contentNodes, id, node); - if (_localDb != null) RegisterChange(id, node.ToKit()); - } - } - } - finally + var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); + var contentTypeNodes = new Dictionary>(); + foreach (var id in contentTypeIdsA) + contentTypeNodes[id] = new List(); + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) + contentTypeNodes[node.ContentType.Id].Add(node.Id); + } + + foreach (var contentType in contentTypes) + { + // again, weird situation + if (contentTypeNodes.ContainsKey(contentType.Id) == false) + continue; + + foreach (var id in contentTypeNodes[contentType.Id]) + { + _contentNodes.TryGetValue(id, out var link); + if (link?.Value == null) + continue; + var node = new ContentNode(link.Value, contentType); + SetValueLocked(_contentNodes, id, node); + if (_localDb != null) RegisterChange(id, node.ToKit()); + } } } @@ -644,8 +658,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("SetAllFastSorted"); - var ok = true; ClearLocked(_contentNodes); @@ -684,7 +696,7 @@ namespace Umbraco.Web.PublishedCache.NuCache previousNode = null; // there is no previous sibling } - _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); SetValueLocked(_contentNodes, thisNode.Id, thisNode); // if we are initializing from the database source ensure the local db is updated @@ -726,8 +738,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - _logger.Debug("SetAll"); - + ClearLocked(_contentNodes); ClearRootLocked(); @@ -742,7 +753,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ok = false; continue; // skip that one } - _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); @@ -777,8 +788,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - _logger.Debug("SetBranch"); - + // get existing _contentNodes.TryGetValue(rootContentId, out var link); var existing = link?.Value; @@ -826,8 +836,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("Clear"); - // try to find the content // if it is not there, nothing to do _contentNodes.TryGetValue(id, out var link); // else null diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 249eb882f9..c37e5731e4 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -943,14 +943,14 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - _contentStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Content, id)); + _contentStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Content, id)); scope.Complete(); } using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - _mediaStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Media, id)); + _mediaStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Media, id)); scope.Complete(); } } @@ -1073,7 +1073,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ? Array.Empty() : _dataSource.GetTypeContentSources(scope, refreshedIds).ToArray(); - _contentStore.UpdateContentTypes(removedIds, typesA, kits); + _contentStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); if (!newIds.IsCollectionEmpty()) @@ -1104,7 +1104,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ? Array.Empty() : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); - _mediaStore.UpdateContentTypes(removedIds, typesA, kits); + _mediaStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); if (!newIds.IsCollectionEmpty()) From 95337d5a7008469a1cfc84215b9c98e461798fe1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jan 2020 21:39:26 +1100 Subject: [PATCH 169/610] Cleans up old notes --- src/Umbraco.Core/Runtime/MainDom.cs | 2 +- .../Cache/SnapDictionaryTests.cs | 82 +++++++++---------- .../PublishedCache/NuCache/ContentStore.cs | 60 +++++++------- .../NuCache/PublishedSnapshot.cs | 8 -- .../NuCache/PublishedSnapshotService.cs | 10 +-- 5 files changed, 73 insertions(+), 89 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index d7bd407015..67c7be3d21 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -151,7 +151,7 @@ namespace Umbraco.Core.Runtime { _logger.Info("Cannot acquire (timeout)."); - // TODO: In previous versions we'd let a TimeoutException be thrown + // In previous versions we'd let a TimeoutException be thrown // and the appdomain would not start. We have the opportunity to allow it to // start without having MainDom? This would mean that it couldn't write // to nucache/examine and would only be ok if this was a super short lived appdomain. diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index 86426d196c..00ba721bdb 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -9,47 +9,6 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.Cache { - /// - /// Used for tests - /// - public static class SnapDictionaryExtensions - { - internal static void Set(this SnapDictionary d, TKey key, TValue value) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.SetLocked(key, value); - } - } - - internal static void Clear(this SnapDictionary d) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.ClearLocked(); - } - } - - internal static void Clear(this SnapDictionary d, TKey key) - where TValue : class - { - using (d.GetScopedWriteLock(GetScopeProvider())) - { - d.ClearLocked(key); - } - } - - private static IScopeProvider GetScopeProvider() - { - var scopeProvider = Mock.Of(); - Mock.Get(scopeProvider) - .Setup(x => x.Context).Returns(() => null); - return scopeProvider; - } - } - [TestFixture] public class SnapDictionaryTests { @@ -1184,4 +1143,45 @@ namespace Umbraco.Tests.Cache return scopeProvider; } } + + /// + /// Used for tests so that we don't have to wrap every Set/Clear call in locks + /// + public static class SnapDictionaryExtensions + { + internal static void Set(this SnapDictionary d, TKey key, TValue value) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.SetLocked(key, value); + } + } + + internal static void Clear(this SnapDictionary d) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(); + } + } + + internal static void Clear(this SnapDictionary d, TKey key) + where TValue : class + { + using (d.GetScopedWriteLock(GetScopeProvider())) + { + d.ClearLocked(key); + } + } + + private static IScopeProvider GetScopeProvider() + { + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.Context).Returns(() => null); + return scopeProvider; + } + } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 8ce299932c..c959d9f67a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -157,40 +157,44 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Release(WriteLockInfo lockInfo, bool commit = true) { - if (commit == false) + try { - lock(_rlocko) + if (commit == false) { - // see SnapDictionary - try { } - finally + lock (_rlocko) { - _nextGen = false; - _liveGen -= 1; + // see SnapDictionary + try { } + finally + { + _nextGen = false; + _liveGen -= 1; + } } - } - Rollback(_contentNodes); - RollbackRoot(); - Rollback(_contentTypesById); - Rollback(_contentTypesByAlias); - } - else if (_localDb != null && _wchanges != null) - { - foreach (var change in _wchanges) + Rollback(_contentNodes); + RollbackRoot(); + Rollback(_contentTypesById); + Rollback(_contentTypesByAlias); + } + else if (_localDb != null && _wchanges != null) { - if (change.Value.IsNull) - _localDb.TryRemove(change.Key, out ContentNodeKit unused); - else - _localDb[change.Key] = change.Value; + foreach (var change in _wchanges) + { + if (change.Value.IsNull) + _localDb.TryRemove(change.Key, out ContentNodeKit unused); + else + _localDb[change.Key] = change.Value; + } + _wchanges = null; + _localDb.Commit(); } - _wchanges = null; - _localDb.Commit(); } - - // TODO: Shouldn't this be in a finally block? - if (lockInfo.Taken) - Monitor.Exit(_wlocko); + finally + { + if (lockInfo.Taken) + Monitor.Exit(_wlocko); + } } private void RollbackRoot() @@ -256,10 +260,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - _logger.Info("Releasing ContentStore..."); - - // TODO: I don't understand this, we would have already closed the BPlusTree store above?? - // I guess it's 'safe' and will just decrement the write lock counter? Release(lockInfo); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs index bbb84fc650..3f5c1aa4d5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs @@ -39,15 +39,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public void Resync() { - // This is annoying since this can cause deadlocks. Since this will be cleared if something happens in the same - // thread after that calls Elements, it will re-lock the _storesLock but that might already be locked again - // and since we're most likely in a ContentStore write lock, the other thread is probably wanting that one too. - // no lock - published snapshots are single-thread - - // TODO: Instead of clearing, we could hold this value in another var in case the above call to GetElements() Or potentially - // a new TryGetElements() fails (since we might be shutting down). In that case we can use the old value. - _elements?.Dispose(); _elements = null; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index c37e5731e4..aee588d567 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -670,15 +670,7 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } - // TODO: These resync's are a problem, they cause deadlocks because when this is called, it's generally called within a writelock - // and then we clear out the snapshot and then if there's some event handler that needs the content cache, it tries to re-get it - // which first tries locking on the _storesLock which may have already been acquired and in this case we deadlock because - // we're still holding the write lock. - // We resync at the end of a ScopedWriteLock - // BUT if we don't resync here then the current snapshot is out of date for any event handlers that wish to use the most up to date - // data... - // so we need to figure out how to deal with the _storesLock - + if (draftChanged || publishedChanged) ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } From cae2c3217296157d83a559fc1427ac99112a1520 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 7 Jan 2020 20:32:04 +0100 Subject: [PATCH 170/610] Remove SetFlag and UnsetFlag extension methods --- src/Umbraco.Core/EnumExtensions.cs | 38 ----------------- .../CoreThings/EnumExtensionsTests.cs | 42 ------------------- .../Mapping/ContentTypeMapDefinition.cs | 10 ++--- 3 files changed, 5 insertions(+), 85 deletions(-) diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs index b2e5f32c9a..1443a27876 100644 --- a/src/Umbraco.Core/EnumExtensions.cs +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -41,43 +41,5 @@ namespace Umbraco.Core return (num & nums) > 0; } - - /// - /// Sets a flag of the given input enum - /// - /// - /// Enum to set flag of - /// Flag to set - /// A new enum with the flag set - public static T SetFlag(this T input, T flag) - where T : Enum - { - var i = Convert.ToUInt64(input); - var f = Convert.ToUInt64(flag); - - // bitwise OR to set flag f of enum i - var result = i | f; - - return (T)Enum.ToObject(typeof(T), result); - } - - /// - /// Unsets a flag of the given input enum - /// - /// - /// Enum to unset flag of - /// Flag to unset - /// A new enum with the flag unset - public static T UnsetFlag(this T input, T flag) - where T : Enum - { - var i = Convert.ToUInt64(input); - var f = Convert.ToUInt64(flag); - - // bitwise AND combined with bitwise complement to unset flag f of enum i - var result = i & ~f; - - return (T)Enum.ToObject(typeof(T), result); - } } } diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs index 4a0c1d0f41..72a55cee85 100644 --- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs +++ b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs @@ -51,47 +51,5 @@ namespace Umbraco.Tests.CoreThings else Assert.IsFalse(value.HasFlagAny(test)); } - - [TestCase(TreeUse.None, TreeUse.None, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Main, TreeUse.Main)] - [TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.Dialog)] - [TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Main, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)] - public void SetFlagTests(TreeUse value, TreeUse flag, TreeUse expected) - { - Assert.AreEqual(expected, value.SetFlag(flag)); - } - - [TestCase(TreeUse.None, TreeUse.None, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Main, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Main, TreeUse.None)] - [TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main)] - [TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)] - [TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main)] - [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)] - public void UnsetFlagTests(TreeUse value, TreeUse flag, TreeUse expected) - { - Assert.AreEqual(expected, value.UnsetFlag(flag)); - } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 95101da4e3..cdd534a14f 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -226,8 +226,8 @@ namespace Umbraco.Web.Models.Mapping target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; target.Variations = source.AllowCultureVariant - ? target.Variations.SetFlag(ContentVariation.Culture) - : target.Variations.UnsetFlag(ContentVariation.Culture); + ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR + : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) if (source.Id > 0) target.Id = source.Id; @@ -399,9 +399,9 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { - target.Variations = source.AllowCultureVariant - ? target.Variations.SetFlag(ContentVariation.Culture) - : target.Variations.UnsetFlag(ContentVariation.Culture); + target.Variations = source.AllowCultureVariant + ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR + : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) } // handle property groups and property types From ec4bc11c1b1163790d0ef705ae0ef2e573371948 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 7 Jan 2020 20:35:00 +0100 Subject: [PATCH 171/610] Obsolete HasFlagAll extension method --- src/Umbraco.Core/EnumExtensions.cs | 45 +++++++++---------- .../CoreThings/EnumExtensionsTests.cs | 6 ++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs index 1443a27876..9097432f64 100644 --- a/src/Umbraco.Core/EnumExtensions.cs +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -3,43 +3,42 @@ namespace Umbraco.Core { /// - /// Provides extension methods to enums. + /// Provides extension methods to . /// public static class EnumExtensions { - // note: - // - no need to HasFlagExact, that's basically an == test - // - HasFlagAll cannot be named HasFlag because ext. methods never take priority over instance methods - /// - /// Determines whether a flag enum has all the specified values. + /// Determines whether all the flags/bits are set within the enum value. /// - /// - /// True when all bits set in are set in , though other bits may be set too. - /// This is the behavior of the original method. - /// - public static bool HasFlagAll(this T use, T uses) + /// The enum type. + /// The enum value. + /// The flags. + /// + /// true if all the flags/bits are set within the enum value; otherwise, false. + /// + [Obsolete("Use Enum.HasFlag() or bitwise operations (if performance is important) instead.")] + public static bool HasFlagAll(this T value, T flags) where T : Enum { - var num = Convert.ToUInt64(use); - var nums = Convert.ToUInt64(uses); - - return (num & nums) == nums; + return value.HasFlag(flags); } /// - /// Determines whether a flag enum has any of the specified values. + /// Determines whether any of the flags/bits are set within the enum value. /// - /// - /// True when at least one of the bits set in is set in . - /// - public static bool HasFlagAny(this T use, T uses) + /// The enum type. + /// The value. + /// The flags. + /// + /// true if any of the flags/bits are set within the enum value; otherwise, false. + /// + public static bool HasFlagAny(this T value, T flags) where T : Enum { - var num = Convert.ToUInt64(use); - var nums = Convert.ToUInt64(uses); + var v = Convert.ToUInt64(value); + var f = Convert.ToUInt64(flags); - return (num & nums) > 0; + return (v & f) > 0; } } } diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs index 72a55cee85..faa15b0077 100644 --- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs +++ b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs @@ -1,6 +1,7 @@ -using NUnit.Framework; -using Umbraco.Web.Trees; +using System; +using NUnit.Framework; using Umbraco.Core; +using Umbraco.Web.Trees; namespace Umbraco.Tests.CoreThings { @@ -22,6 +23,7 @@ namespace Umbraco.Tests.CoreThings Assert.IsFalse(value.HasFlag(test)); } + [Obsolete] [TestCase(TreeUse.Dialog, TreeUse.Dialog, true)] [TestCase(TreeUse.Dialog, TreeUse.Main, false)] [TestCase(TreeUse.Dialog | TreeUse.Main, TreeUse.Dialog, true)] From c38240c872428b366cd716c34c8ce468ced91271 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 9 Jan 2020 09:35:37 +0100 Subject: [PATCH 172/610] Add SetVariesBy extension methods for ContentVariation --- .../ContentVariationExtensions.cs | 32 +++++++++++++++++++ .../Mapping/ContentTypeMapDefinition.cs | 12 +++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 5b157307ab..0b6b3b5c12 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -195,5 +195,37 @@ namespace Umbraco.Core return true; } + + /// + /// Sets or removes the content type variation depending on the specified value. + /// + /// The content type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(variation, value); + + /// + /// Sets or removes the property type variation depending on the specified value. + /// + /// The property type. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); + + /// + /// Sets or removes the variation depending on the specified value. + /// + /// The existing variations. + /// The variation to set or remove. + /// If set to true sets the variation; otherwise, removes the variation. + /// + /// The variations with the specified variation set or removed. + /// + public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) + { + return value + ? variations | variation // Set flag using bitwise logical OR + : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index cdd534a14f..06155d6888 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -215,7 +215,7 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType + // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations private static void Map(PropertyTypeBasic source, PropertyType target, MapperContext context) { target.Name = source.Label; @@ -225,9 +225,7 @@ namespace Umbraco.Web.Models.Mapping target.MandatoryMessage = source.Validation.MandatoryMessage; target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; - target.Variations = source.AllowCultureVariant - ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR - : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); if (source.Id > 0) target.Id = source.Id; @@ -369,7 +367,7 @@ namespace Umbraco.Web.Models.Mapping target.Validation = source.Validation; } - // Umbraco.Code.MapAll -CreatorId -Level -SortOrder + // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) private static void MapSaveToTypeBase(TSource source, IContentTypeComposition target, MapperContext context) @@ -399,9 +397,7 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { - target.Variations = source.AllowCultureVariant - ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR - : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); } // handle property groups and property types From babe1aa60aa5518967b7be7cc60a00e1517eb9aa Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 9 Jan 2020 09:50:33 +0100 Subject: [PATCH 173/610] Add documentation and clean code --- .../ContentVariationExtensions.cs | 115 +++++++++++++++--- 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 0b6b3b5c12..b3688b08b4 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -12,119 +12,199 @@ namespace Umbraco.Core /// /// Determines whether the content type is invariant. /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing(); /// /// Determines whether the content type varies by culture. /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture(); /// /// Determines whether the content type is invariant. /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); /// /// Determines whether the content type varies by culture. /// - /// And then it could also vary by segment. + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); /// /// Determines whether the content type varies by segment. /// - /// And then it could also vary by culture. + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); /// /// Determines whether the content type varies by culture and segment. /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); /// /// Determines whether the property type is invariant. /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// Determines whether the property type varies by culture. /// - /// And then it could also vary by segment. + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); /// /// Determines whether the property type varies by segment. /// - /// And then it could also vary by culture. + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); /// /// Determines whether the property type varies by culture and segment. /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); /// /// Determines whether the content type is invariant. /// + /// The content type. + /// + /// A value indicating whether the content type is invariant. + /// public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing(); /// /// Determines whether the content type varies by culture. /// - /// And then it could also vary by segment. + /// The content type. + /// + /// A value indicating whether the content type varies by culture. + /// public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture(); /// /// Determines whether the content type varies by segment. /// - /// And then it could also vary by culture. + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment(); /// /// Determines whether the content type varies by culture and segment. /// + /// The content type. + /// + /// A value indicating whether the content type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); /// /// Determines whether the property type is invariant. /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// Determines whether the property type varies by culture. /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture(); /// /// Determines whether the property type varies by segment. /// + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment(); /// /// Determines whether the property type varies by culture and segment. /// + /// The property type. + /// + /// A value indicating whether the property type varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); /// /// Determines whether a variation is invariant. /// + /// The variation. + /// + /// A value indicating whether the variation is invariant. + /// public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; /// /// Determines whether a variation varies by culture. /// - /// And then it could also vary by segment. + /// The variation. + /// + /// A value indicating whether the variation varies by culture. + /// public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; /// /// Determines whether a variation varies by segment. /// - /// And then it could also vary by culture. + /// The variation. + /// + /// A value indicating whether the variation varies by segment. + /// public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; /// /// Determines whether a variation varies by culture and segment. /// + /// The variation. + /// + /// A value indicating whether the variation varies by culture and segment. + /// public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment; /// @@ -135,16 +215,18 @@ namespace Umbraco.Core /// The segment. /// A value indicating whether to perform exact validation. /// A value indicating whether to support wildcards. - /// A value indicating whether to throw a when the combination is invalid. - /// True if the combination is valid; otherwise false. + /// A value indicating whether to throw a when the combination is invalid. + /// + /// true if the combination is valid; otherwise false. + /// + /// Occurs when the combination is invalid, and is true. /// /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is /// Culture, an invariant combination is ok. /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type. - /// Both and can be "*" to indicate "all of them". + /// Both and can be "*" to indicate "all of them". /// - /// Occurs when the combination is invalid, and is true. public static bool ValidateVariation(this ContentVariation variation, string culture, string segment, bool exact, bool wildcards, bool throwIfInvalid) { culture = culture.NullOrWhiteSpaceAsNull(); @@ -161,13 +243,14 @@ namespace Umbraco.Core if (variation.VariesByCulture()) { // varies by culture - // in exact mode, the culture cannot be null + // in exact mode, the culture cannot be null if (exact && culture == null) { if (throwIfInvalid) throw new NotSupportedException($"Culture may not be null because culture variation is enabled."); + return false; - } + } } else { @@ -178,9 +261,10 @@ namespace Umbraco.Core { if (throwIfInvalid) throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled."); + return false; } - } + } // if it does not vary by segment // the segment cannot have a value @@ -190,6 +274,7 @@ namespace Umbraco.Core { if (throwIfInvalid) throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled."); + return false; } From a10c4403abda8ca1817817061c2042b8373a1284 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 9 Jan 2020 14:26:18 +0100 Subject: [PATCH 174/610] Remove konamiCode directive since it isn't used anywhere in core (#7437) --- .../directives/util/konami.directive.js | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js deleted file mode 100644 index 7914dfc3f0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Konami Code directive for AngularJS - * @version v0.0.1 - * @license MIT License, https://www.opensource.org/licenses/MIT - */ - -angular.module('umbraco.directives') - .directive('konamiCode', ['$document', function ($document) { - var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; - - return { - restrict: 'A', - link: function (scope, element, attr) { - - if (!attr.konamiCode) { - throw ('Konami directive must receive an expression as value.'); - } - - // Let user define a custom code. - var konamiKeys = attr.konamiKeys || konamiKeysDefault; - var keyIndex = 0; - - /** - * Fired when konami code is type. - */ - function activated() { - if ('konamiOnce' in attr) { - stopListening(); - } - // Execute expression. - scope.$eval(attr.konamiCode); - } - - /** - * Handle keydown events. - */ - function keydown(e) { - if (e.keyCode === konamiKeys[keyIndex++]) { - if (keyIndex === konamiKeys.length) { - keyIndex = 0; - activated(); - } - } else { - keyIndex = 0; - } - } - - /** - * Stop to listen typing. - */ - function stopListening() { - $document.off('keydown', keydown); - } - - // Start listening to key typing. - $document.on('keydown', keydown); - - // Stop listening when scope is destroyed. - scope.$on('$destroy', stopListening); - } - }; - }]); From 16faeb1886276b0843087230d3693f721db62013 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Fri, 10 Jan 2020 08:34:23 +0000 Subject: [PATCH 175/610] V8: Date pickers - parsing non-standard formats (#6001) --- .../datepicker/datepicker.controller.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 0f19bf3b1a..24affc6ac1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -85,13 +85,27 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM }; $scope.datePickerChange = function(date) { - setDate(date); + const momentDate = moment(date); + setDate(momentDate); setDatePickerVal(); }; - $scope.inputChanged = function() { - setDate($scope.model.datetimePickerValue); - setDatePickerVal(); + $scope.inputChanged = function () { + if ($scope.model.datetimePickerValue === "" && $scope.hasDatetimePickerValue) { + // $scope.hasDatetimePickerValue indicates that we had a value before the input was changed, + // but now the input is empty. + $scope.clearDate(); + } else if ($scope.model.datetimePickerValue) { + var momentDate = moment($scope.model.datetimePickerValue, $scope.model.config.format, true); + if (!momentDate || !momentDate.isValid()) { + momentDate = moment(new Date($scope.model.datetimePickerValue)); + } + if (momentDate && momentDate.isValid()) { + setDate(momentDate); + } + setDatePickerVal(); + flatPickr.setDate($scope.model.value, false); + } } //here we declare a special method which will be called whenever the value has changed from the server @@ -103,15 +117,14 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM var newDate = moment(newVal); if (newDate.isAfter(minDate)) { - setDate(newVal); + setDate(newDate); } else { $scope.clearDate(); } } }; - function setDate(date) { - const momentDate = moment(date); + function setDate(momentDate) { angularHelper.safeApply($scope, function() { // when a date is changed, update the model if (momentDate && momentDate.isValid()) { @@ -123,12 +136,11 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM $scope.hasDatetimePickerValue = false; $scope.model.datetimePickerValue = null; } - updateModelValue(date); + updateModelValue(momentDate); }); } - function updateModelValue(date) { - const momentDate = moment(date); + function updateModelValue(momentDate) { if ($scope.hasDatetimePickerValue) { if ($scope.model.config.pickTime) { //check if we are supposed to offset the time From 2b4279315a1d9619455509a29d83aecef56f634c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 10:05:52 +0100 Subject: [PATCH 176/610] V8: Make the markdown editor use the Umbraco link picker (#5745) --- .../lib/markdown/markdown.editor.js | 10 ++++++---- .../markdowneditor.controller.js | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js index a75a7f1f3c..60118dbdb3 100644 --- a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js +++ b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js @@ -60,6 +60,7 @@ * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. */ + hooks.addFalse("insertLinkDialog"); this.getConverter = function () { return markdownConverter; } @@ -1636,7 +1637,7 @@ var that = this; // The function to be executed when you enter a link and press OK or Cancel. // Marks up the link and adds the ref. - var linkEnteredCallback = function (link) { + var linkEnteredCallback = function (link, title) { if (link !== null) { // ( $1 @@ -1667,10 +1668,10 @@ if (!chunk.selection) { if (isImage) { - chunk.selection = "enter image description here"; + chunk.selection = title || "enter image description here"; } else { - chunk.selection = "enter link description here"; + chunk.selection = title || "enter link description here"; } } } @@ -1683,7 +1684,8 @@ ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback); } else { - ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback); + if (!this.hooks.insertLinkDialog(linkEnteredCallback)) + ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback); } return true; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js index f05b1e31d8..bb87f0463d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js @@ -27,6 +27,20 @@ function MarkdownEditorController($scope, $element, assetsService, editorService editorService.mediaPicker(mediaPicker); } + function openLinkPicker(callback) { + var linkPicker = { + hideTarget: true, + submit: function(model) { + callback(model.target.url, model.target.name); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + editorService.linkPicker(linkPicker); + } + assetsService .load([ "lib/markdown/markdown.converter.js", @@ -53,6 +67,12 @@ function MarkdownEditorController($scope, $element, assetsService, editorService return true; // tell the editor that we'll take care of getting the image url }); + //subscribe to the link dialog clicks + editor2.hooks.set("insertLinkDialog", function (callback) { + openLinkPicker(callback); + return true; // tell the editor that we'll take care of getting the link url + }); + editor2.hooks.set("onPreviewRefresh", function () { // We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library. if ($scope.model.value !== $("textarea", $element).val()) { From eeaa5a82d46ebbae5616aafa747a452d36bd05c8 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Fri, 10 Jan 2020 10:39:22 +0100 Subject: [PATCH 177/610] V8/doctype tours (#6980) --- .../src/common/resources/tour.resource.js | 13 +++- .../common/services/editorstate.service.js | 3 +- .../src/common/services/tour.service.js | 65 ++++++++++------ .../common/drawers/help/help.controller.js | 35 ++++++++- .../src/views/common/drawers/help/help.html | 74 ++++++++++++------- src/Umbraco.Web/Editors/TourController.cs | 33 +++++++++ src/Umbraco.Web/Models/BackOfficeTour.cs | 3 + 7 files changed, 174 insertions(+), 52 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js index 40baf0f389..485b0d299a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js @@ -20,10 +20,21 @@ "GetTours")), 'Failed to get tours'); } + + function getToursForDoctype(doctypeAlias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "tourApiBaseUrl", + "GetToursForDoctype", + [{ doctypeAlias: doctypeAlias }])), + 'Failed to get tours'); + } var resource = { - getTours: getTours + getTours: getTours, + getToursForDoctype: getToursForDoctype }; return resource; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js index 97a9ac5c4b..28daa3f245 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js @@ -10,7 +10,7 @@ * * it is possible to modify this object, so should be used with care */ -angular.module('umbraco.services').factory("editorState", function ($rootScope) { +angular.module('umbraco.services').factory("editorState", function ($rootScope, eventsService) { var current = null; @@ -30,6 +30,7 @@ angular.module('umbraco.services').factory("editorState", function ($rootScope) */ set: function (entity) { current = entity; + eventsService.emit("editorState.changed", { entity: entity }); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index e102da5d34..7219395fd6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -134,31 +134,33 @@ var groupedTours = []; tours.forEach(function (item) { - var groupExists = false; - var newGroup = { - "group": "", - "tours": [] - }; + if (item.contentType === null || item.contentType === '') { + var groupExists = false; + var newGroup = { + "group": "", + "tours": [] + }; - groupedTours.forEach(function(group){ - // extend existing group if it is already added - if(group.group === item.group) { - if(item.groupOrder) { - group.groupOrder = item.groupOrder + groupedTours.forEach(function (group) { + // extend existing group if it is already added + if (group.group === item.group) { + if (item.groupOrder) { + group.groupOrder = item.groupOrder; + } + groupExists = true; + group.tours.push(item); } - groupExists = true; - group.tours.push(item) - } - }); + }); - // push new group to array if it doesn't exist - if(!groupExists) { - newGroup.group = item.group; - if(item.groupOrder) { - newGroup.groupOrder = item.groupOrder + // push new group to array if it doesn't exist + if (!groupExists) { + newGroup.group = item.group; + if (item.groupOrder) { + newGroup.groupOrder = item.groupOrder; + } + newGroup.tours.push(item); + groupedTours.push(newGroup); } - newGroup.tours.push(item); - groupedTours.push(newGroup); } }); @@ -188,6 +190,24 @@ return deferred.promise; } + /** + * @ngdoc method + * @name umbraco.services.tourService#getToursForDoctype + * @methodOf umbraco.services.tourService + * + * @description + * Returns a promise of the tours found by documenttype alias. + * @param {Object} doctypeAlias The doctype alias for which the tours which should be returned + * @returns {Array} An array of tour objects for the doctype + */ + function getToursForDoctype(doctypeAlias) { + var deferred = $q.defer(); + tourResource.getToursForDoctype(doctypeAlias).then(function (tours) { + deferred.resolve(tours); + }); + return deferred.promise; + } + /////////// /** @@ -269,7 +289,8 @@ completeTour: completeTour, getCurrentTour: getCurrentTour, getGroupedTours: getGroupedTours, - getTourByAlias: getTourByAlias + getTourByAlias: getTourByAlias, + getToursForDoctype : getToursForDoctype }; return service; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js index 3323f2bfb3..268bfb3a8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter) { + function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState) { var vm = this; var evts = []; @@ -18,6 +18,10 @@ vm.startTour = startTour; vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage; vm.showTourButton = showTourButton; + + vm.showDocTypeTour = false; + vm.docTypeTours = []; + vm.nodeName = ''; function startTour(tour) { tourService.startTour(tour); @@ -58,9 +62,16 @@ handleSectionChange(); })); + evts.push(eventsService.on("editorState.changed", + function (e, args) { + setDocTypeTour(args.entity); + })); + findHelp(vm.section, vm.tree, vm.userType, vm.userLang); }); + + setDocTypeTour(editorState.getCurrent()); // check if a tour is running - if it is open the matching group var currentTour = tourService.getCurrentTour(); @@ -84,7 +95,7 @@ setSectionName(); findHelp(vm.section, vm.tree, vm.userType, vm.userLang); - + setDocTypeTour(); } }); } @@ -168,6 +179,26 @@ }); } + function setDocTypeTour(node) { + vm.showDocTypeTour = false; + vm.docTypeTours = []; + vm.nodeName = ''; + + if (vm.section === 'content' && vm.tree === 'content') { + + if (node) { + tourService.getToursForDoctype(node.contentTypeAlias).then(function (data) { + if (data && data.length > 0) { + vm.docTypeTours = data; + var currentVariant = _.find(node.variants, (x) => x.active); + vm.nodeName = currentVariant.name; + vm.showDocTypeTour = true; + } + }); + } + } + } + evts.push(eventsService.on("appState.tour.complete", function (event, tour) { tourService.getGroupedTours().then(function(groupedTours) { vm.tours = groupedTours; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 96f4a404bc..822b5bdb19 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -8,12 +8,34 @@ - -
+ +
+
Need help editing current item '{{vm.nodeName}}' ?
-
Tours
+
-
+ +
+
+
+
+ {{ tour.name }} +
+
+ +
+
+
+
+
+
+ + +
+ +
Tours
+ +
@@ -51,33 +73,33 @@
-
+
- -
-
-
{{dashboard.label}}
-
-
-
+ +
+
+
{{dashboard.label}}
+
+
+
+
-
- -
-
Articles
-
    -
  • - - - - {{topic.name}} - - - {{topic.description}} - + +
    +
    Articles
    +
    diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 25b4f7e9fc..8991bcdd6a 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -110,6 +110,39 @@ namespace Umbraco.Web.Editors return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); } + /// + /// Gets a tours for a specific doctype + /// + /// The documenttype alias + /// A + public IEnumerable GetToursForDoctype(string doctypeAlias) + { + var tourFiles = this.GetTours(); + + var doctypeAliasWithCompositions = new List + { + doctypeAlias + }; + + var contentType = this.Services.ContentTypeService.Get(doctypeAlias); + + if (contentType != null) + { + doctypeAliasWithCompositions.AddRange(contentType.CompositionAliases()); + } + + return tourFiles.SelectMany(x => x.Tours) + .Where(x => + { + if (string.IsNullOrEmpty(x.ContentType)) + { + return false; + } + var contentTypes = x.ContentType.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim()); + return contentTypes.Intersect(doctypeAliasWithCompositions).Any(); + }); + } + private void TryParseTourFile(string tourFile, ICollection result, List filters, diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index d5987ec5bc..3e14f278e2 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -31,5 +31,8 @@ namespace Umbraco.Web.Models [DataMember(Name = "culture")] public string Culture { get; set; } + + [DataMember(Name = "contentType")] + public string ContentType { get; set; } } } From f92d0b59bd64b0b751e2e329499a2785cf613637 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Fri, 10 Jan 2020 10:41:37 +0100 Subject: [PATCH 178/610] Make sure the configured url provider mode is used when getting a url (#7189) --- src/Umbraco.Web/PublishedContentExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 75eb6adbcb..061422859c 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1193,7 +1193,7 @@ namespace Umbraco.Web /// if any. In addition, when the content type is multi-lingual, this is the url for the /// specified culture. Otherwise, it is the invariant url. /// - public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Auto) + public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default) { var umbracoContext = Composing.Current.UmbracoContext; From 4a44cd3a5823454f3e0c9cb5b44267023a8c98df Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:02:39 +0100 Subject: [PATCH 179/610] =?UTF-8?q?V8:=20If=20Nested=20Content=20has=20mul?= =?UTF-8?q?tiple=20item=20types,=20always=20let=20the=20e=E2=80=A6=20(#542?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lovely work Kenn. Thanks, as ever --- .../nestedcontent/nestedcontent.controller.js | 50 ++++++++++++------- .../nestedcontent.propertyeditor.html | 6 +-- 2 files changed, 36 insertions(+), 20 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 b61f8b94c2..7de3a5b567 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 @@ -41,7 +41,7 @@ if (vm.maxItems === 0) vm.maxItems = 1000; - vm.singleMode = vm.minItems === 1 && vm.maxItems === 1; + vm.singleMode = vm.minItems === 1 && vm.maxItems === 1 && model.config.contentTypes.length === 1;; vm.showIcons = Object.toBoolean(model.config.showIcons); vm.wideMode = Object.toBoolean(model.config.hideLabel); vm.hasContentTypes = model.config.contentTypes.length > 0; @@ -131,6 +131,7 @@ setCurrentNode(newNode); setDirty(); + validate(); }; vm.openNodeTypePicker = function ($event) { @@ -234,12 +235,22 @@ } }; + vm.canDeleteNode = function (idx) { + return (vm.nodes.length > vm.minItems) + ? true + : model.config.contentTypes.length > 1; + } + function deleteNode(idx) { vm.nodes.splice(idx, 1); setDirty(); updateModel(); + validate(); }; vm.requestDeleteNode = function (idx) { + if (!vm.canDeleteNode(idx)) { + return; + } if (model.config.confirmDeletes === true) { localizationService.localizeMany(["content_nestedContentDeleteItem", "general_delete", "general_cancel", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { @@ -468,8 +479,8 @@ } } - // Auto-fill with elementTypes, but only if we have one type to choose from, and if this property is empty. - if (vm.singleMode === true && vm.nodes.length === 0 && model.config.minItems > 0) { + // Enforce min items if we only have one scaffold type + if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { for (var i = vm.nodes.length; i < model.config.minItems; i++) { addNode(vm.scaffolds[0].contentTypeAlias); } @@ -480,6 +491,8 @@ setCurrentNode(vm.nodes[0]); } + validate(); + vm.inited = true; updatePropertyActionStates(); @@ -585,25 +598,28 @@ updateModel(); }); + var validate = function () { + if (vm.nodes.length < vm.minItems) { + $scope.nestedContentForm.minCount.$setValidity("minCount", false); + } + else { + $scope.nestedContentForm.minCount.$setValidity("minCount", true); + } + + if (vm.nodes.length > vm.maxItems) { + $scope.nestedContentForm.maxCount.$setValidity("maxCount", false); + } + else { + $scope.nestedContentForm.maxCount.$setValidity("maxCount", true); + } + } + var watcher = $scope.$watch( function () { return vm.nodes.length; }, function () { - //Validate! - if (vm.nodes.length < vm.minItems) { - $scope.nestedContentForm.minCount.$setValidity("minCount", false); - } - else { - $scope.nestedContentForm.minCount.$setValidity("minCount", true); - } - - if (vm.nodes.length > vm.maxItems) { - $scope.nestedContentForm.maxCount.$setValidity("maxCount", false); - } - else { - $scope.nestedContentForm.maxCount.$setValidity("maxCount", true); - } + validate(); } ); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index b3821cff3d..47293e433b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -2,7 +2,7 @@ - +
    @@ -17,7 +17,7 @@ {{vm.labels.copy_icon_title}} -
    From 2b1e33095769e1dc112308efb586afbc38216849 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:09:51 +0100 Subject: [PATCH 180/610] =?UTF-8?q?V8:=20Reset=20paging=20in=20user=20sear?= =?UTF-8?q?ch=20when=20free=20text=20searching=20and=20fi=E2=80=A6=20(#704?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Kenn :) --- .../src/views/users/views/users/users.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 4ee31806a3..935b1bdfb1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -451,7 +451,7 @@ var search = _.debounce(function () { $scope.$apply(function () { - getUsers(); + changePageNumber(1); }); }, 500); @@ -512,7 +512,7 @@ } updateLocation("userStates", vm.usersOptions.userStates.join(",")); - getUsers(); + changePageNumber(1); } function setUserGroupFilter(userGroup) { @@ -529,7 +529,7 @@ } updateLocation("userGroups", vm.usersOptions.userGroups.join(",")); - getUsers(); + changePageNumber(1); } function setOrderByFilter(value, direction) { From 2906eafa791fc2c3a2e8a270e2a850a7c25f3b01 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:14:11 +0100 Subject: [PATCH 181/610] =?UTF-8?q?V8:=20Make=20it=20possible=20to=20hide?= =?UTF-8?q?=20anchor/querystring=20input=20in=20the=20li=E2=80=A6=20(#7031?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wonderful work --- .../infiniteeditors/linkpicker/linkpicker.controller.js | 1 + .../views/common/infiniteeditors/linkpicker/linkpicker.html | 4 ++-- .../multiurlpicker/multiurlpicker.controller.js | 1 + .../PropertyEditors/MultiUrlPickerConfiguration.cs | 5 ++++- 4 files changed, 8 insertions(+), 3 deletions(-) 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 b5043293e5..47607b7f0b 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 @@ -33,6 +33,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }; $scope.showTarget = $scope.model.hideTarget !== true; + $scope.showAnchor = $scope.model.hideAnchor !== true; // this ensures that we only sync the tree once and only when it's ready var oneTimeTreeSync = { 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 a7d2dbbee2..ad0aaab57c 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 @@ -14,7 +14,7 @@ -
    +
    - + Date: Mon, 13 Jan 2020 09:15:02 +1000 Subject: [PATCH 182/610] guard code for non-infinite editing to prevent route change --- .../src/views/media/media.edit.controller.js | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) 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 a5884c2355..2ed43f7495 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 @@ -73,14 +73,14 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, var content = $scope.content; - // we need to check wether an app is present in the current data, if not we will present the default app. + // we need to check whether an app is present in the current data, if not we will present the default app. var isAppPresent = false; // on first init, we dont have any apps. but if we are re-initializing, we do, but ... if ($scope.app) { // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) - _.forEach(content.apps, function(app) { + content.apps.forEach(app => { if (app === $scope.app) { isAppPresent = true; } @@ -88,7 +88,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { - _.forEach(content.apps, function(app) { + content.apps.forEach(app => { if (app.alias === $scope.app.alias) { isAppPresent = true; app.active = true; @@ -182,24 +182,26 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, formHelper.resetForm({ scope: $scope }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - editorState.set($scope.content); - - syncTreeNode($scope.content, data.path); - - init(); - - $scope.page.saveButtonState = "success"; - // close the editor if it's infinite mode + // submit function manages rebinding changes if(infiniteMode && $scope.model.submit) { $scope.model.mediaNode = $scope.content; $scope.model.submit($scope.model); + } else { + // if not infinite mode, rebind changed props etc + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + + editorState.set($scope.content); + + syncTreeNode($scope.content, data.path); + + $scope.page.saveButtonState = "success"; + + init(); } }, function(err) { From 21aa8fd95921f44d07064738a3c838775895d366 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Mon, 13 Jan 2020 09:15:27 +1000 Subject: [PATCH 183/610] remove lodash in favor of native es6 methods, adds type attribute to buttons --- .../mediapicker/mediapicker.controller.js | 121 +++++++++--------- .../mediapicker/mediapicker.html | 6 +- 2 files changed, 60 insertions(+), 67 deletions(-) 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 c9d4caf312..ec6ab05cbd 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 @@ -48,49 +48,45 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // This is done by remapping the int/guid ids into a new array of items, where we create "Deleted item" placeholders // when there is no match for a selected id. This will ensure that the values being set on save, are the same as before. - medias = _.map(ids, - function (id) { - var found = _.find(medias, - function (m) { - // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and - // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() - // compares and be completely sure it works. - return m.udi.toString() === id.toString() || m.id.toString() === id.toString(); - }); - if (found) { - return found; - } else { - return { - name: vm.labels.deletedItem, - id: $scope.model.config.idType !== "udi" ? id : null, - udi: $scope.model.config.idType === "udi" ? id : null, - icon: "icon-picture", - thumbnail: null, - trashed: true - }; - } - }); + medias = ids.map(id => { + var found = medias.find(m => + // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and + // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() + // compares and be completely sure it works. + m.udi.toString() === id.toString() || m.id.toString() === id.toString()); + + if (found) { + return found; + } else { + return { + name: vm.labels.deletedItem, + id: $scope.model.config.idType !== "udi" ? id : null, + udi: $scope.model.config.idType === "udi" ? id : null, + icon: "icon-picture", + thumbnail: null, + trashed: true + }; + } + }); - _.each(medias, - function (media, i) { + medias.forEach(media => { + if (!media.extension && media.id && media.metaData) { + media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); + } - if (!media.extension && media.id && media.metaData) { - media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); - } + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + } - // if there is no thumbnail, try getting one if the media is not a placeholder item - if (!media.thumbnail && media.id && media.metaData) { - 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); - } else { - $scope.ids.push(media.id); - } - }); + if ($scope.model.config.idType === "udi") { + $scope.ids.push(media.udi); + } else { + $scope.ids.push(media.id); + } + }); sync(); }); @@ -100,7 +96,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl function sync() { $scope.model.value = $scope.ids.join(); removeAllEntriesAction.isDisabled = $scope.ids.length === 0; - }; + } function setDirty() { angularHelper.getCurrentForm($scope).$setDirty(); @@ -111,18 +107,17 @@ 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; - }); - } - }); + $scope.mediaItems.forEach(media => { + if (updatedMediaNodes.indexOf(media.udi) !== -1) { + media.loading = true; + entityResource.getById(media.udi, "Media") + .then(function (mediaEntity) { + angular.extend(media, mediaEntity); + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + media.loading = false; + }); + } + }); } function init() { @@ -177,20 +172,20 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // the media picker is using media entities so we get the // entity so we easily can format it for use in the media grid if (model && model.mediaNode) { - entityResource.getById(model.mediaNode.id, "media") + entityResource.getById(model.mediaNode.id, "Media") .then(function (mediaEntity) { // if an image is selecting more than once // we need to update all the media items - angular.forEach($scope.images, function (image) { - if (image.id === model.mediaNode.id) { - angular.extend(image, mediaEntity); - image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); + $scope.mediaItems.forEach(media => { + if (media.id === model.mediaNode.id) { + angular.extend(media, mediaEntity); + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } }); }); } }, - close: function (model) { + close: function () { editorService.close(); } }; @@ -210,7 +205,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl editorService.close(); - _.each(model.selection, function (media, i) { + model.selection.forEach(media => { // if there is no thumbnail, try getting one if the media is not a placeholder item if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); @@ -280,16 +275,14 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl disabled: !multiPicker, items: "li:not(.add-wrapper)", cancel: ".unsortable", - update: function (e, ui) { + update: function () { setDirty(); $timeout(function() { // TODO: Instead of doing this with a timeout would be better to use a watch like we do in the // 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. - $scope.ids = _.map($scope.mediaItems, - function (item) { - return $scope.model.config.idType === "udi" ? item.udi : item.id; - }); + $scope.ids = $scope.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id); + sync(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index 1f9bd4e3c0..b4da03d30b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -36,16 +36,16 @@
    - -
  • -
  • From d26334ad398c980159cd8ad583b3886debd9391b Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2020 17:16:55 +1100 Subject: [PATCH 184/610] Fixes the issue of manipulating data for an existing Gen, adds notes, makes the Gen comparison operator consistent. --- .../PublishedCache/NuCache/ContentNode.cs | 6 ++++++ .../PublishedCache/NuCache/ContentStore.cs | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs index 3b4859432d..a724e78b72 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs @@ -109,6 +109,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // everything that is common to both draft and published versions // keep this as small as possible + + public readonly int Id; public readonly Guid Uid; public IPublishedContentType ContentType; @@ -116,10 +118,14 @@ namespace Umbraco.Web.PublishedCache.NuCache public readonly string Path; public readonly int SortOrder; public readonly int ParentContentId; + + // TODO: Can we make everything readonly?? This would make it easier to debug and be less error prone especially for new developers. + // Once a Node is created and exists in the cache it is readonly so we should be able to make that happen at the API level too. public int FirstChildContentId; public int LastChildContentId; public int NextSiblingContentId; public int PreviousSiblingContentId; + public readonly DateTime CreateDate; public readonly int CreatorId; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 550fd507d5..3857a49a28 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -602,7 +602,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private void ClearRootLocked() { - if (_root.Gen < _liveGen) + if (_root.Gen != _liveGen) _root = new LinkedNode(new ContentNode(), _liveGen, _root); else _root.Value.FirstChildContentId = -1; @@ -899,8 +899,18 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } + /// + /// This removes this current node from the tree hiearchy by removing it from it's parent's linked list + /// + /// + /// + /// This is called within a lock which means a new Gen is being created therefore this will not modify any existing content in a Gen. + /// private void RemoveTreeNodeLocked(ContentNode content) { + // NOTE: DO NOT modify `content` here, this would modify data for an existing Gen, all modifications are done to clones + // which would be targeting the new Gen. + var parentLink = content.ParentContentId < 0 ? _root : GetRequiredLinkedNode(content.ParentContentId, "parent", null); @@ -937,9 +947,6 @@ namespace Umbraco.Web.PublishedCache.NuCache var prev = GenCloneLocked(prevLink); prev.NextSiblingContentId = content.NextSiblingContentId; } - - content.NextSiblingContentId = -1; - content.PreviousSiblingContentId = -1; } private bool ParentPublishedLocked(ContentNodeKit kit) @@ -955,7 +962,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var node = link.Value; - if (node != null && link.Gen < _liveGen) + if (node != null && link.Gen != _liveGen) { node = new ContentNode(link.Value); if (link == _root) @@ -1109,7 +1116,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this is safe only because we're write-locked foreach (var kvp in dict.Where(x => x.Value != null)) { - if (kvp.Value.Gen < _liveGen) + if (kvp.Value.Gen != _liveGen) { var link = new LinkedNode(null, _liveGen, kvp.Value); dict.TryUpdate(kvp.Key, link, kvp.Value); From b02360518d2da1efb64be81fc6c724c9047699db Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2020 22:25:46 +1100 Subject: [PATCH 185/610] Changed to GetAwaiter/GetResult and updates comment --- src/Umbraco.Core/Runtime/IMainDomLock.cs | 3 +-- src/Umbraco.Core/Runtime/MainDom.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs index c32e990114..6a62f48194 100644 --- a/src/Umbraco.Core/Runtime/IMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -16,9 +16,8 @@ namespace Umbraco.Core.Runtime ///
/// /// - /// A disposable object which will be disposed in order to release the lock + /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded /// - /// Throws a if the elapsed millsecondsTimeout value is exceeded Task AcquireLockAsync(int millisecondsTimeout); ///
diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 67c7be3d21..e6780ec876 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -145,7 +145,7 @@ namespace Umbraco.Core.Runtime _logger.Info("Acquiring."); // Get the lock - var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Result; + var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult(); if (!acquired) { From 3e71de4698f8a46e92ef747e39366b80dcf635aa Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2020 22:28:25 +1100 Subject: [PATCH 186/610] ensure locks the data types --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index c959d9f67a..cecca3eb75 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -462,6 +462,8 @@ namespace Umbraco.Web.PublishedCache.NuCache /// public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) { + EnsureLocked(); + var contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && From d48b33d86d0b81666dec4d33d224b6de2d1ff74c Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Mon, 13 Jan 2020 22:03:34 +0100 Subject: [PATCH 187/610] Icon picker: Accessibility improvements (#6990) --- .../src/less/components/umb-iconpicker.less | 10 ++++++---- .../common/infiniteeditors/iconpicker/iconpicker.html | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less index e8a62f739d..98b2b1d72d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less @@ -15,7 +15,9 @@ overflow: hidden; } -.umb-iconpicker-item a { +.umb-iconpicker-item button { + background: transparent; + border: 0 none; display: flex; justify-content: center; align-items: center; @@ -26,8 +28,8 @@ border-radius: 3px; } -.umb-iconpicker-item a:hover, -.umb-iconpicker-item a:focus { +.umb-iconpicker-item button:hover, +.umb-iconpicker-item button:focus { background: @gray-10; outline: none; } @@ -39,7 +41,7 @@ box-sizing: border-box; } -.umb-iconpicker-item a:active { +.umb-iconpicker-item button:active { background: @gray-10; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 3caa6ae03d..c2b0d0e458 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -46,9 +46,10 @@
  • - - - +
From 8176054432b6187b1cec4921f4542bfb45be2469 Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Mon, 13 Jan 2020 22:33:31 +0100 Subject: [PATCH 188/610] Add label and missing translations (#7019) --- .../components/application/umb-search.less | 6 +- .../components/application/umb-search.html | 14 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4737 +++++++++-------- .../Umbraco/config/lang/en_us.xml | 1 + 4 files changed, 2385 insertions(+), 2373 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less index 70e4f3d372..2f9430ef41 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less @@ -16,6 +16,10 @@ box-shadow: 0 10px 20px rgba(0,0,0,.12),0 6px 6px rgba(0,0,0,.14); } +.umb-search__label{ + margin: 0; +} + /* Search field */ @@ -107,4 +111,4 @@ .umb-search-result__description { color: @gray-5; font-size: 13px; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html index 297ade28c0..cc1c8abb7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html @@ -2,15 +2,21 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html index f4e4dff3af..b45a3bb843 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html @@ -6,11 +6,11 @@ '-left': direction === 'left'}" on-outside-click="clickCancel()"> - + - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 3db1221f5e..5adf121a56 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -402,6 +402,15 @@ angular.module("umbraco") eventsService.emit("grid.rowAdded", { scope: $scope, element: $element, row: row }); + // TODO: find a nicer way to do this without relying on setTimeout + setTimeout(function () { + var newRowEl = $element.find("[data-rowid='" + row.$uniqueId + "']"); + + if(newRowEl !== null) { + newRowEl.focus(); + } + }, 0); + }; $scope.removeRow = function (section, $index) { 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 e889067321..bde2b9148e 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 @@ -95,7 +95,7 @@
- + -
+
+
- +
- +
-
+
@@ -249,14 +249,16 @@
- - - - + + + Add new role + +
@@ -264,10 +266,11 @@

-
+ ng-click="addRow(section, layout)" + type="button">
@@ -285,7 +288,7 @@ {{layout.label || layout.name}} -
+
From 6ca0a8ba665075acf65efeafbfc0eb46dda665ef Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 14:47:25 +0100 Subject: [PATCH 198/610] V8: A few UX updates to the listview move dialog (#6902) * Add some context to the move operation from listviews * Disable submit button in the listview move dialog until a target is chosen * Fix bad merge --- .../common/infiniteeditors/move/move.html | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 4801 +++++++++-------- 4 files changed, 2408 insertions(+), 2401 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html index 8424bcefbe..1c9fd5b822 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html @@ -13,6 +13,9 @@ +

+ Choose where to move the selected item(s) +

@@ -75,7 +78,8 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" - action="vm.submit(model)"> + action="vm.submit(model)" + disabled="!model.target"> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 08a00f502e..78075d9b7a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -35,6 +35,7 @@ Sæt rettigheder for siden %0% Vælg hvor du vil kopiere Vælg hvortil du vil flytte + Vælg hvor du vil flytte de valgte elementer hen til i træstrukturen nedenfor Vælg hvor du vil kopiere de valgte elementer til blev flyttet til diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d8e94017aa..a2cf6ea475 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -37,6 +37,7 @@ Choose where to move to in the tree structure below Choose where to copy the selected item(s) + Choose where to move the selected item(s) was moved to was copied to was deleted 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 82ba12dc17..56d5f1ade2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1,2400 +1,2401 @@ - - - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Culture and Hostnames - Audit Trail - Browse Node - Change Document Type - Copy - Create - Export - Create Package - Create group - Delete - Disable - Empty recycle bin - Enable - Export Document Type - Import Document Type - Import Package - Edit in Canvas - Exit - Move - Notifications - Public access - Publish - Unpublish - Reload - Republish entire site - Rename - Restore - Set permissions for the page %0% - Choose where to copy - Choose where to move - to in the tree structure below - Choose where to copy the selected item(s) - was moved to - was copied to - was deleted - Permissions - Rollback - Send To Publish - Send To Translation - Set group - Sort - Translate - Update - Set permissions - Unlock - Create Content Template - Resend Invitation - - - Content - Administration - Structure - Other - - - Allow access to assign culture and hostnames - Allow access to view a node's history log - Allow access to view a node - Allow access to change document type for a node - Allow access to copy a node - Allow access to create nodes - Allow access to delete nodes - Allow access to move a node - Allow access to set and change public access for a node - Allow access to publish a node - Allow access to unpublish a node - Allow access to change permissions for a node - Allow access to roll back a node to a previous state - Allow access to send a node for approval before publishing - Allow access to send a node for translation - Allow access to change the sort order for nodes - Allow access to translate a node - Allow access to save a node - Allow access to create a Content Template - - - Content - Info - - - Permission denied. - Add new Domain - remove - Invalid node. - One or more domains have an invalid format. - Domain has already been assigned. - Language - Domain - New domain '%0%' has been created - Domain '%0%' is deleted - Domain '%0%' has already been assigned - Domain '%0%' has been updated - Edit Current Domains - - - Inherit - Culture - or inherit culture from parent nodes. Will also apply
- to the current node, unless a domain below applies too.]]>
- Domains - - - Clear selection - Select - Do something else - Bold - Cancel Paragraph Indent - Insert form field - Insert graphic headline - Edit Html - Indent Paragraph - Italic - Center - Justify Left - Justify Right - Insert Link - Insert local link (anchor) - Bullet List - Numeric List - Insert macro - Insert picture - Publish and close - Publish with descendants - Edit relations - Return to list - Save - Save and close - Save and publish - Save and schedule - Send for approval - Save list view - Schedule - Preview - Preview is disabled because there's no template assigned - Choose style - Show styles - Insert table - Generate models and close - Save and generate models - Undo - Redo - Rollback - Delete tag - Cancel - Confirm - More publishing options - - - Viewing for - Content deleted - Content unpublished - Content unpublished for languages: %0% - Content published - Content published for languages: %0% - Content saved - Content saved for languages: %0% - Content moved - Content copied - Content rolled back - Content sent for publishing - Content sent for publishing for languages: %0% - Sort child items performed by user - Copy - Publish - Publish - Move - Save - Save - Delete - Unpublish - Unpublish - Rollback - Send To Publish - Send To Publish - Sort - History (all variants) - - - To change the document type for the selected content, first select from the list of valid types for this location. - Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. - The content has been re-published. - Current Property - Current type - The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. - Document Type Changed - Map Properties - Map to Property - New Template - New Type - none - Content - Select New Document Type - The document type of the selected content has been successfully changed to [new type] and the following properties mapped: - to - Could not complete property mapping as one or more properties have more than one mapping defined. - Only alternate types valid for the current location are displayed. - - - Failed to create a folder under parent with ID %0% - Failed to create a folder under parent with name %0% - The folder name cannot contain illegal characters. - Failed to delete item: %0% - - - Is Published - About this page - Alias - (how would you describe the picture over the phone) - Alternative Links - Click to edit this item - Created by - Original author - Updated by - Created - Date/time this document was created - Document Type - Editing - Remove at - This item has been changed after publication - This item is not published - Last published - There are no items to show - There are no items to show in the list. - No child items have been added - No members have been added - Media Type - Link to media item(s) - Member Group - Role - Member Type - No changes have been made - No date chosen - Page title - This media item has no link - No content can be added for this item - Properties - This document is published but is not visible because the parent '%0%' is unpublished - This culture is published but is not visible because it is unpublished on parent '%0%' - This document is published but is not in the cache - Could not get the url - This document is published but its url would collide with content %0% - This document is published but its url cannot be routed - Publish - Published - Published (pending changes)> - Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> - Publish at - Unpublish at - Clear Date - Set date - Sortorder is updated - To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting - Statistics - Title (optional) - Alternative text (optional) - Type - Unpublish - Draft - Not created - Last edited - Date/time this document was edited - Remove file(s) - Click here to remove the image from the media item - Click here to remove the file from the media item - Link to document - Member of group(s) - Not a member of group(s) - Child items - Target - This translates to the following time on the server: - What does this mean?]]> - Are you sure you want to delete this item? - Are you sure you want to delete all items? - Property %0% uses editor %1% which is not supported by Nested Content. - No content types are configured for this property. - Add element type - Select element type - Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. - Enter an angular expression to evaluate against each item for its name. Use - to display the item index - Add another text box - Remove this text box - Content root - Include drafts: also publish unpublished content items. - This value is hidden. If you need access to view this value please contact your website administrator. - This value is hidden. - What languages would you like to publish? All languages with content are saved! - What languages would you like to publish? - What languages would you like to save? - All languages with content are saved on creation! - What languages would you like to send for approval? - What languages would you like to schedule? - Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. - Published Languages - Unpublished Languages - Unmodified Languages - These languages haven't been created - Ready to Publish? - Ready to Save? - Send for approval - Select the date and time to publish and/or unpublish the content item. - Create new - Paste from clipboard - This item is in the Recycle Bin - - - Create a new Content Template from '%0%' - Blank - Select a Content Template - Content Template created - A Content Template was created from '%0%' - Another Content Template with the same name already exists - A Content Template is predefined content that an editor can select to use as the basis for creating new content - - - Click to upload - or click here to choose files - You can drag files here to upload. - Cannot upload this file, it does not have an approved file type - Max file size is - Media root - Failed to move media - Parent and destination folders cannot be the same - Failed to copy media - Failed to create a folder under parent id %0% - Failed to rename the folder with id %0% - Drag and drop your file(s) into the area - - - Create a new member - All Members - Member groups have no additional properties for editing. - - - Where do you want to create the new %0% - Create an item under - Select the document type you want to make a content template for - Enter a folder name - Choose a type and a title - Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - Document Types within the Settings section.]]> - The selected page in the content tree doesn't allow for any pages to be created below it. - Edit permissions for this document type - Create a new document type - Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> - Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - The selected media in the tree doesn't allow for any other media to be created below it. - Edit permissions for this media type Document Type without a template - New folder - New data type - New JavaScript file - New empty partial view - New partial view macro - New partial view from snippet - New partial view macro from snippet - New partial view macro (without macro) - New style sheet file - New Rich Text Editor style sheet file - - - Browse your website - - Hide - If Umbraco isn't opening, you might need to allow popups from this site - has opened in a new window - Restart - Visit - Welcome - - - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes - Publishing will make the selected items visible on the site. - Unpublishing will remove the selected items and all their descendants from the site. - Unpublishing will remove this page and all its descendants from the site. - You have unsaved changes. Making changes to the Document Type will discard the changes. - - - Done - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - - - Link title - Link - Anchor / querystring - Name - Close this window - Are you sure you want to delete - Are you sure you want to disable - Are you sure? - Are you sure? - Cut - Edit Dictionary Item - Edit Language - Edit selected media - Insert local link - Insert character - Insert graphic headline - Insert picture - Insert link - Click to add a Macro - Insert table - This will delete the language - Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt - Last Edited - Link - Internal link: - When using local links, insert "#" in front of link - Open in new window? - Macro Settings - This macro does not contain any properties you can edit - Paste - Edit permissions for - Set permissions for - Set permissions for %0% for user group %1% - Select the users groups you want to set permissions for - The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place - The recycle bin is now empty - When items are deleted from the recycle bin, they will be gone forever - regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> - Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' - Remove Macro - Required Field - Site is reindexed - The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished - The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. - Number of columns - Number of rows - Click on the image to see full size - Pick item - View Cache Item - Relate to original - Include descendants - The friendliest community - Link to page - Opens the linked document in a new window or tab - Link to media - Select content start node - Select media - Select media type - Select icon - Select item - Select link - Select macro - Select content - Select content type - Select media start node - Select member - Select member group - Select member type - Select node - Select sections - Select users - No icons were found - There are no parameters for this macro - There are no macros available to insert - External login providers - Exception Details - Stacktrace - Inner Exception - Link your - Un-link your - account - Select editor - Select snippet - This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. - - - There are no dictionary items. - - - %0%' below - ]]> - Culture Name - - Dictionary overview - - - Configured Searchers - Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) - Field values - Health status - The health status of the index and if it can be read - Indexers - Index info - Lists the properties of the index - Manage Examine's indexes - Allows you to view the details of each index and provides some tools for managing the indexes - Rebuild index - - Depending on how much content there is in your site this could take a while.
- It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. - ]]> -
- Searchers - Search the index and view the results - Tools - Tools to manage the index - fields - The index cannot be read and will need to be rebuilt - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation - This index cannot be rebuilt because it has no assigned - IIndexPopulator - - - Enter your username - Enter your password - Confirm your password - Name the %0%... - Enter a name... - Enter an email... - Enter a username... - Label... - Enter a description... - Type to search... - Type to filter... - Type to add tags (press enter after each tag)... - Enter your email - Enter a message... - Your username is usually your email - #value or ?key=value - Enter alias... - Generating alias... - - - Create custom list view - Remove custom list view - A content type, media type or member type with this alias already exists - - - Renamed - Enter a new folder name here - %0% was renamed to %1% - - - Add prevalue - Database datatype - Property editor GUID - Property editor - Buttons - Enable advanced settings for - Enable context menu - Maximum default size of inserted images - Related stylesheets - Show label - Width and height - All property types & property data - using this data type will be deleted permanently, please confirm you want to delete these as well - Yes, delete - and all property types & property data using this data type - Select the folder to move - to in the tree structure below - was moved underneath - %0% will delete the properties and their data from the following items]]> - I understand this action will delete the properties and data based on this Data Type - - - Your data has been saved, but before you can publish this page there are some errors you need to fix first: - The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) - %0% already exists - There were errors: - There were errors: - The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) - %0% must be an integer - The %0% field in the %1% tab is mandatory - %0% is a mandatory field - %0% at %1% is not in a correct format - %0% is not in a correct format - - - Received an error from the server - The specified file type has been disallowed by the administrator - NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. - Please fill both alias and name on the new property type! - There is a problem with read/write access to a specific file or folder - Error loading Partial View script (file: %0%) - Please enter a title - Please choose a type - You're about to make the picture larger than the original size. Are you sure that you want to proceed? - Startnode deleted, please contact your administrator - Please mark content before changing style - No active styles available - Please place cursor at the left of the two cells you wish to merge - You cannot split a cell that hasn't been merged. - This property is invalid - - - Options - About - Action - Actions - Add - Alias - All - Are you sure? - Back - Back to overview - Border - by - Cancel - Cell margin - Choose - Clear - Close - Close Window - Comment - Confirm - Constrain - Constrain proportions - Content - Continue - Copy - Create - Database - Date - Default - Delete - Deleted - Deleting... - Design - Dictionary - Dimensions - Down - Download - Edit - Edited - Elements - Email - Error - Field - Find - First - Focal point - General - Groups - Group - Height - Help - Hide - History - Icon - Id - Import - Include subfolders in search - Info - Inner margin - Insert - Install - Invalid - Justify - Label - Language - Last - Layout - Links - Loading - Locked - Login - Log off - Logout - Macro - Mandatory - Message - Move - Name - New - Next - No - of - Off - OK - Open - On - or - Order by - Password - Path - One moment please... - Previous - Properties - Rebuild - Email to receive form data - Recycle Bin - Your recycle bin is empty - Reload - Remaining - Remove - Rename - Renew - Required - Retrieve - Retry - Permissions - Scheduled Publishing - Search - Sorry, we can not find what you are looking for. - No items have been added - Server - Settings - Show - Show page on Send - Size - Sort - Status - Submit - Type - Type to search... - under - Up - Update - Upgrade - Upload - Url - User - Username - Value - View - Welcome... - Width - Yes - Folder - Search results - Reorder - I am done reordering - Preview - Change password - to - List view - Saving... - current - Embed - selected - Other - Articles - Videos - Clear - Installing - - - Blue - - - Add group - Add property - Add editor - Add template - Add child node - Add child - Edit data type - Navigate sections - Shortcuts - show shortcuts - Toggle list view - Toggle allow as root - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - General - Editor - Toggle allow culture variants - - - Background color - Bold - Text color - Font - Text - - - Page - - - The installer cannot connect to the database. - Could not save the web.config file. Please modify the connection string manually. - Your database has been found and is identified as - Database configuration - install button to install the Umbraco %0% database - ]]> - Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

-

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

-

- Click the retry button when - done.
- More information on editing web.config here.

]]>
- - Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - - Press the upgrade button to upgrade your database to Umbraco %0%

-

- Don't worry - no content will be deleted and everything will continue working afterwards! -

- ]]>
- Press Next to - proceed. ]]> - next to continue the configuration wizard]]> - The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> - The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> - The password is changed! - Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. - Not installed yet. - Affected files and folders - More information on setting up permissions for Umbraco here - You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

- You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
- How to Resolve - Click here to read the text version - video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - Your permission settings might be an issue! -

- You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
- Your permission settings are not ready for Umbraco! -

- In order to run Umbraco, you'll need to update your permission settings.]]>
- Your permission settings are perfect!

- You are ready to run Umbraco and install packages!]]>
- Resolving folder issue - Follow this link for more information on problems with ASP.NET and creating folders - Setting up folder permissions - - I want to start from scratch - learn how) - You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> - You've just set up a clean Umbraco platform. What do you want to do next? - Runway is installed - - This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> - Only recommended for experienced users - I want to start with a simple website - - "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, - but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, - Runway offers an easy foundation based on best practices to get you started faster than ever. - If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. -

- - Included with Runway: Home page, Getting Started page, Installing Modules page.
- Optional Modules: Top Navigation, Sitemap, Contact, Gallery. -
- ]]>
- What is Runway - Step 1/5 Accept license - Step 2/5: Database configuration - Step 3/5: Validating File Permissions - Step 4/5: Check Umbraco security - Step 5/5: Umbraco is ready to get you started - Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> - Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
- Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> - Connection to database failed. - Umbraco Version 3 - Umbraco Version 4 - Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. -

- Press "next" to start the wizard.]]>
- - - Culture Code - Culture Name - - - You've been idle and logout will automatically occur in - Renew now to save your work - - - Happy super Sunday - Happy manic Monday - Happy tubular Tuesday - Happy wonderful Wednesday - Happy thunderous Thursday - Happy funky Friday - Happy Caturday - Log in below - Sign in with - Session timed out - © 2001 - %0%
Umbraco.com

]]>
- Forgotten password? - An email will be sent to the address specified with a link to reset your password - An email with password reset instructions will be sent to the specified address if it matched our records - Show password - Hide password - Return to login form - Please provide a new password - Your Password has been updated - The link you have clicked on is invalid or has expired - Umbraco: Reset Password - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Password reset requested -

-

- Your username to login to the Umbraco back-office is: %0% -

-

- - - - - - -
- - Click this link to reset your password - -
-

-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %1% - -
-

-
-
-


-
-
- - - ]]>
- - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - %0%]]> - Notification settings saved for - - The following languages have been modified %0% - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

- - - - - - -
- -
- EDIT
-
-

-

Update summary:

- %6% -

-

- Have a nice day!

- Cheers from the Umbraco robot -

-
-
-


-
-
- - - ]]>
- The following languages have been modified:

- %0% - ]]>
- [%0%] Notification about %1% performed on %2% - Notifications - - - Actions - Created - Create package - - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - This will delete the package - Drop to upload - Include all child nodes - or click here to choose package file - Upload package - Install a local package by selecting it from your machine. Only install packages from sources you know and trust - Upload another package - Cancel and upload another package - I accept - terms of use - Path to file - Absolute path to file (ie: /bin/umbraco.bin) - Installed - Installed packages - Install local - Finish - This package has no configuration view - No packages have been created yet - You don’t have any packages installed - 'Packages' icon in the top right of your screen]]> - Package Actions - Author URL - Package Content - Package Files - Icon URL - Install package - License - License URL - Package Properties - Search for packages - Results for - We couldn’t find anything for - Please try searching for another package or browse through the categories - Popular - New releases - has - karma points - Information - Owner - Contributors - Created - Current version - .NET version - Downloads - Likes - Compatibility - 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 - Package meta data - Package name - Package doesn't contain any items -
- You can safely remove this from the system by clicking "uninstall package" below.]]>
- Package options - Package readme - Package repository - Confirm package uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - - Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - Package version - Upgrading from version - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click 'Finish' to complete installation and reload the page. - Uploading package... - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Group based protection - If you want to grant access to all members of specific member groups - You need to create a member group before you can use group based authentication - Error Page - Used when people are logged on, but do not have access - %0%]]> - %0% is now protected]]> - %0%]]> - Login Page - Choose the page that contains the login form - Remove protection... - %0%?]]> - Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Specific members protection - If you wish to grant access to specific members - - - Insufficient user permissions to publish all descendant documents - - - - - - - - Validation failed for required language '%0%'. This language was saved but not published. - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - Publish to publish %0% and thereby making its content publicly available.

- You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
- - - You have not configured any approved colors - - - You can only select items of type(s): %0% - You have picked a content item currently deleted or in the recycle bin - You have picked content items currently deleted or in the recycle bin - - - Deleted item - You have picked a media item currently deleted or in the recycle bin - You have picked media items currently deleted or in the recycle bin - Trashed - - - enter external link - choose internal page - Caption - Link - Open in new window - enter the display caption - Enter the link - - - Reset crop - Save crop - Add new crop - Done - Undo edits - - - Select a version to compare with the current version - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Content - Forms - Media - Members - Packages - Settings - Translation - Users - - - Tours - The best Umbraco video tutorials - Visit our.umbraco.com - Visit umbraco.tv - - - Default template - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items - - This node has no child nodes to sort - - - Validation - Validation errors must be fixed before the item can be saved - Failed - Saved - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Content published - and is visible on the website - %0% documents published and visible on the website - %0% published and visible on the website - %0% documents published for languages %1% and visible on the website - Content saved - Remember to publish to make changes visible - A schedule for publishing has been updated - %0% saved - Sent For Approval - Changes have been sent for approval - %0% changes have been sent for approval - Media saved - Media saved without any errors - Member saved - Member group saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - User group saved - Cultures and hostnames saved - Error saving cultures and hostnames - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Member Group saved - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - Content unpublished - Content variation %0% unpublished - The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Permissions saved for - Deleted %0% user groups - %0% was deleted - Enabled %0% users - Disabled %0% users - %0% is now enabled - %0% is now disabled - User groups have been set - Unlocked %0% users - %0% is now unlocked - Member was exported to file - An error occurred while exporting the member - User %0% was deleted - Invite user - Invitation has been re-sent to %0% - Cannot publish the document since the required '%0%' is not published - Validation failed for language '%0%' - Document type was exported to file - An error occurred while exporting the document type - The release date cannot be in the past - Cannot schedule the document for publishing since the required '%0%' is not published - Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language - The expire date cannot be in the past - The expire date cannot be before the release date - - - Add style - Edit style - Rich text editor styles - Define the styles that should be available in the rich text editor for this stylesheet - Edit stylesheet - Edit stylesheet property - The name displayed in the editor style selector - Preview - How the text will look like in the rich text editor. - Selector - Uses CSS syntax, e.g. "h1" or ".redHeader" - Styles - The CSS that should be applied in the rich text editor, e.g. "color:red;" - Code - Rich Text Editor - - - Failed to delete template with ID %0% - Edit template - Sections - Insert content area - Insert content area placeholder - Insert - Choose what to insert into your template - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - Master template - No master - Render child template - @RenderBody() placeholder. - ]]> - Define a named section - @section { ... }. This can be rendered in a - specific area of the parent of this template, by using @RenderSection. - ]]> - Render a named section - @RenderSection(name) placeholder. - This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - Query builder - items returned, in - copy to clipboard - I want - all content - content of type "%0%" - from - my website - where - and - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - Id - Name - Created Date - Last Updated Date - order by - ascending - descending - Template - - - Image - Macro - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - This content is not allowed here - This content is allowed here - Click to embed - Click to insert image - Image caption... - Write here... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - Columns - Total combined number of columns in the grid layout - Settings - Configure what settings editors can change - Styles - Configure what styling editors can change - Allow all editors - Allow all row configurations - Maximum items - Leave blank or set to 0 for unlimited - Set as default - Choose extra - Choose default - are added - Warning - You are deleting the row configuration - - Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. - - - - Compositions - Group - You have not added any groups - Add group - Inherited from - Add property - Required label - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - Allowed Templates - Choose which templates editors are allowed to use on content of this type - Allow as root - Allow editors to create content of this type in the root of the content tree. - Allowed child node types - Allow content of the specified types to be created underneath content of this type. - Choose child node - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - Removing a composition will delete all the associated property data. Once you save the document type there's no way back. - Create new - Use existing - Editor settings - Configuration - Yes, delete - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - All Document types - All Documents - All media items - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - and all documents using this type - and all media items using this type - and all members using this type - Member can edit - Allow this property value to be edited by the member on their profile page - Is sensitive data - Hide this property value from content editors that don't have access to view sensitive information - Show on member profile - Allow this property value to be displayed on the member profile page - tab has no sort order - Where is this composition used? - This composition is currently used in the composition of the following content types: - Allow varying by culture - Allow editors to create content of this type in different languages. - Allow varying by culture - Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree. - This is not applicable for an Element type - You have made changes to this property. Are you sure you want to discard them? - - - Add language - Mandatory language - Properties on this language have to be filled out before the node can be published. - Default language - An Umbraco site can only have one default language set. - Switching default language may result in default content missing. - Falls back to - No fall back language - To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. - Fall back language - none - - - Add parameter - Edit parameter - Enter macro name - Parameters - Define the parameters that should be available when using this macro. - Select partial view macro file - - - Building models - this can take a bit of time, don't worry - Models generated - Models could not be generated - Models generation has failed, see exception in U log - - - Add fallback field - Fallback field - Add default value - Default value - Fallback field - Default value - Casing - Encoding - Choose field - Convert line breaks - Yes, convert line breaks - Replaces line breaks with 'br' html tag - Custom Fields - Date only - Format and encoding - Format as date - Format the value as a date, or a date with time, according to the active culture - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - Modify output - None - Output sample - Insert after field - Insert before field - Recursive - Yes, make it recursive - Separator - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Date and time - - - Translation details - Download XML DTD - Fields - Include subpages - - No translator users found. Please create a translator user before you start sending content to translation - The page '%0%' has been send to translation - Send the page '%0%' to translation - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Content - Content Templates - Media - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Member Roles - Member Types - Document Types - Relation Types - Packages - Packages - Partial Views - Partial View Macro Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - Log Viewer - Users - Settings - Templating - Third Party - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Access - Based on the assigned groups and start nodes, the user has access to the following nodes - Assign access - Administrator - Category field - User created - Change Your Password - Change photo - New password - hasn't been locked out - The password hasn't been changed - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Create another user - Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. - Description field - Disable User - Document Type - Editor - Excerpt field - Failed login attempts - Go to user profile - Add groups to assign access and permissions - Invite another user - Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. - Language - Set the language you will see in menus and dialogs - Last lockout date - Last login - Password last changed - Username - Media start node - Limit the media library to a specific start node - Media start nodes - Limit the media library to specific start nodes - Sections - Disable Umbraco Access - has not logged in yet - Old password - Password - Reset password - Your password has been changed! - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Remove photo - Default permissions - Granular permissions - Set permissions for specific nodes - Profile - Search all children - Add sections to give users access - Select user groups - No start node selected - No start nodes selected - Content start node - Limit the content tree to a specific start node - Content start nodes - Limit the content tree to specific start nodes - User last updated - has been created - The new user has successfully been created. To log in to Umbraco use the password below. - User management - Name - User permissions - User group - has been invited - An invitation has been sent to the new user with details about how to log in to Umbraco. - Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. - Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. - Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. - Writer - Change - Your profile - Your recent history - Session expires in - Invite user - Create user - Send invite - Back to users - Umbraco: Invitation - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- You have been invited by %1% to the Umbraco Back Office. -

-

- Message from %1%: -
- %2% -

- - - - - - -
- - - - - - -
- - Click this link to accept the invite - -
-
-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %3% - -
-

-
-
-


-
-
- - ]]>
- Invite - Resending invitation... - Delete User - Are you sure you wish to delete this user account? - All - Active - Disabled - Locked out - Invited - Inactive - Name (A-Z) - Name (Z-A) - Newest - Oldest - Last login - No user groups have been added - - - Validation - Validate as an email address - Validate as a number - Validate as a URL - ...or enter a custom validation - Field is mandatory - Enter a custom validation error message (optional) - Enter a regular expression - Enter a custom validation error message (optional) - You need to add at least - You can only have - items - items selected - Invalid date - Not a number - Invalid email - Value cannot be null - Value cannot be empty - Value is invalid, it does not match the correct pattern - Custom validation - %1% more.]]> - %1% too many.]]> - - - - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - Database - The database schema is correct for this version of Umbraco - %0% problems were detected with your database schema (Check the log for details) - Some errors were detected while validating the database schema against the current version of Umbraco. - Your website's certificate is valid. - Certificate validation error: '%0%' - Your website's SSL certificate has expired. - Your website's SSL certificate is expiring in %0% days. - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - All folders have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> - Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. - A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. - Strict-Transport-Security, also known as the HSTS-header, was found.]]> - Strict-Transport-Security was not found.]]> - Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). - The HSTS header has been added to your web.config file. - X-XSS-Protection was found.]]> - X-XSS-Protection was not found.]]> - Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. - The X-XSS-Protection header has been added to your web.config file. - - %0%.]]> - No headers revealing information about the website technology were found. - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - %0%.]]> - %0%.]]> -

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
- Umbraco Health Check Status: %0% - Check All Groups - Check group - - The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks.

- ]]> -
- - - Disable URL tracker - Enable URL tracker - Culture - Original URL - Redirected To - Redirect Url Management - The following URLs redirect to this content item: - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - This will remove the redirect - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - - - %0% characters left.]]> - %1% too many.]]> - - - Trashed content with Id: {0} related to original parent content with Id: {1} - Trashed media with Id: {0} related to original parent media item with Id: {1} - Cannot automatically restore this item - There is no location where this item can be automatically restored. You can move the item manually using the tree below. - was restored under - - - Direction - Parent to child - Bidirectional - Parent - Child - Count - Relations - Created - Comment - Name - No relations for this relation type. - Relation Type - Relations - - - Getting Started - Redirect URL Management - Content - Welcome - Examine Management - Published Status - Models Builder - Health Check - Profiling - Getting Started - Install Umbraco Forms - - - Go back - Active layout: - Jump to - group - passed - warning - failed - suggestion - Check passed - Check failed - Open backoffice search - Open/Close backoffice help - Open/Close your profile options - Open context menu for - Current language - Switch language to - Create new folder - Partial View - Partial View Macro - Member - Data type - Search the redirect dashboard - Search the user group section - Search the users section - Create item - Create - Edit - Name - - - References - This Data Type has no references. - Used in Document Types - No references to Document Types. - Used in Media Types - No references to Media Types. - Used in Member Types - No references to Member Types. - Used by - - - Log Levels - Saved Searches - Total Items - Timestamp - Level - Machine - Message - Exception - Properties - Search With Google - Search this message with Google - Search With Bing - Search this message with Bing - Search Our Umbraco - Search this message on Our Umbraco forums and docs - Search Our Umbraco with Google - Search Our Umbraco forums using Google - Search Umbraco Source - Search within Umbraco source code on Github - Search Umbraco Issues - Search Umbraco Issues on Github - Delete this search - Find Logs with Request ID - Find Logs with Namespace - Find Logs with Machine Name - Open - - - Copy %0% - %0% from %1% - Remove all items - - - Open Property Actions - - - Wait - Refresh status - Memory Cache - - - - Reload - Database Cache - - Rebuilding can be expensive. - Use it when reloading is not enough, and you think that the database cache has not been - properly generated—which would indicate some critical Umbraco issue. - ]]> - - Rebuild - Internals - - not need to use it. - ]]> - - Collect - Published Cache Status - Caches - - - Performance profiling - - - Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. -

-

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

-

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

- ]]> -
- Activate the profiler by default - Friendly reminder - - - You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. -

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

-

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

- ]]> -
- - - Hours of Umbraco training videos are only a click away - - Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

- ]]> -
- To get you started - - - Start here - This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section - Find out more - - in the Documentation section of Our Umbraco - ]]> - - - Community Forum - ]]> - - - tutorial videos (some are free, some require a subscription) - ]]> - - - productivity boosting tools and commercial support - ]]> - - - training and certification opportunities - ]]> - - - - Welcome to The Friendly CMS - Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - - - Umbraco Forms - Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! - -
+ + + + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Culture and Hostnames + Audit Trail + Browse Node + Change Document Type + Copy + Create + Export + Create Package + Create group + Delete + Disable + Empty recycle bin + Enable + Export Document Type + Import Document Type + Import Package + Edit in Canvas + Exit + Move + Notifications + Public access + Publish + Unpublish + Reload + Republish entire site + Rename + Restore + Set permissions for the page %0% + Choose where to copy + Choose where to move + to in the tree structure below + Choose where to copy the selected item(s) + Choose where to move the selected item(s) + was moved to + was copied to + was deleted + Permissions + Rollback + Send To Publish + Send To Translation + Set group + Sort + Translate + Update + Set permissions + Unlock + Create Content Template + Resend Invitation + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to unpublish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template + + + Content + Info + + + Permission denied. + Add new Domain + remove + Invalid node. + One or more domains have an invalid format. + Domain has already been assigned. + Language + Domain + New domain '%0%' has been created + Domain '%0%' is deleted + Domain '%0%' has already been assigned + Domain '%0%' has been updated + Edit Current Domains + + + Inherit + Culture + or inherit culture from parent nodes. Will also apply
+ to the current node, unless a domain below applies too.]]>
+ Domains + + + Clear selection + Select + Do something else + Bold + Cancel Paragraph Indent + Insert form field + Insert graphic headline + Edit Html + Indent Paragraph + Italic + Center + Justify Left + Justify Right + Insert Link + Insert local link (anchor) + Bullet List + Numeric List + Insert macro + Insert picture + Publish and close + Publish with descendants + Edit relations + Return to list + Save + Save and close + Save and publish + Save and schedule + Send for approval + Save list view + Schedule + Preview + Preview is disabled because there's no template assigned + Choose style + Show styles + Insert table + Generate models and close + Save and generate models + Undo + Redo + Rollback + Delete tag + Cancel + Confirm + More publishing options + + + Viewing for + Content deleted + Content unpublished + Content unpublished for languages: %0% + Content published + Content published for languages: %0% + Content saved + Content saved for languages: %0% + Content moved + Content copied + Content rolled back + Content sent for publishing + Content sent for publishing for languages: %0% + Sort child items performed by user + Copy + Publish + Publish + Move + Save + Save + Delete + Unpublish + Unpublish + Rollback + Send To Publish + Send To Publish + Sort + History (all variants) + + + To change the document type for the selected content, first select from the list of valid types for this location. + Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. + The content has been re-published. + Current Property + Current type + The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. + Document Type Changed + Map Properties + Map to Property + New Template + New Type + none + Content + Select New Document Type + The document type of the selected content has been successfully changed to [new type] and the following properties mapped: + to + Could not complete property mapping as one or more properties have more than one mapping defined. + Only alternate types valid for the current location are displayed. + + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + The folder name cannot contain illegal characters. + Failed to delete item: %0% + + + Is Published + About this page + Alias + (how would you describe the picture over the phone) + Alternative Links + Click to edit this item + Created by + Original author + Updated by + Created + Date/time this document was created + Document Type + Editing + Remove at + This item has been changed after publication + This item is not published + Last published + There are no items to show + There are no items to show in the list. + No child items have been added + No members have been added + Media Type + Link to media item(s) + Member Group + Role + Member Type + No changes have been made + No date chosen + Page title + This media item has no link + No content can be added for this item + Properties + This document is published but is not visible because the parent '%0%' is unpublished + This culture is published but is not visible because it is unpublished on parent '%0%' + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% + This document is published but its url cannot be routed + Publish + Published + Published (pending changes)> + Publication Status + Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> + Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + Publish at + Unpublish at + Clear Date + Set date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Draft + Not created + Last edited + Date/time this document was edited + Remove file(s) + Click here to remove the image from the media item + Click here to remove the file from the media item + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]> + Are you sure you want to delete this item? + Are you sure you want to delete all items? + Property %0% uses editor %1% which is not supported by Nested Content. + No content types are configured for this property. + Add element type + Select element type + Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. + Enter an angular expression to evaluate against each item for its name. Use + to display the item index + Add another text box + Remove this text box + Content root + Include drafts: also publish unpublished content items. + This value is hidden. If you need access to view this value please contact your website administrator. + This value is hidden. + What languages would you like to publish? All languages with content are saved! + What languages would you like to publish? + What languages would you like to save? + All languages with content are saved on creation! + What languages would you like to send for approval? + What languages would you like to schedule? + Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. + Published Languages + Unpublished Languages + Unmodified Languages + These languages haven't been created + Ready to Publish? + Ready to Save? + Send for approval + Select the date and time to publish and/or unpublish the content item. + Create new + Paste from clipboard + This item is in the Recycle Bin + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is predefined content that an editor can select to use as the basis for creating new content + + + Click to upload + or click here to choose files + You can drag files here to upload. + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + Failed to move media + Parent and destination folders cannot be the same + Failed to copy media + Failed to create a folder under parent id %0% + Failed to rename the folder with id %0% + Drag and drop your file(s) into the area + + + Create a new member + All Members + Member groups have no additional properties for editing. + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Enter a folder name + Choose a type and a title + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type Document Type without a template + New folder + New data type + New JavaScript file + New empty partial view + New partial view macro + New partial view from snippet + New partial view macro from snippet + New partial view macro (without macro) + New style sheet file + New Rich Text Editor style sheet file + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Publishing will make the selected items visible on the site. + Unpublishing will remove the selected items and all their descendants from the site. + Unpublishing will remove this page and all its descendants from the site. + You have unsaved changes. Making changes to the Document Type will discard the changes. + + + Done + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Anchor / querystring + Name + Close this window + Are you sure you want to delete + Are you sure you want to disable + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Edit selected media + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + Click on the image to see full size + Pick item + View Cache Item + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Select content start node + Select media + Select media type + Select icon + Select item + Select link + Select macro + Select content + Select content type + Select media start node + Select member + Select member group + Select member type + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select snippet + This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. + + + There are no dictionary items. + + + %0%' below + ]]> + Culture Name + + Dictionary overview + + + Configured Searchers + Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) + Field values + Health status + The health status of the index and if it can be read + Indexers + Index info + Lists the properties of the index + Manage Examine's indexes + Allows you to view the details of each index and provides some tools for managing the indexes + Rebuild index + + Depending on how much content there is in your site this could take a while.
+ It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. + ]]> +
+ Searchers + Search the index and view the results + Tools + Tools to manage the index + fields + The index cannot be read and will need to be rebuilt + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + This index cannot be rebuilt because it has no assigned + IIndexPopulator + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email + Enter a message... + Your username is usually your email + #value or ?key=value + Enter alias... + Generating alias... + + + Create custom list view + Remove custom list view + A content type, media type or member type with this alias already exists + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + %0% will delete the properties and their data from the following items]]> + I understand this action will delete the properties and data based on this Data Type + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + This property is invalid + + + Options + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Back to overview + Border + by + Cancel + Cell margin + Choose + Clear + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Content + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Field + Find + First + Focal point + General + Groups + Group + Height + Help + Hide + History + Icon + Id + Import + Include subfolders in search + Info + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Links + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + Name + New + Next + No + of + Off + OK + Open + On + or + Order by + Password + Path + One moment please... + Previous + Properties + Rebuild + Email to receive form data + Recycle Bin + Your recycle bin is empty + Reload + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Scheduled Publishing + Search + Sorry, we can not find what you are looking for. + No items have been added + Server + Settings + Show + Show page on Send + Size + Sort + Status + Submit + Type + Type to search... + under + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + selected + Other + Articles + Videos + Clear + Installing + + + Blue + + + Add group + Add property + Add editor + Add template + Add child node + Add child + Edit data type + Navigate sections + Shortcuts + show shortcuts + Toggle list view + Toggle allow as root + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + General + Editor + Toggle allow culture variants + + + Background color + Bold + Text color + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + install button to install the Umbraco %0% database + ]]> + Next to proceed.]]> + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

+

To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

+

+ Click the retry button when + done.
+ More information on editing web.config here.

]]>
+ + Please contact your ISP if necessary. + If you're installing on a local machine or server you might need information from your system administrator.]]> + + Press the upgrade button to upgrade your database to Umbraco %0%

+

+ Don't worry - no content will be deleted and everything will continue working afterwards! +

+ ]]>
+ Press Next to + proceed. ]]> + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + Your permission settings are almost perfect!

+ You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
+ How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + Your permission settings might be an issue! +

+ You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
+ Your permission settings are not ready for Umbraco! +

+ In order to run Umbraco, you'll need to update your permission settings.]]>
+ Your permission settings are perfect!

+ You are ready to run Umbraco and install packages!]]>
+ Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + I want to start from scratch + learn how) + You can still choose to install Runway later on. Please go to the Developer section and choose Packages. + ]]> + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules + ]]> + Only recommended for experienced users + I want to start with a simple website + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + Runway offers an easy foundation based on best practices to get you started faster than ever. + If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. +

+ + Included with Runway: Home page, Getting Started page, Installing Modules page.
+ Optional Modules: Top Navigation, Sitemap, Contact, Gallery. +
+ ]]>
+ What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + Umbraco %0% is installed and ready for use + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]>
+ Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + Umbraco %0% for a fresh install or upgrading from version 3.0. +

+ Press "next" to start the wizard.]]>
+ + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Password reset requested +

+

+ Your username to login to the Umbraco back-office is: %0% +

+

+ + + + + + +
+ + Click this link to reset your password + +
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %1% + +
+

+
+
+


+
+
+ + + ]]>
+ + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + %0%]]> + Notification settings saved for + + The following languages have been modified %0% + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

+ + + + + + +
+ +
+ EDIT
+
+

+

Update summary:

+ %6% +

+

+ Have a nice day!

+ Cheers from the Umbraco robot +

+
+
+


+
+
+ + + ]]>
+ The following languages have been modified:

+ %0% + ]]>
+ [%0%] Notification about %1% performed on %2% + Notifications + + + Actions + Created + Create package + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + This will delete the package + Drop to upload + Include all child nodes + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + I accept + terms of use + Path to file + Absolute path to file (ie: /bin/umbraco.bin) + Installed + Installed packages + Install local + Finish + This package has no configuration view + No packages have been created yet + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Package Actions + Author URL + Package Content + Package Files + Icon URL + Install package + License + License URL + Package Properties + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + 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 + Package meta data + Package name + Package doesn't contain any items +
+ You can safely remove this from the system by clicking "uninstall package" below.]]>
+ Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, + so uninstall with caution. If in doubt, contact the package author.]]> + Package version + Upgrading from version + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication + Error Page + Used when people are logged on, but do not have access + %0%]]> + %0% is now protected]]> + %0%]]> + Login Page + Choose the page that contains the login form + Remove protection... + %0%?]]> + Select the pages that contain login form and error messages + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members + + + Insufficient user permissions to publish all descendant documents + + + + + + + + Validation failed for required language '%0%'. This language was saved but not published. + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + Publish to publish %0% and thereby making its content publicly available.

+ You can publish this page and all its subpages by checking Include unpublished subpages below. + ]]>
+ + + You have not configured any approved colors + + + You can only select items of type(s): %0% + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + Deleted item + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Trashed + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset crop + Save crop + Add new crop + Done + Undo edits + + + Select a version to compare with the current version + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Content + Forms + Media + Members + Packages + Settings + Translation + Users + + + Tours + The best Umbraco video tutorials + Visit our.umbraco.com + Visit umbraco.tv + + + Default template + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + This node has no child nodes to sort + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Content published + and is visible on the website + %0% documents published and visible on the website + %0% published and visible on the website + %0% documents published for languages %1% and visible on the website + Content saved + Remember to publish to make changes visible + A schedule for publishing has been updated + %0% saved + Sent For Approval + Changes have been sent for approval + %0% changes have been sent for approval + Media saved + Media saved without any errors + Member saved + Member group saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + Cultures and hostnames saved + Error saving cultures and hostnames + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Member Group saved + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + Content unpublished + Content variation %0% unpublished + The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Deleted %0% user groups + %0% was deleted + Enabled %0% users + Disabled %0% users + %0% is now enabled + %0% is now disabled + User groups have been set + Unlocked %0% users + %0% is now unlocked + Member was exported to file + An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% + Cannot publish the document since the required '%0%' is not published + Validation failed for language '%0%' + Document type was exported to file + An error occurred while exporting the document type + The release date cannot be in the past + Cannot schedule the document for publishing since the required '%0%' is not published + Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language + The expire date cannot be in the past + The expire date cannot be before the release date + + + Add style + Edit style + Rich text editor styles + Define the styles that should be available in the rich text editor for this stylesheet + Edit stylesheet + Edit stylesheet property + The name displayed in the editor style selector + Preview + How the text will look like in the rich text editor. + Selector + Uses CSS syntax, e.g. "h1" or ".redHeader" + Styles + The CSS that should be applied in the rich text editor, e.g. "color:red;" + Code + Rich Text Editor + + + Failed to delete template with ID %0% + Edit template + Sections + Insert content area + Insert content area placeholder + Insert + Choose what to insert into your template + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template + No master + Render child template + @RenderBody() placeholder. + ]]> + Define a named section + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + Render a named section + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + Query builder + items returned, in + copy to clipboard + I want + all content + content of type "%0%" + from + my website + where + and + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + Id + Name + Created Date + Last Updated Date + order by + ascending + descending + Template + + + Image + Macro + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + This content is not allowed here + This content is allowed here + Click to embed + Click to insert image + Image caption... + Write here... + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + Columns + Total combined number of columns in the grid layout + Settings + Configure what settings editors can change + Styles + Configure what styling editors can change + Allow all editors + Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited + Set as default + Choose extra + Choose default + are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + + + + Compositions + Group + You have not added any groups + Add group + Inherited from + Add property + Required label + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree. + Allowed child node types + Allow content of the specified types to be created underneath content of this type. + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. + Create new + Use existing + Editor settings + Configuration + Yes, delete + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + All Document types + All Documents + All media items + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + and all documents using this type + and all media items using this type + and all members using this type + Member can edit + Allow this property value to be edited by the member on their profile page + Is sensitive data + Hide this property value from content editors that don't have access to view sensitive information + Show on member profile + Allow this property value to be displayed on the member profile page + tab has no sort order + Where is this composition used? + This composition is currently used in the composition of the following content types: + Allow varying by culture + Allow editors to create content of this type in different languages. + Allow varying by culture + Element type + Is an Element type + An Element type is meant to be used for instance in Nested Content, and not in the tree. + This is not applicable for an Element type + You have made changes to this property. Are you sure you want to discard them? + + + Add language + Mandatory language + Properties on this language have to be filled out before the node can be published. + Default language + An Umbraco site can only have one default language set. + Switching default language may result in default content missing. + Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language + none + + + Add parameter + Edit parameter + Enter macro name + Parameters + Define the parameters that should be available when using this macro. + Select partial view macro file + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Translation details + Download XML DTD + Fields + Include subpages + + No translator users found. Please create a translator user before you start sending content to translation + The page '%0%' has been send to translation + Send the page '%0%' to translation + Total words + Translate to + Translation completed. + You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. + Translation failed, the XML file might be corrupt + Translation options + Translator + Upload translation XML + + + Content + Content Templates + Media + Cache Browser + Recycle Bin + Created packages + Data Types + Dictionary + Installed packages + Install skin + Install starter kit + Languages + Install local package + Macros + Media Types + Members + Member Groups + Member Roles + Member Types + Document Types + Relation Types + Packages + Packages + Partial Views + Partial View Macro Files + Install from repository + Install Runway + Runway modules + Scripting Files + Scripts + Stylesheets + Templates + Log Viewer + Users + Settings + Templating + Third Party + + + New update ready + %0% is ready, click here for download + No connection to server + Error checking for update. Please review trace-stack for further information + + + Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access + Administrator + Category field + User created + Change Your Password + Change photo + New password + hasn't been locked out + The password hasn't been changed + Confirm new password + You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button + Content Channel + Create another user + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. + Description field + Disable User + Document Type + Editor + Excerpt field + Failed login attempts + Go to user profile + Add groups to assign access and permissions + Invite another user + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. + Language + Set the language you will see in menus and dialogs + Last lockout date + Last login + Password last changed + Username + Media start node + Limit the media library to a specific start node + Media start nodes + Limit the media library to specific start nodes + Sections + Disable Umbraco Access + has not logged in yet + Old password + Password + Reset password + Your password has been changed! + Please confirm the new password + Enter your new password + Your new password cannot be blank! + Current password + Invalid current password + There was a difference between the new password and the confirmed password. Please try again! + The confirmed password doesn't match the new password! + Replace child node permissions + You are currently modifying permissions for the pages: + Select pages to modify their permissions + Remove photo + Default permissions + Granular permissions + Set permissions for specific nodes + Profile + Search all children + Add sections to give users access + Select user groups + No start node selected + No start nodes selected + Content start node + Limit the content tree to a specific start node + Content start nodes + Limit the content tree to specific start nodes + User last updated + has been created + The new user has successfully been created. To log in to Umbraco use the password below. + User management + Name + User permissions + User group + has been invited + An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. + Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. + Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. + Writer + Change + Your profile + Your recent history + Session expires in + Invite user + Create user + Send invite + Back to users + Umbraco: Invitation + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ You have been invited by %1% to the Umbraco Back Office. +

+

+ Message from %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Click this link to accept the invite + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ + %3% + +
+

+
+
+


+
+
+ + ]]>
+ Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? + All + Active + Disabled + Locked out + Invited + Inactive + Name (A-Z) + Name (Z-A) + Newest + Oldest + Last login + No user groups have been added + + + Validation + Validate as an email address + Validate as a number + Validate as a URL + ...or enter a custom validation + Field is mandatory + Enter a custom validation error message (optional) + Enter a regular expression + Enter a custom validation error message (optional) + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + Value cannot be null + Value cannot be empty + Value is invalid, it does not match the correct pattern + Custom validation + %1% more.]]> + %1% too many.]]> + + + + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + All folders have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + All files have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. + A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + Strict-Transport-Security was not found.]]> + Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). + The HSTS header has been added to your web.config file. + X-XSS-Protection was found.]]> + X-XSS-Protection was not found.]]> + Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. + The X-XSS-Protection header has been added to your web.config file. + + %0%.]]> + No headers revealing information about the website technology were found. + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + %0%.]]> + %0%.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
+ Umbraco Health Check Status: %0% + Check All Groups + Check group + + The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

+ ]]> +
+ + + Disable URL tracker + Enable URL tracker + Culture + Original URL + Redirected To + Redirect Url Management + The following URLs redirect to this content item: + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + This will remove the redirect + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + %0% characters left.]]> + %1% too many.]]> + + + Trashed content with Id: {0} related to original parent content with Id: {1} + Trashed media with Id: {0} related to original parent media item with Id: {1} + Cannot automatically restore this item + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under + + + Direction + Parent to child + Bidirectional + Parent + Child + Count + Relations + Created + Comment + Name + No relations for this relation type. + Relation Type + Relations + + + Getting Started + Redirect URL Management + Content + Welcome + Examine Management + Published Status + Models Builder + Health Check + Profiling + Getting Started + Install Umbraco Forms + + + Go back + Active layout: + Jump to + group + passed + warning + failed + suggestion + Check passed + Check failed + Open backoffice search + Open/Close backoffice help + Open/Close your profile options + Open context menu for + Current language + Switch language to + Create new folder + Partial View + Partial View Macro + Member + Data type + Search the redirect dashboard + Search the user group section + Search the users section + Create item + Create + Edit + Name + + + References + This Data Type has no references. + Used in Document Types + No references to Document Types. + Used in Media Types + No references to Media Types. + Used in Member Types + No references to Member Types. + Used by + + + Log Levels + Saved Searches + Total Items + Timestamp + Level + Machine + Message + Exception + Properties + Search With Google + Search this message with Google + Search With Bing + Search this message with Bing + Search Our Umbraco + Search this message on Our Umbraco forums and docs + Search Our Umbraco with Google + Search Our Umbraco forums using Google + Search Umbraco Source + Search within Umbraco source code on Github + Search Umbraco Issues + Search Umbraco Issues on Github + Delete this search + Find Logs with Request ID + Find Logs with Namespace + Find Logs with Machine Name + Open + + + Copy %0% + %0% from %1% + Remove all items + + + Open Property Actions + + + Wait + Refresh status + Memory Cache + + + + Reload + Database Cache + + Rebuilding can be expensive. + Use it when reloading is not enough, and you think that the database cache has not been + properly generated—which would indicate some critical Umbraco issue. + ]]> + + Rebuild + Internals + + not need to use it. + ]]> + + Collect + Published Cache Status + Caches + + + Performance profiling + + + Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

+

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

+

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

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

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

+

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

+ ]]> +
+ + + Hours of Umbraco training videos are only a click away + + Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

+ ]]> +
+ To get you started + + + Start here + This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section + Find out more + + in the Documentation section of Our Umbraco + ]]> + + + Community Forum + ]]> + + + tutorial videos (some are free, some require a subscription) + ]]> + + + productivity boosting tools and commercial support + ]]> + + + training and certification opportunities + ]]> + + + + Welcome to The Friendly CMS + Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. + + + Umbraco Forms + Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + +
From e85640c48338a2e407b181930b940ef53ab3fb39 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 15:15:04 +0100 Subject: [PATCH 199/610] V8: Set tab focus on newly created nested content items (#6914) * Set tab focus on newly created nested content items * Fix bad merge * Undo unintentional change --- .../nestedcontent/nestedcontent.propertyeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index 47293e433b..91afbf3ba9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -8,7 +8,7 @@
-
+
From dbe088eedb89b18a3813af1ae523c01e6e93b75d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 15:36:54 +0100 Subject: [PATCH 200/610] V8: Add infinite editing to datatype references tab (#6907) * Add infinite editing to datatype references tab * Use correct save button label in infinite editing mode --- .../src/common/services/editor.service.js | 18 ++++++ .../views/datatype.info.controller.js | 56 ++++++++++++++++++- .../views/datatypes/views/datatype.info.html | 6 +- .../src/views/membertypes/edit.controller.js | 34 ++++++++--- .../src/views/membertypes/edit.html | 11 +++- 5 files changed, 112 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 1d80d3a3ed..284a7db4d8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -640,6 +640,23 @@ When building a custom infinite editor view you can use the same components as a editor.view = "views/mediatypes/edit.html"; open(editor); } + + /** + * @ngdoc method + * @name umbraco.services.editorService#memberTypeEditor + * @methodOf umbraco.services.editorService + * + * @description + * Opens the member type editor in infinite editing, the submit callback returns the saved member type + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ + function memberTypeEditor(editor) { + editor.view = "views/membertypes/edit.html"; + open(editor); + } /** * @ngdoc method @@ -1011,6 +1028,7 @@ When building a custom infinite editor view you can use the same components as a iconPicker: iconPicker, documentTypeEditor: documentTypeEditor, mediaTypeEditor: mediaTypeEditor, + memberTypeEditor: memberTypeEditor, queryBuilder: queryBuilder, treePicker: treePicker, nodePermissions: nodePermissions, diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js index d4aff871a2..be8ddba592 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js @@ -6,7 +6,7 @@ * @description * The controller for the info view of the datatype editor */ -function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout) { +function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout, editorService) { var vm = this; var evts = []; @@ -17,6 +17,9 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe vm.view = {}; vm.view.loading = true; + vm.openDocumentType = openDocumentType; + vm.openMediaType = openMediaType; + vm.openMemberType = openMemberType; /** Loads in the data type references one time */ function loadRelations() { @@ -31,6 +34,57 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe } } + function openDocumentType(id, event) { + open(id, event, "documentType"); + } + + function openMediaType(id, event) { + open(id, event, "mediaType"); + } + + function openMemberType(id, event) { + open(id, event, "memberType"); + } + + function open(id, event, type) { + // targeting a new tab/window? + if (event.ctrlKey || + event.shiftKey || + event.metaKey || // apple + (event.button && event.button === 1) // middle click, >IE9 + everyone else + ) { + // yes, let the link open itself + return; + } + event.stopPropagation(); + event.preventDefault(); + + const editor = { + id: id, + submit: function (model) { + editorService.close(); + vm.view.loading = true; + referencesLoaded = false; + loadRelations(); + }, + close: function () { + editorService.close(); + } + }; + + switch (type) { + case "documentType": + editorService.documentTypeEditor(editor); + break; + case "mediaType": + editorService.mediaTypeEditor(editor); + break; + case "memberType": + editorService.memberTypeEditor(editor); + break; + } + } + // load data type references when the references tab is activated evts.push(eventsService.on("app.tabChange", function (event, args) { $timeout(function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html index 16b2d4b263..0af1634b29 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html @@ -43,7 +43,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
@@ -73,7 +73,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
@@ -104,7 +104,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index ab54a453b5..b2e515e187 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js @@ -13,8 +13,13 @@ var evts = []; var vm = this; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var memberTypeId = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; vm.save = save; + vm.close = close; + vm.editorfor = "visuallyHiddenTexts_newMember"; vm.header = {}; vm.header.editorfor = "content_membergroup"; @@ -25,6 +30,7 @@ vm.page.loading = false; vm.page.saveButtonState = "init"; vm.labels = {}; + vm.saveButtonKey = infiniteMode ? "buttons_saveAndClose" : "buttons_save"; var labelKeys = [ "general_design", @@ -86,7 +92,7 @@ vm.page.defaultButton = { hotKey: "ctrl+s", hotKeyWhenHidden: true, - labelKey: "buttons_save", + labelKey: vm.saveButtonKey, letter: "S", type: "submit", handler: function () { vm.save(); } @@ -94,7 +100,7 @@ vm.page.subButtons = [{ hotKey: "ctrl+g", hotKeyWhenHidden: true, - labelKey: "buttons_saveAndGenerateModels", + labelKey: infiniteMode ? "buttons_generateModelsAndClose" : "buttons_saveAndGenerateModels", letter: "G", handler: function () { @@ -147,12 +153,12 @@ } }); - if ($routeParams.create) { + if (create) { vm.page.loading = true; //we are creating so get an empty data type item - memberTypeResource.getScaffold($routeParams.id) + memberTypeResource.getScaffold(memberTypeId) .then(function (dt) { init(dt); @@ -163,10 +169,12 @@ vm.page.loading = true; - memberTypeResource.getById($routeParams.id).then(function (dt) { + memberTypeResource.getById(memberTypeId).then(function (dt) { init(dt); - syncTreeNode(vm.contentType, dt.path, true); + if(!infiniteMode) { + syncTreeNode(vm.contentType, dt.path, true); + } vm.page.loading = false; }); @@ -219,10 +227,16 @@ } }).then(function (data) { //success - syncTreeNode(vm.contentType, data.path); + if(!infiniteMode) { + syncTreeNode(vm.contentType, data.path); + } vm.page.saveButtonState = "success"; + if(infiniteMode && $scope.model.submit) { + $scope.model.submit(); + } + deferred.resolve(data); }, function (err) { //error @@ -307,6 +321,12 @@ }); } + + function close() { + if(infiniteMode && $scope.model.close) { + $scope.model.close(); + } + } evts.push(eventsService.on("editors.groupsBuilder.changed", function(name, args) { angularHelper.getCurrentForm($scope).$setDirty(); diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html index 07824bc7ec..c4c521c857 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html @@ -40,6 +40,14 @@ + + + + label-key="{{vm.saveButtonKey}}"> Date: Thu, 16 Jan 2020 16:11:08 +0100 Subject: [PATCH 201/610] Responsive Angular API documentation (#7030) --- src/Umbraco.Web.UI.Docs/umb-docs.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Umbraco.Web.UI.Docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css index 931c22b255..0f2e3e7f74 100644 --- a/src/Umbraco.Web.UI.Docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -7,6 +7,21 @@ body { } +.container, .navbar-static-top .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container { + max-width: 1500px; + width: 95%; +} + +.span3 { + width: 220px; + width: calc(90% / 12 * 3); +} + +.span9 { + width: 700px; + width: calc(90% / 12 * 9); +} + .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { font-family: inherit; font-weight: 400; From 4c68f7e53a324aae9fb0dbc57de5f0ab967e4bde Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Thu, 16 Jan 2020 16:13:25 +0100 Subject: [PATCH 202/610] List view configuration - Sort order distance fix (#6905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks, Jan. You're a 🌟 --- .../lib/bootstrap/less/type.less | 4 ++++ .../listview/orderDirection.prevalues.html | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less index bf1167f950..3f93deaf56 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less @@ -132,6 +132,10 @@ ol.inline { display: inline-block; padding-left: 5px; padding-right: 5px; + + &.-no-padding-left{ + padding-left: 0; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html index 83a905ccf7..7ec0895936 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html @@ -1,10 +1,16 @@ 
- - +
    +
  • + +
  • +
  • + +
  • +
- Required + Required
From d4e6eb2b6b052b79d34d095f2405b21f15618ede Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Jan 2020 17:04:38 +0100 Subject: [PATCH 203/610] AB4084 - Backported/reimplemented fix for contentservice returning outdated results --- .../Cache/DistributedCacheExtensions.cs | 930 ++++---- src/Umbraco.Web/Cache/MediaCacheRefresher.cs | 390 +-- .../Cache/UnpublishedPageCacheRefresher.cs | 46 +- .../XmlPublishedCache/PublishedMediaCache.cs | 2086 ++++++++--------- src/Umbraco.Web/Search/ExamineEvents.cs | 1465 ++++++------ 5 files changed, 2482 insertions(+), 2435 deletions(-) diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 849cd6c81a..321e710d8e 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -1,465 +1,465 @@ -using System; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Events; -using Umbraco.Core.Models; -using umbraco; -using umbraco.cms.businesslogic.web; -using Umbraco.Core.Persistence.Repositories; - -namespace Umbraco.Web.Cache -{ - /// - /// Extension methods for - /// - internal static class DistributedCacheExtensions - { - #region Public access - - public static void RefreshPublicAccess(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.PublicAccessCacheRefresherGuid); - } - - #endregion - - #region Application tree cache - - public static void RefreshAllApplicationTreeCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.ApplicationTreeCacheRefresherGuid); - } - - #endregion - - #region Application cache - - public static void RefreshAllApplicationCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.ApplicationCacheRefresherGuid); - } - - #endregion - - #region User cache - - public static void RemoveUserCache(this DistributedCache dc, int userId) - { - dc.Remove(DistributedCache.UserCacheRefresherGuid, userId); - } - - public static void RefreshUserCache(this DistributedCache dc, int userId) - { - dc.Refresh(DistributedCache.UserCacheRefresherGuid, userId); - } - - public static void RefreshAllUserCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.UserCacheRefresherGuid); - } - - #endregion - - #region User group cache - - public static void RemoveUserGroupCache(this DistributedCache dc, int userId) - { - dc.Remove(DistributedCache.UserGroupCacheRefresherGuid, userId); - } - - public static void RefreshUserGroupCache(this DistributedCache dc, int userId) - { - dc.Refresh(DistributedCache.UserGroupCacheRefresherGuid, userId); - } - - public static void RefreshAllUserGroupCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.UserGroupCacheRefresherGuid); - } - - #endregion - - #region User group permissions cache - - public static void RemoveUserGroupPermissionsCache(this DistributedCache dc, int groupId) - { - dc.Remove(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); - } - - public static void RefreshUserGroupPermissionsCache(this DistributedCache dc, int groupId) - { - //TODO: Not sure if we need this yet depends if we start caching permissions - //dc.Refresh(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); - } - - public static void RefreshAllUserGroupPermissionsCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.UserGroupPermissionsCacheRefresherGuid); - } - - #endregion - - #region Template cache - - public static void RefreshTemplateCache(this DistributedCache dc, int templateId) - { - dc.Refresh(DistributedCache.TemplateRefresherGuid, templateId); - } - - public static void RemoveTemplateCache(this DistributedCache dc, int templateId) - { - dc.Remove(DistributedCache.TemplateRefresherGuid, templateId); - } - - #endregion - - #region Dictionary cache - - public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Refresh(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); - } - - public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Remove(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); - } - - #endregion - - #region Data type cache - - public static void RefreshDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) - { - if (dataType == null) return; - dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } - - public static void RemoveDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) - { - if (dataType == null) return; - dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } - - #endregion - - #region Page cache - - public static void RefreshAllPageCache(this DistributedCache dc) - { - dc.RefreshAll(DistributedCache.PageCacheRefresherGuid); - } - - public static void RefreshPageCache(this DistributedCache dc, int documentId) - { - dc.Refresh(DistributedCache.PageCacheRefresherGuid, documentId); - } - - public static void RefreshPageCache(this DistributedCache dc, params IContent[] content) - { - dc.Refresh(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); - } - - public static void RemovePageCache(this DistributedCache dc, params IContent[] content) - { - dc.Remove(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); - } - - public static void RemovePageCache(this DistributedCache dc, int documentId) - { - dc.Remove(DistributedCache.PageCacheRefresherGuid, documentId); - } - - public static void RefreshUnpublishedPageCache(this DistributedCache dc, params IContent[] content) - { - dc.Refresh(DistributedCache.UnpublishedPageCacheRefresherGuid, x => x.Id, content); - } - - public static void RemoveUnpublishedPageCache(this DistributedCache dc, params IContent[] content) - { - dc.Remove(DistributedCache.UnpublishedPageCacheRefresherGuid, x => x.Id, content); - } - - public static void RemoveUnpublishedCachePermanently(this DistributedCache dc, params int[] contentIds) - { - dc.RefreshByJson(DistributedCache.UnpublishedPageCacheRefresherGuid, UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds)); - } - - #endregion - - #region Member cache - - public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) - { - dc.Refresh(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); - } - - public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) - { - dc.Remove(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); - } - - [Obsolete("Use the RefreshMemberCache with strongly typed IMember objects instead")] - public static void RefreshMemberCache(this DistributedCache dc, int memberId) - { - dc.Refresh(DistributedCache.MemberCacheRefresherGuid, memberId); - } - - [Obsolete("Use the RemoveMemberCache with strongly typed IMember objects instead")] - public static void RemoveMemberCache(this DistributedCache dc, int memberId) - { - dc.Remove(DistributedCache.MemberCacheRefresherGuid, memberId); - } - - #endregion - - #region Member group cache - - public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Refresh(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); - } - - public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Remove(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); - } - - #endregion - - #region Media Cache - - public static void RefreshMediaCache(this DistributedCache dc, params IMedia[] media) - { - dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); - } - - public static void RefreshMediaCacheAfterMoving(this DistributedCache dc, params MoveEventInfo[] media) - { - dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Saved, media)); - } - - // clearing by Id will never work for load balanced scenarios for media since we require a Path - // to clear all of the cache but the media item will be removed before the other servers can - // look it up. Only here for legacy purposes. - [Obsolete("Ensure to clear with other RemoveMediaCache overload")] - public static void RemoveMediaCache(this DistributedCache dc, int mediaId) - { - dc.Remove(new Guid(DistributedCache.MediaCacheRefresherId), mediaId); - } - - public static void RemoveMediaCacheAfterRecycling(this DistributedCache dc, params MoveEventInfo[] media) - { - dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Trashed, media)); - } - - public static void RemoveMediaCachePermanently(this DistributedCache dc, params int[] mediaIds) - { - dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(mediaIds)); - } - - #endregion - - #region Macro Cache - - public static void ClearAllMacroCacheOnCurrentServer(this DistributedCache dc) - { - var macroRefresher = CacheRefreshersResolver.Current.GetById(DistributedCache.MacroCacheRefresherGuid); - macroRefresher.RefreshAll(); - } - - public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) return; - dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); - } - - public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) return; - dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); - } - - public static void RefreshMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) - { - if (macro == null) return; - dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); - } - - public static void RemoveMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) - { - if (macro == null) return; - dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); - } - - public static void RemoveMacroCache(this DistributedCache dc, macro macro) - { - if (macro == null || macro.Model == null) return; - dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); - } - - #endregion - - #region Document type cache - - public static void RefreshContentTypeCache(this DistributedCache dc, IContentType contentType) - { - if (contentType == null) return; - dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, contentType)); - } - - public static void RemoveContentTypeCache(this DistributedCache dc, IContentType contentType) - { - if (contentType == null) return; - dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, contentType)); - } - - #endregion - - #region Media type cache - - public static void RefreshMediaTypeCache(this DistributedCache dc, IMediaType mediaType) - { - if (mediaType == null) return; - dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, mediaType)); - } - - public static void RemoveMediaTypeCache(this DistributedCache dc, IMediaType mediaType) - { - if (mediaType == null) return; - dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, mediaType)); - } - - #endregion - - #region Media type cache - - public static void RefreshMemberTypeCache(this DistributedCache dc, IMemberType memberType) - { - if (memberType == null) return; - dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, memberType)); - } - - public static void RemoveMemberTypeCache(this DistributedCache dc, IMemberType memberType) - { - if (memberType == null) return; - dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, memberType)); - } - - #endregion - - #region Stylesheet Cache - - public static void RefreshStylesheetPropertyCache(this DistributedCache dc, global::umbraco.cms.businesslogic.web.StylesheetProperty styleSheetProperty) - { - if (styleSheetProperty == null) return; - dc.Refresh(DistributedCache.StylesheetPropertyCacheRefresherGuid, styleSheetProperty.Id); - } - - public static void RemoveStylesheetPropertyCache(this DistributedCache dc, global::umbraco.cms.businesslogic.web.StylesheetProperty styleSheetProperty) - { - if (styleSheetProperty == null) return; - dc.Remove(DistributedCache.StylesheetPropertyCacheRefresherGuid, styleSheetProperty.Id); - } - - public static void RefreshStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) - { - if (styleSheet == null) return; - dc.Refresh(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); - } - - public static void RemoveStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) - { - if (styleSheet == null) return; - dc.Remove(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); - } - - public static void RefreshStylesheetCache(this DistributedCache dc, Umbraco.Core.Models.Stylesheet styleSheet) - { - if (styleSheet == null) return; - dc.Refresh(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); - } - - public static void RemoveStylesheetCache(this DistributedCache dc, Umbraco.Core.Models.Stylesheet styleSheet) - { - if (styleSheet == null) return; - dc.Remove(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); - } - - #endregion - - #region Domain Cache - - public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) return; - dc.Refresh(DistributedCache.DomainCacheRefresherGuid, domain.Id); - } - - public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) return; - dc.Remove(DistributedCache.DomainCacheRefresherGuid, domain.Id); - } - - public static void ClearDomainCacheOnCurrentServer(this DistributedCache dc) - { - var domainRefresher = CacheRefreshersResolver.Current.GetById(DistributedCache.DomainCacheRefresherGuid); - domainRefresher.RefreshAll(); - } - - #endregion - - #region Language Cache - - public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) return; - dc.Refresh(DistributedCache.LanguageCacheRefresherGuid, language.Id); - } - - public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) return; - dc.Remove(DistributedCache.LanguageCacheRefresherGuid, language.Id); - } - - public static void RefreshLanguageCache(this DistributedCache dc, global::umbraco.cms.businesslogic.language.Language language) - { - if (language == null) return; - dc.Refresh(DistributedCache.LanguageCacheRefresherGuid, language.id); - } - - public static void RemoveLanguageCache(this DistributedCache dc, global::umbraco.cms.businesslogic.language.Language language) - { - if (language == null) return; - dc.Remove(DistributedCache.LanguageCacheRefresherGuid, language.id); - } - - #endregion - - #region Xslt Cache - - public static void ClearXsltCacheOnCurrentServer(this DistributedCache dc) - { - if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration <= 0) return; - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); - } - - #endregion - - #region Relation type cache - - public static void RefreshRelationTypeCache(this DistributedCache dc, int id) - { - dc.Refresh(DistributedCache.RelationTypeCacheRefresherGuid, id); - } - - public static void RemoveRelationTypeCache(this DistributedCache dc, int id) - { - dc.Remove(DistributedCache.RelationTypeCacheRefresherGuid, id); - } - - #endregion - } -} \ No newline at end of file +using System; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using umbraco; +using umbraco.cms.businesslogic.web; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Web.Cache +{ + /// + /// Extension methods for + /// + internal static class DistributedCacheExtensions + { + #region Public access + + public static void RefreshPublicAccess(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.PublicAccessCacheRefresherGuid); + } + + #endregion + + #region Application tree cache + + public static void RefreshAllApplicationTreeCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.ApplicationTreeCacheRefresherGuid); + } + + #endregion + + #region Application cache + + public static void RefreshAllApplicationCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.ApplicationCacheRefresherGuid); + } + + #endregion + + #region User cache + + public static void RemoveUserCache(this DistributedCache dc, int userId) + { + dc.Remove(DistributedCache.UserCacheRefresherGuid, userId); + } + + public static void RefreshUserCache(this DistributedCache dc, int userId) + { + dc.Refresh(DistributedCache.UserCacheRefresherGuid, userId); + } + + public static void RefreshAllUserCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.UserCacheRefresherGuid); + } + + #endregion + + #region User group cache + + public static void RemoveUserGroupCache(this DistributedCache dc, int userId) + { + dc.Remove(DistributedCache.UserGroupCacheRefresherGuid, userId); + } + + public static void RefreshUserGroupCache(this DistributedCache dc, int userId) + { + dc.Refresh(DistributedCache.UserGroupCacheRefresherGuid, userId); + } + + public static void RefreshAllUserGroupCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.UserGroupCacheRefresherGuid); + } + + #endregion + + #region User group permissions cache + + public static void RemoveUserGroupPermissionsCache(this DistributedCache dc, int groupId) + { + dc.Remove(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); + } + + public static void RefreshUserGroupPermissionsCache(this DistributedCache dc, int groupId) + { + //TODO: Not sure if we need this yet depends if we start caching permissions + //dc.Refresh(DistributedCache.UserGroupPermissionsCacheRefresherGuid, groupId); + } + + public static void RefreshAllUserGroupPermissionsCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.UserGroupPermissionsCacheRefresherGuid); + } + + #endregion + + #region Template cache + + public static void RefreshTemplateCache(this DistributedCache dc, int templateId) + { + dc.Refresh(DistributedCache.TemplateRefresherGuid, templateId); + } + + public static void RemoveTemplateCache(this DistributedCache dc, int templateId) + { + dc.Remove(DistributedCache.TemplateRefresherGuid, templateId); + } + + #endregion + + #region Dictionary cache + + public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) + { + dc.Refresh(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); + } + + public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) + { + dc.Remove(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); + } + + #endregion + + #region Data type cache + + public static void RefreshDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) + { + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); + } + + public static void RemoveDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) + { + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); + } + + #endregion + + #region Page cache + + public static void RefreshAllPageCache(this DistributedCache dc) + { + dc.RefreshAll(DistributedCache.PageCacheRefresherGuid); + } + + public static void RefreshPageCache(this DistributedCache dc, int documentId) + { + dc.Refresh(DistributedCache.PageCacheRefresherGuid, documentId); + } + + public static void RefreshPageCache(this DistributedCache dc, params IContent[] content) + { + dc.Refresh(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); + } + + public static void RemovePageCache(this DistributedCache dc, params IContent[] content) + { + dc.Remove(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); + } + + public static void RemovePageCache(this DistributedCache dc, int documentId) + { + dc.Remove(DistributedCache.PageCacheRefresherGuid, documentId); + } + + public static void RefreshUnpublishedPageCache(this DistributedCache dc, params IContent[] content) + { + dc.RefreshByJson(DistributedCache.UnpublishedPageCacheRefresherGuid, UnpublishedPageCacheRefresher.SerializeToJsonPayload(UnpublishedPageCacheRefresher.OperationType.Refresh, content)); + } + + public static void RemoveUnpublishedPageCache(this DistributedCache dc, params IContent[] content) + { + dc.RefreshByJson(DistributedCache.UnpublishedPageCacheRefresherGuid, UnpublishedPageCacheRefresher.SerializeToJsonPayload(UnpublishedPageCacheRefresher.OperationType.Deleted, content)); + } + + public static void RemoveUnpublishedCachePermanently(this DistributedCache dc, params int[] contentIds) + { + dc.RefreshByJson(DistributedCache.UnpublishedPageCacheRefresherGuid, UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds)); + } + + #endregion + + #region Member cache + + public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + { + dc.Refresh(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); + } + + public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) + { + dc.Remove(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); + } + + [Obsolete("Use the RefreshMemberCache with strongly typed IMember objects instead")] + public static void RefreshMemberCache(this DistributedCache dc, int memberId) + { + dc.Refresh(DistributedCache.MemberCacheRefresherGuid, memberId); + } + + [Obsolete("Use the RemoveMemberCache with strongly typed IMember objects instead")] + public static void RemoveMemberCache(this DistributedCache dc, int memberId) + { + dc.Remove(DistributedCache.MemberCacheRefresherGuid, memberId); + } + + #endregion + + #region Member group cache + + public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) + { + dc.Refresh(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); + } + + public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) + { + dc.Remove(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); + } + + #endregion + + #region Media Cache + + public static void RefreshMediaCache(this DistributedCache dc, params IMedia[] media) + { + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); + } + + public static void RefreshMediaCacheAfterMoving(this DistributedCache dc, params MoveEventInfo[] media) + { + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Saved, media)); + } + + // clearing by Id will never work for load balanced scenarios for media since we require a Path + // to clear all of the cache but the media item will be removed before the other servers can + // look it up. Only here for legacy purposes. + [Obsolete("Ensure to clear with other RemoveMediaCache overload")] + public static void RemoveMediaCache(this DistributedCache dc, int mediaId) + { + dc.Remove(new Guid(DistributedCache.MediaCacheRefresherId), mediaId); + } + + public static void RemoveMediaCacheAfterRecycling(this DistributedCache dc, params MoveEventInfo[] media) + { + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Trashed, media)); + } + + public static void RemoveMediaCachePermanently(this DistributedCache dc, params int[] mediaIds) + { + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(mediaIds)); + } + + #endregion + + #region Macro Cache + + public static void ClearAllMacroCacheOnCurrentServer(this DistributedCache dc) + { + var macroRefresher = CacheRefreshersResolver.Current.GetById(DistributedCache.MacroCacheRefresherGuid); + macroRefresher.RefreshAll(); + } + + public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); + } + + public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); + } + + public static void RefreshMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) + { + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); + } + + public static void RemoveMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) + { + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); + } + + public static void RemoveMacroCache(this DistributedCache dc, macro macro) + { + if (macro == null || macro.Model == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); + } + + #endregion + + #region Document type cache + + public static void RefreshContentTypeCache(this DistributedCache dc, IContentType contentType) + { + if (contentType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, contentType)); + } + + public static void RemoveContentTypeCache(this DistributedCache dc, IContentType contentType) + { + if (contentType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, contentType)); + } + + #endregion + + #region Media type cache + + public static void RefreshMediaTypeCache(this DistributedCache dc, IMediaType mediaType) + { + if (mediaType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, mediaType)); + } + + public static void RemoveMediaTypeCache(this DistributedCache dc, IMediaType mediaType) + { + if (mediaType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, mediaType)); + } + + #endregion + + #region Media type cache + + public static void RefreshMemberTypeCache(this DistributedCache dc, IMemberType memberType) + { + if (memberType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, memberType)); + } + + public static void RemoveMemberTypeCache(this DistributedCache dc, IMemberType memberType) + { + if (memberType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, memberType)); + } + + #endregion + + #region Stylesheet Cache + + public static void RefreshStylesheetPropertyCache(this DistributedCache dc, global::umbraco.cms.businesslogic.web.StylesheetProperty styleSheetProperty) + { + if (styleSheetProperty == null) return; + dc.Refresh(DistributedCache.StylesheetPropertyCacheRefresherGuid, styleSheetProperty.Id); + } + + public static void RemoveStylesheetPropertyCache(this DistributedCache dc, global::umbraco.cms.businesslogic.web.StylesheetProperty styleSheetProperty) + { + if (styleSheetProperty == null) return; + dc.Remove(DistributedCache.StylesheetPropertyCacheRefresherGuid, styleSheetProperty.Id); + } + + public static void RefreshStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) + { + if (styleSheet == null) return; + dc.Refresh(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); + } + + public static void RemoveStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) + { + if (styleSheet == null) return; + dc.Remove(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); + } + + public static void RefreshStylesheetCache(this DistributedCache dc, Umbraco.Core.Models.Stylesheet styleSheet) + { + if (styleSheet == null) return; + dc.Refresh(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); + } + + public static void RemoveStylesheetCache(this DistributedCache dc, Umbraco.Core.Models.Stylesheet styleSheet) + { + if (styleSheet == null) return; + dc.Remove(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); + } + + #endregion + + #region Domain Cache + + public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) return; + dc.Refresh(DistributedCache.DomainCacheRefresherGuid, domain.Id); + } + + public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) return; + dc.Remove(DistributedCache.DomainCacheRefresherGuid, domain.Id); + } + + public static void ClearDomainCacheOnCurrentServer(this DistributedCache dc) + { + var domainRefresher = CacheRefreshersResolver.Current.GetById(DistributedCache.DomainCacheRefresherGuid); + domainRefresher.RefreshAll(); + } + + #endregion + + #region Language Cache + + public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) return; + dc.Refresh(DistributedCache.LanguageCacheRefresherGuid, language.Id); + } + + public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) return; + dc.Remove(DistributedCache.LanguageCacheRefresherGuid, language.Id); + } + + public static void RefreshLanguageCache(this DistributedCache dc, global::umbraco.cms.businesslogic.language.Language language) + { + if (language == null) return; + dc.Refresh(DistributedCache.LanguageCacheRefresherGuid, language.id); + } + + public static void RemoveLanguageCache(this DistributedCache dc, global::umbraco.cms.businesslogic.language.Language language) + { + if (language == null) return; + dc.Remove(DistributedCache.LanguageCacheRefresherGuid, language.id); + } + + #endregion + + #region Xslt Cache + + public static void ClearXsltCacheOnCurrentServer(this DistributedCache dc) + { + if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration <= 0) return; + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); + } + + #endregion + + #region Relation type cache + + public static void RefreshRelationTypeCache(this DistributedCache dc, int id) + { + dc.Refresh(DistributedCache.RelationTypeCacheRefresherGuid, id); + } + + public static void RemoveRelationTypeCache(this DistributedCache dc, int id) + { + dc.Remove(DistributedCache.RelationTypeCacheRefresherGuid, id); + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index 783ca95841..bdaf9aa9e3 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -1,194 +1,196 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Web.Script.Serialization; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Events; -using Umbraco.Core.IO; -using Umbraco.Core.Models; - -using Umbraco.Core.Persistence.Repositories; -using umbraco.interfaces; -using System.Linq; -using Newtonsoft.Json; -using Umbraco.Web.PublishedCache.XmlPublishedCache; - -namespace Umbraco.Web.Cache -{ - /// - /// A cache refresher to ensure media cache is updated - /// - /// - /// This is not intended to be used directly in your code and it should be sealed but due to legacy code we cannot seal it. - /// - public class MediaCacheRefresher : JsonCacheRefresherBase - { - #region Static helpers - - /// - /// Converts the json to a JsonPayload object - /// - /// - /// - public static JsonPayload[] DeserializeFromJsonPayload(string json) - { - var jsonObject = JsonConvert.DeserializeObject(json); - return jsonObject; - } - - /// - /// Creates the custom Json payload used to refresh cache amongst the servers - /// - /// - /// - /// - internal static string SerializeToJsonPayload(OperationType operation, params IMedia[] media) - { - var items = media.Select(x => FromMedia(x, operation)).ToArray(); - var json = JsonConvert.SerializeObject(items); - return json; - } - - internal static string SerializeToJsonPayloadForMoving(OperationType operation, MoveEventInfo[] media) - { - var items = media.Select(x => new JsonPayload - { - Id = x.Entity.Id, - Operation = operation, - Path = x.OriginalPath - }).ToArray(); - var json = JsonConvert.SerializeObject(items); - return json; - } - - internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] mediaIds) - { - var items = mediaIds.Select(x => new JsonPayload - { - Id = x, - Operation = OperationType.Deleted - }).ToArray(); - var json = JsonConvert.SerializeObject(items); - return json; - } - - /// - /// Converts a macro to a jsonPayload object - /// - /// - /// - /// - internal static JsonPayload FromMedia(IMedia media, OperationType operation) - { - if (media == null) return null; - - var payload = new JsonPayload - { - Id = media.Id, - Path = media.Path, - Operation = operation - }; - return payload; - } - - #endregion - - #region Sub classes - - public enum OperationType - { - Saved, - Trashed, - Deleted - } - - public class JsonPayload - { - public string Path { get; set; } - public int Id { get; set; } - public OperationType Operation { get; set; } - } - - #endregion - - protected override MediaCacheRefresher Instance - { - get { return this; } - } - - public override Guid UniqueIdentifier - { - get { return new Guid(DistributedCache.MediaCacheRefresherId); } - } - - public override string Name - { - get { return "Clears Media Cache from umbraco.library"; } - } - - public override void Refresh(string jsonPayload) - { - ClearCache(DeserializeFromJsonPayload(jsonPayload)); - base.Refresh(jsonPayload); - } - - public override void Refresh(int id) - { - ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), OperationType.Saved)); - base.Refresh(id); - } - - public override void Remove(int id) - { - ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), - //NOTE: we'll just default to trashed for this one. - OperationType.Trashed)); - base.Remove(id); - } - - private static void ClearCache(params JsonPayload[] payloads) - { - if (payloads == null) return; - - ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); - - foreach (var payload in payloads) - { - if (payload.Operation == OperationType.Deleted) - ApplicationContext.Current.Services.IdkMap.ClearCache(payload.Id); - - var mediaCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); - - //if there's no path, then just use id (this will occur on permanent deletion like emptying recycle bin) - if (payload.Path.IsNullOrWhiteSpace()) - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); - } - else - { - foreach (var idPart in payload.Path.Split(',')) - { - int idPartAsInt; - if (int.TryParse(idPart, out idPartAsInt) && mediaCache) - { - mediaCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(idPartAsInt)); - } - - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); - - // Also clear calls that only query this specific item! - if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); - } - } - - // published cache... - PublishedMediaCache.ClearCache(payload.Id); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Web.Script.Serialization; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Models; + +using Umbraco.Core.Persistence.Repositories; +using umbraco.interfaces; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Web.PublishedCache.XmlPublishedCache; + +namespace Umbraco.Web.Cache +{ + /// + /// A cache refresher to ensure media cache is updated + /// + /// + /// This is not intended to be used directly in your code and it should be sealed but due to legacy code we cannot seal it. + /// + public class MediaCacheRefresher : JsonCacheRefresherBase + { + #region Static helpers + + /// + /// Converts the json to a JsonPayload object + /// + /// + /// + public static JsonPayload[] DeserializeFromJsonPayload(string json) + { + var jsonObject = JsonConvert.DeserializeObject(json); + return jsonObject; + } + + /// + /// Creates the custom Json payload used to refresh cache amongst the servers + /// + /// + /// + /// + internal static string SerializeToJsonPayload(OperationType operation, params IMedia[] media) + { + var items = media.Select(x => FromMedia(x, operation)).ToArray(); + var json = JsonConvert.SerializeObject(items); + return json; + } + + internal static string SerializeToJsonPayloadForMoving(OperationType operation, MoveEventInfo[] media) + { + var items = media.Select(x => new JsonPayload + { + Id = x.Entity.Id, + Operation = operation, + Path = x.OriginalPath + }).ToArray(); + var json = JsonConvert.SerializeObject(items); + return json; + } + + internal static string SerializeToJsonPayloadForPermanentDeletion(params int[] mediaIds) + { + var items = mediaIds.Select(x => new JsonPayload + { + Id = x, + Operation = OperationType.Deleted + }).ToArray(); + var json = JsonConvert.SerializeObject(items); + return json; + } + + /// + /// Converts a macro to a jsonPayload object + /// + /// + /// + /// + internal static JsonPayload FromMedia(IMedia media, OperationType operation) + { + if (media == null) return null; + + var payload = new JsonPayload + { + Id = media.Id, + Key = media.Key, + Path = media.Path, + Operation = operation + }; + return payload; + } + + #endregion + + #region Sub classes + + public enum OperationType + { + Saved, + Trashed, + Deleted + } + + public class JsonPayload + { + public string Path { get; set; } + public int Id { get; set; } + public OperationType Operation { get; set; } + public Guid? Key { get; set; } + } + + #endregion + + protected override MediaCacheRefresher Instance + { + get { return this; } + } + + public override Guid UniqueIdentifier + { + get { return new Guid(DistributedCache.MediaCacheRefresherId); } + } + + public override string Name + { + get { return "Clears Media Cache from umbraco.library"; } + } + + public override void Refresh(string jsonPayload) + { + ClearCache(DeserializeFromJsonPayload(jsonPayload)); + base.Refresh(jsonPayload); + } + + public override void Refresh(int id) + { + ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), OperationType.Saved)); + base.Refresh(id); + } + + public override void Remove(int id) + { + ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), + //NOTE: we'll just default to trashed for this one. + OperationType.Trashed)); + base.Remove(id); + } + + private static void ClearCache(params JsonPayload[] payloads) + { + if (payloads == null) return; + + ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); + + foreach (var payload in payloads) + { + if (payload.Operation == OperationType.Deleted) + ApplicationContext.Current.Services.IdkMap.ClearCache(payload.Id); + + var mediaCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + mediaCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Key)); + //if there's no path, then just use id (this will occur on permanent deletion like emptying recycle bin) + if (payload.Path.IsNullOrWhiteSpace()) + { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( + string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); + } + else + { + foreach (var idPart in payload.Path.Split(',')) + { + int idPartAsInt; + if (int.TryParse(idPart, out idPartAsInt) && mediaCache) + { + mediaCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(idPartAsInt)); + } + + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( + string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); + + // Also clear calls that only query this specific item! + if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( + string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); + } + } + + // published cache... + PublishedMediaCache.ClearCache(payload.Id); + } + } + } +} diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index bf58454bfc..4fc38a27a4 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -56,6 +56,17 @@ namespace Umbraco.Web.Cache var json = JsonConvert.SerializeObject(items); return json; } + internal static string SerializeToJsonPayload(OperationType operationType, params IContent[] contents) + { + var items = contents.Select(x => new JsonPayload + { + Id = x.Id, + Key = x.Key, + Operation = operationType + }).ToArray(); + var json = JsonConvert.SerializeObject(items); + return json; + } #endregion @@ -63,12 +74,14 @@ namespace Umbraco.Web.Cache internal enum OperationType { - Deleted + Deleted, + Refresh } internal class JsonPayload { public int Id { get; set; } + public Guid? Key { get; set; } public OperationType Operation { get; set; } } @@ -139,10 +152,26 @@ namespace Umbraco.Web.Cache foreach (var payload in DeserializeFromJsonPayload(jsonPayload)) { - ApplicationContext.Current.Services.IdkMap.ClearCache(payload.Id); ClearRepositoryCacheItemById(payload.Id); - content.Instance.UpdateSortOrder(payload.Id); - content.Instance.ClearPreviewXmlContent(payload.Id); + ClearRepositoryCacheItemById(payload.Key); + ClearAllIsolatedCacheByEntityType(); + + if (payload.Operation == OperationType.Deleted) + { + ApplicationContext.Current.Services.IdkMap.ClearCache(payload.Id); + content.Instance.ClearPreviewXmlContent(payload.Id); + base.Remove(payload.Id); + } + + if (payload.Operation == OperationType.Refresh) + { + content.Instance.UpdateSortOrder(payload.Id); + var d = new Document(payload.Id); + content.Instance.UpdateDocumentCache(d); + content.Instance.UpdatePreviewXmlContent(d); + + base.Refresh(payload.Id); + } } DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); @@ -158,5 +187,14 @@ namespace Umbraco.Web.Cache contentCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); } } + + private void ClearRepositoryCacheItemById(Guid? key) + { + var contentCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (contentCache) + { + contentCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(key)); + } + } } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index 3bc0ecb7cd..14ee38031d 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -1,1047 +1,1047 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Configuration; -using System.IO; -using System.Linq; -using System.Threading; -using System.Xml.XPath; -using Examine; -using Examine.LuceneEngine.SearchCriteria; -using Examine.Providers; -using Lucene.Net.Documents; -using Lucene.Net.Store; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Dynamics; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Xml; -using Umbraco.Web.Models; -using UmbracoExamine; -using umbraco; -using Umbraco.Core.Cache; -using Umbraco.Core.Sync; -using Umbraco.Web.Cache; - -namespace Umbraco.Web.PublishedCache.XmlPublishedCache -{ - /// - /// An IPublishedMediaStore that first checks for the media in Examine, and then reverts to the database - /// - /// - /// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly. - /// - internal class PublishedMediaCache : IPublishedMediaCache - { - public PublishedMediaCache(ApplicationContext applicationContext) - { - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - _applicationContext = applicationContext; - } - - /// - /// Generally used for unit testing to use an explicit examine searcher - /// - /// - /// - /// - internal PublishedMediaCache(ApplicationContext applicationContext, BaseSearchProvider searchProvider, BaseIndexProvider indexProvider) - { - if (applicationContext == null) throw new ArgumentNullException("applicationContext"); - if (searchProvider == null) throw new ArgumentNullException("searchProvider"); - if (indexProvider == null) throw new ArgumentNullException("indexProvider"); - - _applicationContext = applicationContext; - _searchProvider = searchProvider; - _indexProvider = indexProvider; - } - - static PublishedMediaCache() - { - InitializeCacheConfig(); - } - - private readonly ApplicationContext _applicationContext; - private readonly BaseSearchProvider _searchProvider; - private readonly BaseIndexProvider _indexProvider; - - public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int nodeId) - { - return GetUmbracoMedia(nodeId); - } - - public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, Guid nodeKey) - { - // TODO optimize with Examine? - var mapAttempt = ApplicationContext.Current.Services.IdkMap.GetIdForKey(nodeKey, UmbracoObjectTypes.Media); - return mapAttempt ? GetById(umbracoContext, preview, mapAttempt.Result) : null; - } - - public virtual IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview) - { - var searchProvider = GetSearchProviderSafe(); - - if (searchProvider != null) - { - try - { - // first check in Examine for the cache values - // +(+parentID:-1) +__IndexType:media - - var criteria = searchProvider.CreateSearchCriteria("media"); - var filter = criteria.ParentId(-1).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); - - var result = searchProvider.Search(filter.Compile()); - if (result != null) - return result.Select(x => CreateFromCacheValues(ConvertFromSearchResult(x))); - } - catch (Exception ex) - { - if (ex is FileNotFoundException) - { - //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco - //See this thread: http://examine.cdodeplex.com/discussions/264341 - //Catch the exception here for the time being, and just fallback to GetMedia - //TODO: Need to fix examine in LB scenarios! - LogHelper.Error("Could not load data from Examine index for media", ex); - } - else if (ex is AlreadyClosedException) - { - //If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot - //be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db. - LogHelper.Error("Could not load data from Examine index for media, the app domain is most likely in a shutdown state", ex); - } - else throw; - } +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Threading; +using System.Xml.XPath; +using Examine; +using Examine.LuceneEngine.SearchCriteria; +using Examine.Providers; +using Lucene.Net.Documents; +using Lucene.Net.Store; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Dynamics; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Xml; +using Umbraco.Web.Models; +using UmbracoExamine; +using umbraco; +using Umbraco.Core.Cache; +using Umbraco.Core.Sync; +using Umbraco.Web.Cache; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + /// + /// An IPublishedMediaStore that first checks for the media in Examine, and then reverts to the database + /// + /// + /// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly. + /// + internal class PublishedMediaCache : IPublishedMediaCache + { + public PublishedMediaCache(ApplicationContext applicationContext) + { + if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + _applicationContext = applicationContext; + } + + /// + /// Generally used for unit testing to use an explicit examine searcher + /// + /// + /// + /// + internal PublishedMediaCache(ApplicationContext applicationContext, BaseSearchProvider searchProvider, BaseIndexProvider indexProvider) + { + if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + if (searchProvider == null) throw new ArgumentNullException("searchProvider"); + if (indexProvider == null) throw new ArgumentNullException("indexProvider"); + + _applicationContext = applicationContext; + _searchProvider = searchProvider; + _indexProvider = indexProvider; + } + + static PublishedMediaCache() + { + InitializeCacheConfig(); + } + + private readonly ApplicationContext _applicationContext; + private readonly BaseSearchProvider _searchProvider; + private readonly BaseIndexProvider _indexProvider; + + public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int nodeId) + { + return GetUmbracoMedia(nodeId); + } + + public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, Guid nodeKey) + { + // TODO optimize with Examine? + var mapAttempt = ApplicationContext.Current.Services.IdkMap.GetIdForKey(nodeKey, UmbracoObjectTypes.Media); + return mapAttempt ? GetById(umbracoContext, preview, mapAttempt.Result) : null; + } + + public virtual IEnumerable GetAtRoot(UmbracoContext umbracoContext, bool preview) + { + var searchProvider = GetSearchProviderSafe(); + + if (searchProvider != null) + { + try + { + // first check in Examine for the cache values + // +(+parentID:-1) +__IndexType:media + + var criteria = searchProvider.CreateSearchCriteria("media"); + var filter = criteria.ParentId(-1).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + + var result = searchProvider.Search(filter.Compile()); + if (result != null) + return result.Select(x => CreateFromCacheValues(ConvertFromSearchResult(x))); + } + catch (Exception ex) + { + if (ex is FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco + //See this thread: http://examine.cdodeplex.com/discussions/264341 + //Catch the exception here for the time being, and just fallback to GetMedia + //TODO: Need to fix examine in LB scenarios! + LogHelper.Error("Could not load data from Examine index for media", ex); + } + else if (ex is AlreadyClosedException) + { + //If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot + //be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db. + LogHelper.Error("Could not load data from Examine index for media, the app domain is most likely in a shutdown state", ex); + } + else throw; + } } //something went wrong, fetch from the db - - var rootMedia = _applicationContext.Services.MediaService.GetRootMedia(); - return rootMedia.Select(m => CreateFromCacheValues(ConvertFromIMedia(m))); + + var rootMedia = _applicationContext.Services.MediaService.GetRootMedia(); + return rootMedia.Select(m => CreateFromCacheValues(ConvertFromIMedia(m))); } - - public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) - { - throw new NotImplementedException("PublishedMediaCache does not support XPath."); - } - - public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars) - { - throw new NotImplementedException("PublishedMediaCache does not support XPath."); - } - - public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) - { - throw new NotImplementedException("PublishedMediaCache does not support XPath."); - } - - public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars) - { - throw new NotImplementedException("PublishedMediaCache does not support XPath."); - } - - public virtual XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview) - { - throw new NotImplementedException("PublishedMediaCache does not support XPath."); - } - - public bool XPathNavigatorIsNavigable { get { return false; } } - - public virtual bool HasContent(UmbracoContext context, bool preview) { throw new NotImplementedException(); } - - private ExamineManager GetExamineManagerSafe() - { - try - { - return ExamineManager.Instance; - } - catch (TypeInitializationException) - { - return null; - } - } - - private BaseIndexProvider GetIndexProviderSafe() - { - if (_indexProvider != null) - return _indexProvider; - - var eMgr = GetExamineManagerSafe(); - if (eMgr != null) - { - try - { - //by default use the InternalSearcher - var indexer = eMgr.IndexProviderCollection[Constants.Examine.InternalIndexer]; - if (indexer.IndexerData.IncludeNodeTypes.Any() || indexer.IndexerData.ExcludeNodeTypes.Any()) - { - LogHelper.Warn("The InternalIndexer for examine is configured incorrectly, it should not list any include/exclude node types or field names, it should simply be configured as: " + ""); - } - return indexer; - } - catch (Exception ex) - { - LogHelper.Error("Could not retrieve the InternalIndexer", ex); - //something didn't work, continue returning null. - } - } - return null; - } - - private BaseSearchProvider GetSearchProviderSafe() - { - if (_searchProvider != null) - return _searchProvider; - - var eMgr = GetExamineManagerSafe(); - if (eMgr != null) - { - try - { - //by default use the InternalSearcher - return eMgr.SearchProviderCollection[Constants.Examine.InternalSearcher]; - } - catch (FileNotFoundException) - { - //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco - //See this thread: http://examine.cdodeplex.com/discussions/264341 - //Catch the exception here for the time being, and just fallback to GetMedia - //TODO: Need to fix examine in LB scenarios! - } - catch (NullReferenceException) - { - //This will occur when the search provider cannot be initialized. In newer examine versions the initialization is lazy and therefore - // the manager will return the singleton without throwing initialization errors, however if examine isn't configured correctly a null - // reference error will occur because the examine settings are null. - } - catch (AlreadyClosedException) - { - //If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot - //be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db. - } - } - return null; - } - - private IPublishedContent GetUmbracoMedia(int id) - { - // this recreates an IPublishedContent and model each time - // it is called, but at least it should NOT hit the database - // nor Lucene each time, relying on the memory cache instead - - if (id <= 0) return null; // fail fast - - var cacheValues = GetCacheValues(id, GetUmbracoMediaCacheValues); - - return cacheValues == null ? null : CreateFromCacheValues(cacheValues); - } - - private CacheValues GetUmbracoMediaCacheValues(int id) - { - var searchProvider = GetSearchProviderSafe(); - - if (searchProvider != null) - { - try - { - // first check in Examine as this is WAY faster - // - // the filter will create a query like this: - // +(+__NodeId:3113 -__Path:-1,-21,*) +__IndexType:media - // - // note that since the use of the wildcard, it automatically escapes it in Lucene. - - var criteria = searchProvider.CreateSearchCriteria("media"); - var filter = criteria.Id(id).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); - - var result = searchProvider.Search(filter.Compile()).FirstOrDefault(); - if (result != null) return ConvertFromSearchResult(result); - } - catch (Exception ex) - { - if (ex is FileNotFoundException) - { - //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco - //See this thread: http://examine.cdodeplex.com/discussions/264341 - //Catch the exception here for the time being, and just fallback to GetMedia - //TODO: Need to fix examine in LB scenarios! - LogHelper.Error("Could not load data from Examine index for media", ex); - } - else if (ex is AlreadyClosedException) - { - //If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot - //be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db. - LogHelper.Error("Could not load data from Examine index for media, the app domain is most likely in a shutdown state", ex); - } - else throw; - } - } - - // don't log a warning here, as it can flood the log in case of eg a media picker referencing a media - // that has been deleted, hence is not in the Examine index anymore (for a good reason). try to get - // the media from the service, first - - var media = ApplicationContext.Current.Services.MediaService.GetById(id); - if (media == null || media.Trashed) return null; // not found, ok - - // so, the media was not found in Examine's index *yet* it exists, which probably indicates that - // the index is corrupted. Or not up-to-date. Log a warning, but only once, and only if seeing the - // error more that a number of times. - - var miss = Interlocked.CompareExchange(ref _examineIndexMiss, 0, 0); // volatile read - if (miss < ExamineIndexMissMax && Interlocked.Increment(ref _examineIndexMiss) == ExamineIndexMissMax) - LogHelper.Warn("Failed ({0} times) to retrieve medias from Examine index and had to load" - + " them from DB. This may indicate that the Examine index is corrupted.", - () => ExamineIndexMissMax); - - return ConvertFromIMedia(media); - } - - private const int ExamineIndexMissMax = 10; - private int _examineIndexMiss; - - internal CacheValues ConvertFromXPathNodeIterator(XPathNodeIterator media, int id) - { - if (media != null && media.Current != null) - { - return media.Current.Name.InvariantEquals("error") - ? null - : ConvertFromXPathNavigator(media.Current); - } - - LogHelper.Warn( - "Could not retrieve media {0} from Examine index or from legacy library.GetMedia method", - () => id); - - return null; - } - - internal CacheValues ConvertFromSearchResult(SearchResult searchResult) - { - //NOTE: Some fields will not be included if the config section for the internal index has been - //mucked around with. It should index everything and so the index definition should simply be: - // - - - var values = new Dictionary(searchResult.Fields); - //we need to ensure some fields exist, because of the above issue - if (!new[] { "template", "templateId" }.Any(values.ContainsKey)) - values.Add("template", 0.ToString()); - if (!new[] { "sortOrder" }.Any(values.ContainsKey)) - values.Add("sortOrder", 0.ToString()); - if (!new[] { "urlName" }.Any(values.ContainsKey)) - values.Add("urlName", ""); - if (!new[] { "nodeType" }.Any(values.ContainsKey)) - values.Add("nodeType", 0.ToString()); - if (!new[] { "creatorName" }.Any(values.ContainsKey)) - values.Add("creatorName", ""); - if (!new[] { "writerID" }.Any(values.ContainsKey)) - values.Add("writerID", 0.ToString()); - if (!new[] { "creatorID" }.Any(values.ContainsKey)) - values.Add("creatorID", 0.ToString()); - if (!new[] { "createDate" }.Any(values.ContainsKey)) - values.Add("createDate", default(DateTime).ToString("yyyy-MM-dd HH:mm:ss")); - if (!new[] { "level" }.Any(values.ContainsKey)) - { - values.Add("level", values["__Path"].Split(',').Length.ToString()); - } - - // because, migration - if (values.ContainsKey("key") == false) - values["key"] = Guid.Empty.ToString(); - - return new CacheValues - { - Values = values, - FromExamine = true - }; - - //var content = new DictionaryPublishedContent(values, - // d => d.ParentId != -1 //parent should be null if -1 - // ? GetUmbracoMedia(d.ParentId) - // : null, - // //callback to return the children of the current node - // d => GetChildrenMedia(d.Id), - // GetProperty, - // true); - //return content.CreateModel(); - } - - internal CacheValues ConvertFromXPathNavigator(XPathNavigator xpath, bool forceNav = false) - { - if (xpath == null) throw new ArgumentNullException("xpath"); - - var values = new Dictionary { { "nodeName", xpath.GetAttribute("nodeName", "") } }; - if (!UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema) - { - values["nodeTypeAlias"] = xpath.Name; - } - - var result = xpath.SelectChildren(XPathNodeType.Element); - //add the attributes e.g. id, parentId etc - if (result.Current != null && result.Current.HasAttributes) - { - if (result.Current.MoveToFirstAttribute()) - { - //checking for duplicate keys because of the 'nodeTypeAlias' might already be added above. - if (!values.ContainsKey(result.Current.Name)) - { - values[result.Current.Name] = result.Current.Value; - } - while (result.Current.MoveToNextAttribute()) - { - if (!values.ContainsKey(result.Current.Name)) - { - values[result.Current.Name] = result.Current.Value; - } - } - result.Current.MoveToParent(); - } - } - // because, migration - if (values.ContainsKey("key") == false) - values["key"] = Guid.Empty.ToString(); - //add the user props - while (result.MoveNext()) - { - if (result.Current != null && !result.Current.HasAttributes) - { - string value = result.Current.Value; - if (string.IsNullOrEmpty(value)) - { - if (result.Current.HasAttributes || result.Current.SelectChildren(XPathNodeType.Element).Count > 0) - { - value = result.Current.OuterXml; - } - } - values[result.Current.Name] = value; - } - } - - return new CacheValues - { - Values = values, - XPath = forceNav ? xpath : null // outside of tests we do NOT want to cache the navigator! - }; - - //var content = new DictionaryPublishedContent(values, - // d => d.ParentId != -1 //parent should be null if -1 - // ? GetUmbracoMedia(d.ParentId) - // : null, - // //callback to return the children of the current node based on the xml structure already found - // d => GetChildrenMedia(d.Id, xpath), - // GetProperty, - // false); - //return content.CreateModel(); - } - - internal CacheValues ConvertFromIMedia(IMedia media) - { - var values = new Dictionary(); - - var creator = _applicationContext.Services.UserService.GetProfileById(media.CreatorId); - var creatorName = creator == null ? "" : creator.Name; - - values["id"] = media.Id.ToString(); - values["key"] = media.Key.ToString(); - values["parentID"] = media.ParentId.ToString(); - values["level"] = media.Level.ToString(); - values["creatorID"] = media.CreatorId.ToString(); - values["creatorName"] = creatorName; - values["writerID"] = media.CreatorId.ToString(); - values["writerName"] = creatorName; - values["template"] = "0"; - values["urlName"] = ""; - values["sortOrder"] = media.SortOrder.ToString(); - values["createDate"] = media.CreateDate.ToString("yyyy-MM-dd HH:mm:ss"); - values["updateDate"] = media.UpdateDate.ToString("yyyy-MM-dd HH:mm:ss"); - values["nodeName"] = media.Name; - values["path"] = media.Path; - values["nodeType"] = media.ContentType.Id.ToString(); - values["nodeTypeAlias"] = media.ContentType.Alias; - - // add the user props - foreach (var prop in media.Properties) - values[prop.Alias] = prop.Value == null ? null : prop.Value.ToString(); - - return new CacheValues - { - Values = values - }; - } - - /// - /// We will need to first check if the document was loaded by Examine, if so we'll need to check if this property exists - /// in the results, if it does not, then we'll have to revert to looking up in the db. - /// - /// - /// - /// - private IPublishedProperty GetProperty(DictionaryPublishedContent dd, string alias) - { - //lets check if the alias does not exist on the document. - //NOTE: Examine will not index empty values and we do not output empty XML Elements to the cache - either of these situations - // would mean that the property is missing from the collection whether we are getting the value from Examine or from the library media cache. - if (dd.Properties.All(x => x.PropertyTypeAlias.InvariantEquals(alias) == false)) - { - return null; - } - - if (dd.LoadedFromExamine) - { - //We are going to check for a special field however, that is because in some cases we store a 'Raw' - //value in the index such as for xml/html. - var rawValue = dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(UmbracoContentIndexer.RawFieldPrefix + alias)); - return rawValue - ?? dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); - } - - //if its not loaded from examine, then just return the property - return dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); - } - - /// - /// A Helper methods to return the children for media whther it is based on examine or xml - /// - /// - /// - /// - private IEnumerable GetChildrenMedia(int parentId, XPathNavigator xpath = null) - { - - //if there is no navigator, try examine first, then re-look it up - if (xpath == null) - { - var searchProvider = GetSearchProviderSafe(); - - if (searchProvider != null) - { - try - { - //first check in Examine as this is WAY faster - var criteria = searchProvider.CreateSearchCriteria("media"); - - var filter = criteria.ParentId(parentId).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); - //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. - //+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media - - ISearchResults results; - - //we want to check if the indexer for this searcher has "sortOrder" flagged as sortable. - //if so, we'll use Lucene to do the sorting, if not we'll have to manually sort it (slower). - var indexer = GetIndexProviderSafe(); - var useLuceneSort = indexer != null && indexer.IndexerData.StandardFields.Any(x => x.Name.InvariantEquals("sortOrder") && x.EnableSorting); - if (useLuceneSort) - { - //we have a sortOrder field declared to be sorted, so we'll use Examine - results = searchProvider.Search( - filter.And().OrderBy(new SortableField("sortOrder", SortType.Int)).Compile()); - } - else - { - results = searchProvider.Search(filter.Compile()); - } - - if (results.Any()) - { - // var medias = results.Select(ConvertFromSearchResult); - var medias = results.Select(x => - { - int nid; - if (int.TryParse(x["__NodeId"], out nid) == false && int.TryParse(x["NodeId"], out nid) == false) - throw new Exception("Failed to extract NodeId from search result."); - var cacheValues = GetCacheValues(nid, id => ConvertFromSearchResult(x)); - return CreateFromCacheValues(cacheValues); - }); - - return useLuceneSort ? medias : medias.OrderBy(x => x.SortOrder); - } - else - { - //if there's no result then return null. Previously we defaulted back to library.GetMedia below - //but this will always get called for when we are getting descendents since many items won't have - //children and then we are hitting the database again! - //So instead we're going to rely on Examine to have the correct results like it should. - return Enumerable.Empty(); - } - } - catch (FileNotFoundException) - { - //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco - //See this thread: http://examine.cdodeplex.com/discussions/264341 - //Catch the exception here for the time being, and just fallback to GetMedia - } - } - - //falling back to get media - - var media = library.GetMedia(parentId, true); - if (media != null && media.Current != null) - { - xpath = media.Current; - } - else - { - return Enumerable.Empty(); - } - } - - var mediaList = new List(); - - // this is so bad, really - var item = xpath.Select("//*[@id='" + parentId + "']"); - if (item.Current == null) - return Enumerable.Empty(); - var items = item.Current.SelectChildren(XPathNodeType.Element); - - // and this does not work, because... meh - //var q = "//* [@id='" + parentId + "']/* [@id]"; - //var items = xpath.Select(q); - - foreach (XPathNavigator itemm in items) - { - int id; - if (int.TryParse(itemm.GetAttribute("id", ""), out id) == false) - continue; // wtf? - var captured = itemm; - var cacheValues = GetCacheValues(id, idd => ConvertFromXPathNavigator(captured)); - mediaList.Add(CreateFromCacheValues(cacheValues)); - } - - ////The xpath might be the whole xpath including the current ones ancestors so we need to select the current node - //var item = xpath.Select("//*[@id='" + parentId + "']"); - //if (item.Current == null) - //{ - // return Enumerable.Empty(); - //} - //var children = item.Current.SelectChildren(XPathNodeType.Element); - - //foreach(XPathNavigator x in children) - //{ - // //NOTE: I'm not sure why this is here, it is from legacy code of ExamineBackedMedia, but - // // will leave it here as it must have done something! - // if (x.Name != "contents") - // { - // //make sure it's actually a node, not a property - // if (!string.IsNullOrEmpty(x.GetAttribute("path", "")) && - // !string.IsNullOrEmpty(x.GetAttribute("id", ""))) - // { - // mediaList.Add(ConvertFromXPathNavigator(x)); - // } - // } - //} - - return mediaList; - } - - /// - /// An IPublishedContent that is represented all by a dictionary. - /// - /// - /// This is a helper class and definitely not intended for public use, it expects that all of the values required - /// to create an IPublishedContent exist in the dictionary by specific aliases. - /// - internal class DictionaryPublishedContent : PublishedContentWithKeyBase - { - // note: I'm not sure this class fully complies with IPublishedContent rules especially - // I'm not sure that _properties contains all properties including those without a value, - // neither that GetProperty will return a property without a value vs. null... @zpqrtbnk - - // List of properties that will appear in the XML and do not match - // anything in the ContentType, so they must be ignored. - private static readonly string[] IgnoredKeys = { "version", "isDoc" }; - - public DictionaryPublishedContent( - IDictionary valueDictionary, - Func getParent, - Func> getChildren, - Func getProperty, - XPathNavigator nav, - bool fromExamine) - { - if (valueDictionary == null) throw new ArgumentNullException("valueDictionary"); - if (getParent == null) throw new ArgumentNullException("getParent"); - if (getProperty == null) throw new ArgumentNullException("getProperty"); - - _getParent = new Lazy(() => getParent(ParentId)); - _getChildren = new Lazy>(() => getChildren(Id, nav)); - _getProperty = getProperty; - - LoadedFromExamine = fromExamine; - - ValidateAndSetProperty(valueDictionary, val => _id = int.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int! - ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key"); - // wtf are we dealing with templates for medias?! - ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId"); - ValidateAndSetProperty(valueDictionary, val => _sortOrder = int.Parse(val), "sortOrder"); - ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName", "__nodeName"); - ValidateAndSetProperty(valueDictionary, val => _urlName = val, "urlName"); - ValidateAndSetProperty(valueDictionary, val => _documentTypeAlias = val, "nodeTypeAlias", UmbracoContentIndexer.NodeTypeAliasFieldName); - ValidateAndSetProperty(valueDictionary, val => _documentTypeId = int.Parse(val), "nodeType"); - ValidateAndSetProperty(valueDictionary, val => _writerName = val, "writerName"); - ValidateAndSetProperty(valueDictionary, val => _creatorName = val, "creatorName", "writerName"); //this is a bit of a hack fix for: U4-1132 - ValidateAndSetProperty(valueDictionary, val => _writerId = int.Parse(val), "writerID"); - ValidateAndSetProperty(valueDictionary, val => _creatorId = int.Parse(val), "creatorID", "writerID"); //this is a bit of a hack fix for: U4-1132 - ValidateAndSetProperty(valueDictionary, val => _path = val, "path", "__Path"); - ValidateAndSetProperty(valueDictionary, val => _createDate = ParseDateTimeValue(val), "createDate"); - ValidateAndSetProperty(valueDictionary, val => _updateDate = ParseDateTimeValue(val), "updateDate"); - ValidateAndSetProperty(valueDictionary, val => _level = int.Parse(val), "level"); - ValidateAndSetProperty(valueDictionary, val => - { - int pId; - ParentId = -1; - if (int.TryParse(val, out pId)) - { - ParentId = pId; - } - }, "parentID"); - - _contentType = PublishedContentType.Get(PublishedItemType.Media, _documentTypeAlias); - _properties = new Collection(); - - //handle content type properties - //make sure we create them even if there's no value - foreach (var propertyType in _contentType.PropertyTypes) - { - var alias = propertyType.PropertyTypeAlias; - _keysAdded.Add(alias); - string value; - const bool isPreviewing = false; // false :: never preview a media - var property = valueDictionary.TryGetValue(alias, out value) == false || value == null - ? new XmlPublishedProperty(propertyType, isPreviewing) - : new XmlPublishedProperty(propertyType, isPreviewing, value); - _properties.Add(property); - } - - //loop through remaining values that haven't been applied - foreach (var i in valueDictionary.Where(x => - _keysAdded.Contains(x.Key) == false // not already processed - && IgnoredKeys.Contains(x.Key) == false)) // not ignorable - { - if (i.Key.InvariantStartsWith("__")) - { - // no type for that one, dunno how to convert - IPublishedProperty property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty); - _properties.Add(property); - } - else - { - // this is a property that does not correspond to anything, ignore and log - LogHelper.Warn("Dropping property \"" + i.Key + "\" because it does not belong to the content type."); - } - } - } - - private DateTime ParseDateTimeValue(string val) - { - if (LoadedFromExamine) - { - try - { - //we might need to parse the date time using Lucene converters - return DateTools.StringToDate(val); - } - catch (FormatException) - { - //swallow exception, its not formatted correctly so revert to just trying to parse - } - } - - return DateTime.Parse(val); - } - - /// - /// Flag to get/set if this was laoded from examine cache - /// - internal bool LoadedFromExamine { get; private set; } - - //private readonly Func _getParent; - private readonly Lazy _getParent; - //private readonly Func> _getChildren; - private readonly Lazy> _getChildren; - private readonly Func _getProperty; - - /// - /// Returns 'Media' as the item type - /// - public override PublishedItemType ItemType - { - get { return PublishedItemType.Media; } - } - - public override IPublishedContent Parent - { - get { return _getParent.Value; } - } - - public int ParentId { get; private set; } - public override int Id - { - get { return _id; } - } - - public override Guid Key { get { return _key; } } - - public override int TemplateId - { - get - { - //TODO: should probably throw a not supported exception since media doesn't actually support this. - return _templateId; - } - } - - public override int SortOrder - { - get { return _sortOrder; } - } - - public override string Name - { - get { return _name; } - } - - public override string UrlName - { - get { return _urlName; } - } - - public override string DocumentTypeAlias - { - get { return _documentTypeAlias; } - } - - public override int DocumentTypeId - { - get { return _documentTypeId; } - } - - public override string WriterName - { - get { return _writerName; } - } - - public override string CreatorName - { - get { return _creatorName; } - } - - public override int WriterId - { - get { return _writerId; } - } - - public override int CreatorId - { - get { return _creatorId; } - } - - public override string Path - { - get { return _path; } - } - - public override DateTime CreateDate - { - get { return _createDate; } - } - - public override DateTime UpdateDate - { - get { return _updateDate; } - } - - public override Guid Version - { - get { return _version; } - } - - public override int Level - { - get { return _level; } - } - - public override bool IsDraft - { - get { return false; } - } - - public override ICollection Properties - { - get { return _properties; } - } - - public override IEnumerable Children - { - get { return _getChildren.Value; } - } - - public override IPublishedProperty GetProperty(string alias) - { - return _getProperty(this, alias); - } - - public override PublishedContentType ContentType - { - get { return _contentType; } - } - - // override to implement cache - // cache at context level, ie once for the whole request - // but cache is not shared by requests because we wouldn't know how to clear it - public override IPublishedProperty GetProperty(string alias, bool recurse) - { - if (recurse == false) return GetProperty(alias); - - IPublishedProperty property; - string key = null; - var cache = UmbracoContextCache.Current; - - if (cache != null) - { - key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant()); - object o; - if (cache.TryGetValue(key, out o)) - { - property = o as IPublishedProperty; - if (property == null) - throw new InvalidOperationException("Corrupted cache."); - return property; - } - } - - // else get it for real, no cache - property = base.GetProperty(alias, true); - - if (cache != null) - cache[key] = property; - - return property; - } - - private readonly List _keysAdded = new List(); - private int _id; - private Guid _key; - private int _templateId; - private int _sortOrder; - private string _name; - private string _urlName; - private string _documentTypeAlias; - private int _documentTypeId; - private string _writerName; - private string _creatorName; - private int _writerId; - private int _creatorId; - private string _path; - private DateTime _createDate; - private DateTime _updateDate; - private Guid _version; - private int _level; - private readonly ICollection _properties; - private readonly PublishedContentType _contentType; - - private void ValidateAndSetProperty(IDictionary valueDictionary, Action setProperty, params string[] potentialKeys) - { - var key = potentialKeys.FirstOrDefault(x => valueDictionary.ContainsKey(x) && valueDictionary[x] != null); - if (key == null) - { - throw new FormatException("The valueDictionary is not formatted correctly and is missing any of the '" + string.Join(",", potentialKeys) + "' elements"); - } - - setProperty(valueDictionary[key]); - _keysAdded.Add(key); - } - } - - // REFACTORING - - // caching the basic atomic values - and the parent id - // but NOT caching actual parent nor children and NOT even - // the list of children ids - BUT caching the path - - internal class CacheValues - { - public IDictionary Values { get; set; } - public XPathNavigator XPath { get; set; } - public bool FromExamine { get; set; } - } - - public const string PublishedMediaCacheKey = "MediaCacheMeh."; - private const int PublishedMediaCacheTimespanSeconds = 4 * 60; // 4 mins - private static TimeSpan _publishedMediaCacheTimespan; - private static bool _publishedMediaCacheEnabled; - - private static void InitializeCacheConfig() - { - var value = ConfigurationManager.AppSettings["Umbraco.PublishedMediaCache.Seconds"]; - int seconds; - if (int.TryParse(value, out seconds) == false) - seconds = PublishedMediaCacheTimespanSeconds; - if (seconds > 0) - { - _publishedMediaCacheEnabled = true; - _publishedMediaCacheTimespan = TimeSpan.FromSeconds(seconds); - } - else - { - _publishedMediaCacheEnabled = false; - } - } - - internal IPublishedContent CreateFromCacheValues(CacheValues cacheValues) - { - var content = new DictionaryPublishedContent( - cacheValues.Values, - parentId => parentId < 0 ? null : GetUmbracoMedia(parentId), - GetChildrenMedia, - GetProperty, - cacheValues.XPath, // though, outside of tests, that should be null - cacheValues.FromExamine - ); - return content.CreateModel(); - } - - private static CacheValues GetCacheValues(int id, Func func) - { - if (_publishedMediaCacheEnabled == false) - return func(id); - - var cache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - var key = PublishedMediaCacheKey + id; - return (CacheValues)cache.GetCacheItem(key, () => func(id), _publishedMediaCacheTimespan); - } - - internal static void ClearCache(int id) - { - var cache = ApplicationContext.Current.ApplicationCache.RuntimeCache; - var sid = id.ToString(); - var key = PublishedMediaCacheKey + sid; - - // we do clear a lot of things... but the cache refresher is somewhat - // convoluted and it's hard to tell what to clear exactly ;-( - - // clear the parent - NOT (why?) - //var exist = (CacheValues) cache.GetCacheItem(key); - //if (exist != null) - // cache.ClearCacheItem(PublishedMediaCacheKey + GetValuesValue(exist.Values, "parentID")); - - // clear the item - cache.ClearCacheItem(key); - - // clear all children - in case we moved and their path has changed - var fid = "/" + sid + "/"; - cache.ClearCacheObjectTypes((k, v) => - GetValuesValue(v.Values, "path", "__Path").Contains(fid)); - } - - private static string GetValuesValue(IDictionary d, params string[] keys) - { - string value = null; - var ignored = keys.Any(x => d.TryGetValue(x, out value)); - return value ?? ""; - } - } -} + + public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) + { + throw new NotImplementedException("PublishedMediaCache does not support XPath."); + } + + public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars) + { + throw new NotImplementedException("PublishedMediaCache does not support XPath."); + } + + public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) + { + throw new NotImplementedException("PublishedMediaCache does not support XPath."); + } + + public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars) + { + throw new NotImplementedException("PublishedMediaCache does not support XPath."); + } + + public virtual XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview) + { + throw new NotImplementedException("PublishedMediaCache does not support XPath."); + } + + public bool XPathNavigatorIsNavigable { get { return false; } } + + public virtual bool HasContent(UmbracoContext context, bool preview) { throw new NotImplementedException(); } + + private ExamineManager GetExamineManagerSafe() + { + try + { + return ExamineManager.Instance; + } + catch (TypeInitializationException) + { + return null; + } + } + + private BaseIndexProvider GetIndexProviderSafe() + { + if (_indexProvider != null) + return _indexProvider; + + var eMgr = GetExamineManagerSafe(); + if (eMgr != null) + { + try + { + //by default use the InternalSearcher + var indexer = eMgr.IndexProviderCollection[Constants.Examine.InternalIndexer]; + if (indexer.IndexerData.IncludeNodeTypes.Any() || indexer.IndexerData.ExcludeNodeTypes.Any()) + { + LogHelper.Warn("The InternalIndexer for examine is configured incorrectly, it should not list any include/exclude node types or field names, it should simply be configured as: " + ""); + } + return indexer; + } + catch (Exception ex) + { + LogHelper.Error("Could not retrieve the InternalIndexer", ex); + //something didn't work, continue returning null. + } + } + return null; + } + + private BaseSearchProvider GetSearchProviderSafe() + { + if (_searchProvider != null) + return _searchProvider; + + var eMgr = GetExamineManagerSafe(); + if (eMgr != null) + { + try + { + //by default use the InternalSearcher + return eMgr.SearchProviderCollection[Constants.Examine.InternalSearcher]; + } + catch (FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco + //See this thread: http://examine.cdodeplex.com/discussions/264341 + //Catch the exception here for the time being, and just fallback to GetMedia + //TODO: Need to fix examine in LB scenarios! + } + catch (NullReferenceException) + { + //This will occur when the search provider cannot be initialized. In newer examine versions the initialization is lazy and therefore + // the manager will return the singleton without throwing initialization errors, however if examine isn't configured correctly a null + // reference error will occur because the examine settings are null. + } + catch (AlreadyClosedException) + { + //If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot + //be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db. + } + } + return null; + } + + private IPublishedContent GetUmbracoMedia(int id) + { + // this recreates an IPublishedContent and model each time + // it is called, but at least it should NOT hit the database + // nor Lucene each time, relying on the memory cache instead + + if (id <= 0) return null; // fail fast + + var cacheValues = GetCacheValues(id, GetUmbracoMediaCacheValues); + + return cacheValues == null ? null : CreateFromCacheValues(cacheValues); + } + + private CacheValues GetUmbracoMediaCacheValues(int id) + { + var searchProvider = GetSearchProviderSafe(); + + if (searchProvider != null) + { + try + { + // first check in Examine as this is WAY faster + // + // the filter will create a query like this: + // +(+__NodeId:3113 -__Path:-1,-21,*) +__IndexType:media + // + // note that since the use of the wildcard, it automatically escapes it in Lucene. + + var criteria = searchProvider.CreateSearchCriteria("media"); + var filter = criteria.Id(id).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + + var result = searchProvider.Search(filter.Compile()).FirstOrDefault(); + if (result != null) return ConvertFromSearchResult(result); + } + catch (Exception ex) + { + if (ex is FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco + //See this thread: http://examine.cdodeplex.com/discussions/264341 + //Catch the exception here for the time being, and just fallback to GetMedia + //TODO: Need to fix examine in LB scenarios! + LogHelper.Error("Could not load data from Examine index for media", ex); + } + else if (ex is AlreadyClosedException) + { + //If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot + //be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db. + LogHelper.Error("Could not load data from Examine index for media, the app domain is most likely in a shutdown state", ex); + } + else throw; + } + } + + // don't log a warning here, as it can flood the log in case of eg a media picker referencing a media + // that has been deleted, hence is not in the Examine index anymore (for a good reason). try to get + // the media from the service, first + + var media = ApplicationContext.Current.Services.MediaService.GetById(id); + if (media == null || media.Trashed) return null; // not found, ok + + // so, the media was not found in Examine's index *yet* it exists, which probably indicates that + // the index is corrupted. Or not up-to-date. Log a warning, but only once, and only if seeing the + // error more that a number of times. + + var miss = Interlocked.CompareExchange(ref _examineIndexMiss, 0, 0); // volatile read + if (miss < ExamineIndexMissMax && Interlocked.Increment(ref _examineIndexMiss) == ExamineIndexMissMax) + LogHelper.Warn("Failed ({0} times) to retrieve medias from Examine index and had to load" + + " them from DB. This may indicate that the Examine index is corrupted.", + () => ExamineIndexMissMax); + + return ConvertFromIMedia(media); + } + + private const int ExamineIndexMissMax = 10; + private int _examineIndexMiss; + + internal CacheValues ConvertFromXPathNodeIterator(XPathNodeIterator media, int id) + { + if (media != null && media.Current != null) + { + return media.Current.Name.InvariantEquals("error") + ? null + : ConvertFromXPathNavigator(media.Current); + } + + LogHelper.Warn( + "Could not retrieve media {0} from Examine index or from legacy library.GetMedia method", + () => id); + + return null; + } + + internal CacheValues ConvertFromSearchResult(SearchResult searchResult) + { + //NOTE: Some fields will not be included if the config section for the internal index has been + //mucked around with. It should index everything and so the index definition should simply be: + // + + + var values = new Dictionary(searchResult.Fields); + //we need to ensure some fields exist, because of the above issue + if (!new[] { "template", "templateId" }.Any(values.ContainsKey)) + values.Add("template", 0.ToString()); + if (!new[] { "sortOrder" }.Any(values.ContainsKey)) + values.Add("sortOrder", 0.ToString()); + if (!new[] { "urlName" }.Any(values.ContainsKey)) + values.Add("urlName", ""); + if (!new[] { "nodeType" }.Any(values.ContainsKey)) + values.Add("nodeType", 0.ToString()); + if (!new[] { "creatorName" }.Any(values.ContainsKey)) + values.Add("creatorName", ""); + if (!new[] { "writerID" }.Any(values.ContainsKey)) + values.Add("writerID", 0.ToString()); + if (!new[] { "creatorID" }.Any(values.ContainsKey)) + values.Add("creatorID", 0.ToString()); + if (!new[] { "createDate" }.Any(values.ContainsKey)) + values.Add("createDate", default(DateTime).ToString("yyyy-MM-dd HH:mm:ss")); + if (!new[] { "level" }.Any(values.ContainsKey)) + { + values.Add("level", values["__Path"].Split(',').Length.ToString()); + } + + // because, migration + if (values.ContainsKey("key") == false) + values["key"] = Guid.Empty.ToString(); + + return new CacheValues + { + Values = values, + FromExamine = true + }; + + //var content = new DictionaryPublishedContent(values, + // d => d.ParentId != -1 //parent should be null if -1 + // ? GetUmbracoMedia(d.ParentId) + // : null, + // //callback to return the children of the current node + // d => GetChildrenMedia(d.Id), + // GetProperty, + // true); + //return content.CreateModel(); + } + + internal CacheValues ConvertFromXPathNavigator(XPathNavigator xpath, bool forceNav = false) + { + if (xpath == null) throw new ArgumentNullException("xpath"); + + var values = new Dictionary { { "nodeName", xpath.GetAttribute("nodeName", "") } }; + if (!UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema) + { + values["nodeTypeAlias"] = xpath.Name; + } + + var result = xpath.SelectChildren(XPathNodeType.Element); + //add the attributes e.g. id, parentId etc + if (result.Current != null && result.Current.HasAttributes) + { + if (result.Current.MoveToFirstAttribute()) + { + //checking for duplicate keys because of the 'nodeTypeAlias' might already be added above. + if (!values.ContainsKey(result.Current.Name)) + { + values[result.Current.Name] = result.Current.Value; + } + while (result.Current.MoveToNextAttribute()) + { + if (!values.ContainsKey(result.Current.Name)) + { + values[result.Current.Name] = result.Current.Value; + } + } + result.Current.MoveToParent(); + } + } + // because, migration + if (values.ContainsKey("key") == false) + values["key"] = Guid.Empty.ToString(); + //add the user props + while (result.MoveNext()) + { + if (result.Current != null && !result.Current.HasAttributes) + { + string value = result.Current.Value; + if (string.IsNullOrEmpty(value)) + { + if (result.Current.HasAttributes || result.Current.SelectChildren(XPathNodeType.Element).Count > 0) + { + value = result.Current.OuterXml; + } + } + values[result.Current.Name] = value; + } + } + + return new CacheValues + { + Values = values, + XPath = forceNav ? xpath : null // outside of tests we do NOT want to cache the navigator! + }; + + //var content = new DictionaryPublishedContent(values, + // d => d.ParentId != -1 //parent should be null if -1 + // ? GetUmbracoMedia(d.ParentId) + // : null, + // //callback to return the children of the current node based on the xml structure already found + // d => GetChildrenMedia(d.Id, xpath), + // GetProperty, + // false); + //return content.CreateModel(); + } + + internal CacheValues ConvertFromIMedia(IMedia media) + { + var values = new Dictionary(); + + var creator = _applicationContext.Services.UserService.GetProfileById(media.CreatorId); + var creatorName = creator == null ? "" : creator.Name; + + values["id"] = media.Id.ToString(); + values["key"] = media.Key.ToString(); + values["parentID"] = media.ParentId.ToString(); + values["level"] = media.Level.ToString(); + values["creatorID"] = media.CreatorId.ToString(); + values["creatorName"] = creatorName; + values["writerID"] = media.CreatorId.ToString(); + values["writerName"] = creatorName; + values["template"] = "0"; + values["urlName"] = ""; + values["sortOrder"] = media.SortOrder.ToString(); + values["createDate"] = media.CreateDate.ToString("yyyy-MM-dd HH:mm:ss"); + values["updateDate"] = media.UpdateDate.ToString("yyyy-MM-dd HH:mm:ss"); + values["nodeName"] = media.Name; + values["path"] = media.Path; + values["nodeType"] = media.ContentType.Id.ToString(); + values["nodeTypeAlias"] = media.ContentType.Alias; + + // add the user props + foreach (var prop in media.Properties) + values[prop.Alias] = prop.Value == null ? null : prop.Value.ToString(); + + return new CacheValues + { + Values = values + }; + } + + /// + /// We will need to first check if the document was loaded by Examine, if so we'll need to check if this property exists + /// in the results, if it does not, then we'll have to revert to looking up in the db. + /// + /// + /// + /// + private IPublishedProperty GetProperty(DictionaryPublishedContent dd, string alias) + { + //lets check if the alias does not exist on the document. + //NOTE: Examine will not index empty values and we do not output empty XML Elements to the cache - either of these situations + // would mean that the property is missing from the collection whether we are getting the value from Examine or from the library media cache. + if (dd.Properties.All(x => x.PropertyTypeAlias.InvariantEquals(alias) == false)) + { + return null; + } + + if (dd.LoadedFromExamine) + { + //We are going to check for a special field however, that is because in some cases we store a 'Raw' + //value in the index such as for xml/html. + var rawValue = dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(UmbracoContentIndexer.RawFieldPrefix + alias)); + return rawValue + ?? dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); + } + + //if its not loaded from examine, then just return the property + return dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias)); + } + + /// + /// A Helper methods to return the children for media whther it is based on examine or xml + /// + /// + /// + /// + private IEnumerable GetChildrenMedia(int parentId, XPathNavigator xpath = null) + { + + //if there is no navigator, try examine first, then re-look it up + if (xpath == null) + { + var searchProvider = GetSearchProviderSafe(); + + if (searchProvider != null) + { + try + { + //first check in Examine as this is WAY faster + var criteria = searchProvider.CreateSearchCriteria("media"); + + var filter = criteria.ParentId(parentId).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. + //+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media + + ISearchResults results; + + //we want to check if the indexer for this searcher has "sortOrder" flagged as sortable. + //if so, we'll use Lucene to do the sorting, if not we'll have to manually sort it (slower). + var indexer = GetIndexProviderSafe(); + var useLuceneSort = indexer != null && indexer.IndexerData.StandardFields.Any(x => x.Name.InvariantEquals("sortOrder") && x.EnableSorting); + if (useLuceneSort) + { + //we have a sortOrder field declared to be sorted, so we'll use Examine + results = searchProvider.Search( + filter.And().OrderBy(new SortableField("sortOrder", SortType.Int)).Compile()); + } + else + { + results = searchProvider.Search(filter.Compile()); + } + + if (results.Any()) + { + // var medias = results.Select(ConvertFromSearchResult); + var medias = results.Select(x => + { + int nid; + if (int.TryParse(x["__NodeId"], out nid) == false && int.TryParse(x["NodeId"], out nid) == false) + throw new Exception("Failed to extract NodeId from search result."); + var cacheValues = GetCacheValues(nid, id => ConvertFromSearchResult(x)); + return CreateFromCacheValues(cacheValues); + }); + + return useLuceneSort ? medias : medias.OrderBy(x => x.SortOrder); + } + else + { + //if there's no result then return null. Previously we defaulted back to library.GetMedia below + //but this will always get called for when we are getting descendents since many items won't have + //children and then we are hitting the database again! + //So instead we're going to rely on Examine to have the correct results like it should. + return Enumerable.Empty(); + } + } + catch (FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco + //See this thread: http://examine.cdodeplex.com/discussions/264341 + //Catch the exception here for the time being, and just fallback to GetMedia + } + } + + //falling back to get media + + var media = library.GetMedia(parentId, true); + if (media != null && media.Current != null) + { + xpath = media.Current; + } + else + { + return Enumerable.Empty(); + } + } + + var mediaList = new List(); + + // this is so bad, really + var item = xpath.Select("//*[@id='" + parentId + "']"); + if (item.Current == null) + return Enumerable.Empty(); + var items = item.Current.SelectChildren(XPathNodeType.Element); + + // and this does not work, because... meh + //var q = "//* [@id='" + parentId + "']/* [@id]"; + //var items = xpath.Select(q); + + foreach (XPathNavigator itemm in items) + { + int id; + if (int.TryParse(itemm.GetAttribute("id", ""), out id) == false) + continue; // wtf? + var captured = itemm; + var cacheValues = GetCacheValues(id, idd => ConvertFromXPathNavigator(captured)); + mediaList.Add(CreateFromCacheValues(cacheValues)); + } + + ////The xpath might be the whole xpath including the current ones ancestors so we need to select the current node + //var item = xpath.Select("//*[@id='" + parentId + "']"); + //if (item.Current == null) + //{ + // return Enumerable.Empty(); + //} + //var children = item.Current.SelectChildren(XPathNodeType.Element); + + //foreach(XPathNavigator x in children) + //{ + // //NOTE: I'm not sure why this is here, it is from legacy code of ExamineBackedMedia, but + // // will leave it here as it must have done something! + // if (x.Name != "contents") + // { + // //make sure it's actually a node, not a property + // if (!string.IsNullOrEmpty(x.GetAttribute("path", "")) && + // !string.IsNullOrEmpty(x.GetAttribute("id", ""))) + // { + // mediaList.Add(ConvertFromXPathNavigator(x)); + // } + // } + //} + + return mediaList; + } + + /// + /// An IPublishedContent that is represented all by a dictionary. + /// + /// + /// This is a helper class and definitely not intended for public use, it expects that all of the values required + /// to create an IPublishedContent exist in the dictionary by specific aliases. + /// + internal class DictionaryPublishedContent : PublishedContentWithKeyBase + { + // note: I'm not sure this class fully complies with IPublishedContent rules especially + // I'm not sure that _properties contains all properties including those without a value, + // neither that GetProperty will return a property without a value vs. null... @zpqrtbnk + + // List of properties that will appear in the XML and do not match + // anything in the ContentType, so they must be ignored. + private static readonly string[] IgnoredKeys = { "version", "isDoc" }; + + public DictionaryPublishedContent( + IDictionary valueDictionary, + Func getParent, + Func> getChildren, + Func getProperty, + XPathNavigator nav, + bool fromExamine) + { + if (valueDictionary == null) throw new ArgumentNullException("valueDictionary"); + if (getParent == null) throw new ArgumentNullException("getParent"); + if (getProperty == null) throw new ArgumentNullException("getProperty"); + + _getParent = new Lazy(() => getParent(ParentId)); + _getChildren = new Lazy>(() => getChildren(Id, nav)); + _getProperty = getProperty; + + LoadedFromExamine = fromExamine; + + ValidateAndSetProperty(valueDictionary, val => _id = int.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int! + ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key"); + // wtf are we dealing with templates for medias?! + ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId"); + ValidateAndSetProperty(valueDictionary, val => _sortOrder = int.Parse(val), "sortOrder"); + ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName", "__nodeName"); + ValidateAndSetProperty(valueDictionary, val => _urlName = val, "urlName"); + ValidateAndSetProperty(valueDictionary, val => _documentTypeAlias = val, "nodeTypeAlias", UmbracoContentIndexer.NodeTypeAliasFieldName); + ValidateAndSetProperty(valueDictionary, val => _documentTypeId = int.Parse(val), "nodeType"); + ValidateAndSetProperty(valueDictionary, val => _writerName = val, "writerName"); + ValidateAndSetProperty(valueDictionary, val => _creatorName = val, "creatorName", "writerName"); //this is a bit of a hack fix for: U4-1132 + ValidateAndSetProperty(valueDictionary, val => _writerId = int.Parse(val), "writerID"); + ValidateAndSetProperty(valueDictionary, val => _creatorId = int.Parse(val), "creatorID", "writerID"); //this is a bit of a hack fix for: U4-1132 + ValidateAndSetProperty(valueDictionary, val => _path = val, "path", "__Path"); + ValidateAndSetProperty(valueDictionary, val => _createDate = ParseDateTimeValue(val), "createDate"); + ValidateAndSetProperty(valueDictionary, val => _updateDate = ParseDateTimeValue(val), "updateDate"); + ValidateAndSetProperty(valueDictionary, val => _level = int.Parse(val), "level"); + ValidateAndSetProperty(valueDictionary, val => + { + int pId; + ParentId = -1; + if (int.TryParse(val, out pId)) + { + ParentId = pId; + } + }, "parentID"); + + _contentType = PublishedContentType.Get(PublishedItemType.Media, _documentTypeAlias); + _properties = new Collection(); + + //handle content type properties + //make sure we create them even if there's no value + foreach (var propertyType in _contentType.PropertyTypes) + { + var alias = propertyType.PropertyTypeAlias; + _keysAdded.Add(alias); + string value; + const bool isPreviewing = false; // false :: never preview a media + var property = valueDictionary.TryGetValue(alias, out value) == false || value == null + ? new XmlPublishedProperty(propertyType, isPreviewing) + : new XmlPublishedProperty(propertyType, isPreviewing, value); + _properties.Add(property); + } + + //loop through remaining values that haven't been applied + foreach (var i in valueDictionary.Where(x => + _keysAdded.Contains(x.Key) == false // not already processed + && IgnoredKeys.Contains(x.Key) == false)) // not ignorable + { + if (i.Key.InvariantStartsWith("__")) + { + // no type for that one, dunno how to convert + IPublishedProperty property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty); + _properties.Add(property); + } + else + { + // this is a property that does not correspond to anything, ignore and log + LogHelper.Warn("Dropping property \"" + i.Key + "\" because it does not belong to the content type."); + } + } + } + + private DateTime ParseDateTimeValue(string val) + { + if (LoadedFromExamine) + { + try + { + //we might need to parse the date time using Lucene converters + return DateTools.StringToDate(val); + } + catch (FormatException) + { + //swallow exception, its not formatted correctly so revert to just trying to parse + } + } + + return DateTime.Parse(val); + } + + /// + /// Flag to get/set if this was laoded from examine cache + /// + internal bool LoadedFromExamine { get; private set; } + + //private readonly Func _getParent; + private readonly Lazy _getParent; + //private readonly Func> _getChildren; + private readonly Lazy> _getChildren; + private readonly Func _getProperty; + + /// + /// Returns 'Media' as the item type + /// + public override PublishedItemType ItemType + { + get { return PublishedItemType.Media; } + } + + public override IPublishedContent Parent + { + get { return _getParent.Value; } + } + + public int ParentId { get; private set; } + public override int Id + { + get { return _id; } + } + + public override Guid Key { get { return _key; } } + + public override int TemplateId + { + get + { + //TODO: should probably throw a not supported exception since media doesn't actually support this. + return _templateId; + } + } + + public override int SortOrder + { + get { return _sortOrder; } + } + + public override string Name + { + get { return _name; } + } + + public override string UrlName + { + get { return _urlName; } + } + + public override string DocumentTypeAlias + { + get { return _documentTypeAlias; } + } + + public override int DocumentTypeId + { + get { return _documentTypeId; } + } + + public override string WriterName + { + get { return _writerName; } + } + + public override string CreatorName + { + get { return _creatorName; } + } + + public override int WriterId + { + get { return _writerId; } + } + + public override int CreatorId + { + get { return _creatorId; } + } + + public override string Path + { + get { return _path; } + } + + public override DateTime CreateDate + { + get { return _createDate; } + } + + public override DateTime UpdateDate + { + get { return _updateDate; } + } + + public override Guid Version + { + get { return _version; } + } + + public override int Level + { + get { return _level; } + } + + public override bool IsDraft + { + get { return false; } + } + + public override ICollection Properties + { + get { return _properties; } + } + + public override IEnumerable Children + { + get { return _getChildren.Value; } + } + + public override IPublishedProperty GetProperty(string alias) + { + return _getProperty(this, alias); + } + + public override PublishedContentType ContentType + { + get { return _contentType; } + } + + // override to implement cache + // cache at context level, ie once for the whole request + // but cache is not shared by requests because we wouldn't know how to clear it + public override IPublishedProperty GetProperty(string alias, bool recurse) + { + if (recurse == false) return GetProperty(alias); + + IPublishedProperty property; + string key = null; + var cache = UmbracoContextCache.Current; + + if (cache != null) + { + key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant()); + object o; + if (cache.TryGetValue(key, out o)) + { + property = o as IPublishedProperty; + if (property == null) + throw new InvalidOperationException("Corrupted cache."); + return property; + } + } + + // else get it for real, no cache + property = base.GetProperty(alias, true); + + if (cache != null) + cache[key] = property; + + return property; + } + + private readonly List _keysAdded = new List(); + private int _id; + private Guid _key; + private int _templateId; + private int _sortOrder; + private string _name; + private string _urlName; + private string _documentTypeAlias; + private int _documentTypeId; + private string _writerName; + private string _creatorName; + private int _writerId; + private int _creatorId; + private string _path; + private DateTime _createDate; + private DateTime _updateDate; + private Guid _version; + private int _level; + private readonly ICollection _properties; + private readonly PublishedContentType _contentType; + + private void ValidateAndSetProperty(IDictionary valueDictionary, Action setProperty, params string[] potentialKeys) + { + var key = potentialKeys.FirstOrDefault(x => valueDictionary.ContainsKey(x) && valueDictionary[x] != null); + if (key == null) + { + throw new FormatException("The valueDictionary is not formatted correctly and is missing any of the '" + string.Join(",", potentialKeys) + "' elements"); + } + + setProperty(valueDictionary[key]); + _keysAdded.Add(key); + } + } + + // REFACTORING + + // caching the basic atomic values - and the parent id + // but NOT caching actual parent nor children and NOT even + // the list of children ids - BUT caching the path + + internal class CacheValues + { + public IDictionary Values { get; set; } + public XPathNavigator XPath { get; set; } + public bool FromExamine { get; set; } + } + + public const string PublishedMediaCacheKey = "MediaCacheMeh."; + private const int PublishedMediaCacheTimespanSeconds = 4 * 60; // 4 mins + private static TimeSpan _publishedMediaCacheTimespan; + private static bool _publishedMediaCacheEnabled; + + private static void InitializeCacheConfig() + { + var value = ConfigurationManager.AppSettings["Umbraco.PublishedMediaCache.Seconds"]; + int seconds; + if (int.TryParse(value, out seconds) == false) + seconds = PublishedMediaCacheTimespanSeconds; + if (seconds > 0) + { + _publishedMediaCacheEnabled = true; + _publishedMediaCacheTimespan = TimeSpan.FromSeconds(seconds); + } + else + { + _publishedMediaCacheEnabled = false; + } + } + + internal IPublishedContent CreateFromCacheValues(CacheValues cacheValues) + { + var content = new DictionaryPublishedContent( + cacheValues.Values, + parentId => parentId < 0 ? null : GetUmbracoMedia(parentId), + GetChildrenMedia, + GetProperty, + cacheValues.XPath, // though, outside of tests, that should be null + cacheValues.FromExamine + ); + return content.CreateModel(); + } + + private static CacheValues GetCacheValues(int id, Func func) + { + if (_publishedMediaCacheEnabled == false) + return func(id); + + var cache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var key = PublishedMediaCacheKey + id; + return (CacheValues)cache.GetCacheItem(key, () => func(id), _publishedMediaCacheTimespan); + } + + internal static void ClearCache(int id) + { + var cache = ApplicationContext.Current.ApplicationCache.RuntimeCache; + var sid = id.ToString(); + var key = PublishedMediaCacheKey + sid; + + // we do clear a lot of things... but the cache refresher is somewhat + // convoluted and it's hard to tell what to clear exactly ;-( + + // clear the parent - NOT (why?) + //var exist = (CacheValues) cache.GetCacheItem(key); + //if (exist != null) + // cache.ClearCacheItem(PublishedMediaCacheKey + GetValuesValue(exist.Values, "parentID")); + + // clear the item + cache.ClearCacheItem(key); + + // clear all children - in case we moved and their path has changed + var fid = "/" + sid + "/"; + cache.ClearCacheObjectTypes((k, v) => + GetValuesValue(v.Values, "path", "__Path").Contains(fid)); + } + + private static string GetValuesValue(IDictionary d, params string[] keys) + { + string value = null; + var ignored = keys.Any(x => d.TryGetValue(x, out value)); + return value ?? ""; + } + } +} diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index cf4d6e33d4..0c8eda7ecb 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -1,729 +1,736 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Xml; -using System.Xml.Linq; -using Examine; -using Examine.LuceneEngine; -using Lucene.Net.Documents; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Scoping; -using Umbraco.Core.Sync; -using Umbraco.Web.Cache; -using UmbracoExamine; -using Content = umbraco.cms.businesslogic.Content; -using Document = umbraco.cms.businesslogic.web.Document; - -namespace Umbraco.Web.Search -{ - /// - /// Used to wire up events for Examine - /// - public sealed class ExamineEvents : ApplicationEventHandler - { - // the default enlist priority is 100 - // enlist with a lower priority to ensure that anything "default" runs after us - // but greater that SafeXmlReaderWriter priority which is 60 - private const int EnlistPriority = 80; - - /// - /// Once the application has started we should bind to all events and initialize the providers. - /// - /// - /// - /// - /// We need to do this on the Started event as to guarantee that all resolvers are setup properly. - /// - protected override void ApplicationStarted(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext) - { - LogHelper.Info("Initializing Examine and binding to business logic events"); - - var registeredProviders = ExamineManager.Instance.IndexProviderCollection - .OfType().Count(x => x.EnableDefaultEventHandler); - - LogHelper.Info("Adding examine event handlers for index providers: {0}", () => registeredProviders); - - //don't bind event handlers if we're not suppose to listen - if (registeredProviders == 0) - return; - - //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part - // in a load balanced environment. - CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; - CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; - CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; - CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; - CacheRefresherBase.CacheUpdated += ContentTypeCacheRefresherCacheUpdated; - - var contentIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalIndexer] as UmbracoContentIndexer; - if (contentIndexer != null) - { - contentIndexer.DocumentWriting += IndexerDocumentWriting; - } - var memberIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalMemberIndexer] as UmbracoMemberIndexer; - if (memberIndexer != null) - { - memberIndexer.DocumentWriting += IndexerDocumentWriting; - } - } - - /// - /// This is used to refresh content indexers IndexData based on the DataService whenever a content type is changed since - /// properties may have been added/removed, then we need to re-index any required data if aliases have been changed - /// - /// - /// - /// - /// See: http://issues.umbraco.org/issue/U4-4798, http://issues.umbraco.org/issue/U4-7833 - /// - static void ContentTypeCacheRefresherCacheUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs e) - { - if (Suspendable.ExamineEvents.CanIndex == false) - return; - - var indexersToUpdated = ExamineManager.Instance.IndexProviderCollection.OfType(); - foreach (var provider in indexersToUpdated) - { - provider.RefreshIndexerDataFromDataService(); - } - - if (e.MessageType == MessageType.RefreshByJson) - { - var contentTypesChanged = new HashSet(); - var mediaTypesChanged = new HashSet(); - var memberTypesChanged = new HashSet(); - - var payloads = ContentTypeCacheRefresher.DeserializeFromJsonPayload(e.MessageObject.ToString()); - foreach (var payload in payloads) - { - if (payload.IsNew == false - && (payload.WasDeleted || payload.AliasChanged || payload.PropertyRemoved || payload.PropertyTypeAliasChanged)) - { - //if we get here it means that some aliases have changed and the indexes for those particular doc types will need to be updated - if (payload.Type == typeof(IContentType).Name) - { - //if it is content - contentTypesChanged.Add(payload.Alias); - } - else if (payload.Type == typeof(IMediaType).Name) - { - //if it is media - mediaTypesChanged.Add(payload.Alias); - } - else if (payload.Type == typeof(IMemberType).Name) - { - //if it is members - memberTypesChanged.Add(payload.Alias); - } - } - } - - //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up - // the re-indexing process, we don't want to revert to rebuilding the whole thing! - - if (contentTypesChanged.Count > 0) - { - foreach (var alias in contentTypesChanged) - { - var ctType = ApplicationContext.Current.Services.ContentTypeService.GetContentType(alias); - if (ctType != null) - { - var contentItems = ApplicationContext.Current.Services.ContentService.GetContentOfContentType(ctType.Id); - foreach (var contentItem in contentItems) - { - ReIndexForContent(contentItem, contentItem.HasPublishedVersion && contentItem.Trashed == false); - } - } - } - } - if (mediaTypesChanged.Count > 0) - { - foreach (var alias in mediaTypesChanged) - { - var ctType = ApplicationContext.Current.Services.ContentTypeService.GetMediaType(alias); - if (ctType != null) - { - var mediaItems = ApplicationContext.Current.Services.MediaService.GetMediaOfMediaType(ctType.Id); - foreach (var mediaItem in mediaItems) - { - ReIndexForMedia(mediaItem, mediaItem.Trashed == false); - } - } - } - } - if (memberTypesChanged.Count > 0) - { - foreach (var alias in memberTypesChanged) - { - var ctType = ApplicationContext.Current.Services.MemberTypeService.Get(alias); - if (ctType != null) - { - var memberItems = ApplicationContext.Current.Services.MemberService.GetMembersByMemberType(ctType.Id); - foreach (var memberItem in memberItems) - { - ReIndexForMember(memberItem); - } - } - } - } - } - - } - - static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) - { - if (Suspendable.ExamineEvents.CanIndex == false) - return; - - switch (e.MessageType) - { - case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.MemberService.GetById((int)e.MessageObject); - if (c1 != null) - { - ReIndexForMember(c1); - } - break; - case MessageType.RemoveById: - - // This is triggered when the item is permanently deleted - - DeleteIndexForEntity((int)e.MessageObject, false); - break; - case MessageType.RefreshByInstance: - var c3 = e.MessageObject as IMember; - if (c3 != null) - { - ReIndexForMember(c3); - } - break; - case MessageType.RemoveByInstance: - - // This is triggered when the item is permanently deleted - - var c4 = e.MessageObject as IMember; - if (c4 != null) - { - DeleteIndexForEntity(c4.Id, false); - } - break; - case MessageType.RefreshAll: - case MessageType.RefreshByJson: - default: - //We don't support these, these message types will not fire for unpublished content - break; - } - } - - /// - /// Handles index management for all media events - basically handling saving/copying/trashing/deleting - /// - /// - /// - static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) - { - if (Suspendable.ExamineEvents.CanIndex == false) - return; - - switch (e.MessageType) - { - case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); - if (c1 != null) - { - ReIndexForMedia(c1, c1.Trashed == false); - } - break; - case MessageType.RemoveById: - var c2 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); - if (c2 != null) - { - //This is triggered when the item has trashed. - // So we need to delete the index from all indexes not supporting unpublished content. - - DeleteIndexForEntity(c2.Id, true); - - //We then need to re-index this item for all indexes supporting unpublished content - - ReIndexForMedia(c2, false); - } - break; - case MessageType.RefreshByJson: - - var jsonPayloads = MediaCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); - if (jsonPayloads.Any()) - { - foreach (var payload in jsonPayloads) - { - switch (payload.Operation) - { - case MediaCacheRefresher.OperationType.Saved: - var media1 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); - if (media1 != null) - { - ReIndexForMedia(media1, media1.Trashed == false); - } - break; - case MediaCacheRefresher.OperationType.Trashed: - - //keep if trashed for indexes supporting unpublished - //(delete the index from all indexes not supporting unpublished content) - - DeleteIndexForEntity(payload.Id, true); - - //We then need to re-index this item for all indexes supporting unpublished content - var media2 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); - if (media2 != null) - { - ReIndexForMedia(media2, false); - } - - break; - case MediaCacheRefresher.OperationType.Deleted: - - //permanently remove from all indexes - - DeleteIndexForEntity(payload.Id, false); - - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - break; - case MessageType.RefreshByInstance: - case MessageType.RemoveByInstance: - case MessageType.RefreshAll: - default: - //We don't support these, these message types will not fire for media - break; - } - } - - /// - /// Handles index management for all published content events - basically handling published/unpublished - /// - /// - /// - /// - /// This will execute on all servers taking part in load balancing - /// - static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) - { - if (Suspendable.ExamineEvents.CanIndex == false) - return; - - switch (e.MessageType) - { - case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); - if (c1 != null) - { - ReIndexForContent(c1, true); - } - break; - case MessageType.RemoveById: - - //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). - - var c2 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); - if (c2 != null) - { - // So we need to delete the index from all indexes not supporting unpublished content. - - DeleteIndexForEntity(c2.Id, true); - - // We then need to re-index this item for all indexes supporting unpublished content - - ReIndexForContent(c2, false); - } - break; - case MessageType.RefreshByInstance: - var c3 = e.MessageObject as IContent; - if (c3 != null) - { - ReIndexForContent(c3, true); - } - break; - case MessageType.RemoveByInstance: - - //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). - - var c4 = e.MessageObject as IContent; - if (c4 != null) - { - // So we need to delete the index from all indexes not supporting unpublished content. - - DeleteIndexForEntity(c4.Id, true); - - // We then need to re-index this item for all indexes supporting unpublished content - - ReIndexForContent(c4, false); - } - break; - case MessageType.RefreshAll: - case MessageType.RefreshByJson: - default: - //We don't support these for examine indexing - break; - } - } - - /// - /// Handles index management for all unpublished content events - basically handling saving/copying/deleting - /// - /// - /// - /// - /// This will execute on all servers taking part in load balancing - /// - static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) - { - if (Suspendable.ExamineEvents.CanIndex == false) - return; - - switch (e.MessageType) - { - case MessageType.RefreshById: - var c1 = ApplicationContext.Current.Services.ContentService.GetById((int) e.MessageObject); - if (c1 != null) - { - ReIndexForContent(c1, false); - } - break; - case MessageType.RemoveById: - - // This is triggered when the item is permanently deleted - - DeleteIndexForEntity((int)e.MessageObject, false); - break; - case MessageType.RefreshByInstance: - var c3 = e.MessageObject as IContent; - if (c3 != null) - { - ReIndexForContent(c3, false); - } - break; - case MessageType.RemoveByInstance: - - // This is triggered when the item is permanently deleted - - var c4 = e.MessageObject as IContent; - if (c4 != null) - { - DeleteIndexForEntity(c4.Id, false); - } - break; - case MessageType.RefreshByJson: - - var jsonPayloads = UnpublishedPageCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); - if (jsonPayloads.Any()) - { - foreach (var payload in jsonPayloads) - { - switch (payload.Operation) - { - case UnpublishedPageCacheRefresher.OperationType.Deleted: - - //permanently remove from all indexes - - DeleteIndexForEntity(payload.Id, false); - - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - break; - - case MessageType.RefreshAll: - default: - //We don't support these, these message types will not fire for unpublished content - break; - } - } - - private static void ReIndexForMember(IMember member) - { - var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); - if (actions != null) - actions.Add(new DeferedReIndexForMember(member)); - else - DeferedReIndexForMember.Execute(member); - } - - /// - /// Event handler to create a lower cased version of the node name, this is so we can support case-insensitive searching and still - /// use the Whitespace Analyzer - /// - /// - /// - - private static void IndexerDocumentWriting(object sender, DocumentWritingEventArgs e) - { - if (e.Fields.Keys.Contains("nodeName")) - { - //TODO: This logic should really be put into the content indexer instead of hidden here!! - - //add the lower cased version - e.Document.Add(new Field("__nodeName", - e.Fields["nodeName"].ToLower(), - Field.Store.YES, - Field.Index.ANALYZED, - Field.TermVector.NO - )); - } - } - - private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) - { - var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); - if (actions != null) - actions.Add(new DeferedReIndexForMedia(sender, isMediaPublished)); - else - DeferedReIndexForMedia.Execute(sender, isMediaPublished); - } - - /// - /// Remove items from any index that doesn't support unpublished content - /// - /// - /// - /// If true, indicates that we will only delete this item from indexes that don't support unpublished content. - /// If false it will delete this from all indexes regardless. - /// - private static void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) - { - var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); - if (actions != null) - actions.Add(new DeferedDeleteIndex(entityId, keepIfUnpublished)); - else - DeferedDeleteIndex.Execute(entityId, keepIfUnpublished); - } - - /// - /// Re-indexes a content item whether published or not but only indexes them for indexes supporting unpublished content - /// - /// - /// - /// Value indicating whether the item is published or not - /// - private static void ReIndexForContent(IContent sender, bool isContentPublished) - { - var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); - if (actions != null) - actions.Add(new DeferedReIndexForContent(sender, isContentPublished)); - else - DeferedReIndexForContent.Execute(sender, isContentPublished); - } - - private class DeferedActions - { - private readonly List _actions = new List(); - - public static DeferedActions Get(IScopeProvider scopeProvider) - { - var scopeContext = scopeProvider.Context; - if (scopeContext == null) return null; - - return scopeContext.Enlist("examineEvents", - () => new DeferedActions(), // creator - (completed, actions) => // action - { - if (completed) actions.Execute(); - }, EnlistPriority); - } - - public void Add(DeferedAction action) - { - _actions.Add(action); - } - - private void Execute() - { - foreach (var action in _actions) - action.Execute(); - } - } - - private abstract class DeferedAction - { - public virtual void Execute() - { } - } - - private class DeferedReIndexForContent : DeferedAction - { - private readonly IContent _content; - private readonly bool _isPublished; - - public DeferedReIndexForContent(IContent content, bool isPublished) - { - _content = content; - _isPublished = isPublished; - } - - public override void Execute() - { - Execute(_content, _isPublished); - } - - public static void Execute(IContent content, bool isPublished) - { - var xml = content.ToXml(); - //add an icon attribute to get indexed - xml.Add(new XAttribute("icon", content.ContentType.Icon)); - - ExamineManager.Instance.ReIndexNode( - xml, IndexTypes.Content, - ExamineManager.Instance.IndexProviderCollection.OfType() - - //Index this item for all indexers if the content is published, otherwise if the item is not published - // then only index this for indexers supporting unpublished content - - .Where(x => isPublished || (x.SupportUnpublishedContent)) - .Where(x => x.EnableDefaultEventHandler)); - } - } - - private class DeferedReIndexForMedia : DeferedAction - { - private readonly IMedia _media; - private readonly bool _isPublished; - - public DeferedReIndexForMedia(IMedia media, bool isPublished) - { - _media = media; - _isPublished = isPublished; - } - - public override void Execute() - { - Execute(_media, _isPublished); - } - - public static void Execute(IMedia media, bool isPublished) - { - var xml = media.ToXml(); - //add an icon attribute to get indexed - xml.Add(new XAttribute("icon", media.ContentType.Icon)); - - ExamineManager.Instance.ReIndexNode( - xml, IndexTypes.Media, - ExamineManager.Instance.IndexProviderCollection.OfType() - - //Index this item for all indexers if the media is not trashed, otherwise if the item is trashed - // then only index this for indexers supporting unpublished media - - .Where(x => isPublished || (x.SupportUnpublishedContent)) - .Where(x => x.EnableDefaultEventHandler)); - } - } - - private class DeferedReIndexForMember : DeferedAction - { - private readonly IMember _member; - - public DeferedReIndexForMember(IMember member) - { - _member = member; - } - - public override void Execute() - { - Execute(_member); - } - - public static void Execute(IMember member) - { - ExamineManager.Instance.ReIndexNode( - member.ToXml(), IndexTypes.Member, - ExamineManager.Instance.IndexProviderCollection.OfType() - //ensure that only the providers are flagged to listen execute - .Where(x => x.EnableDefaultEventHandler)); - } - } - - private class DeferedDeleteIndex : DeferedAction - { - private readonly int _id; - private readonly bool _keepIfUnpublished; - - public DeferedDeleteIndex(int id, bool keepIfUnpublished) - { - _id = id; - _keepIfUnpublished = keepIfUnpublished; - } - - public override void Execute() - { - Execute(_id, _keepIfUnpublished); - } - - public static void Execute(int id, bool keepIfUnpublished) - { - ExamineManager.Instance.DeleteFromIndex( - id.ToString(CultureInfo.InvariantCulture), - ExamineManager.Instance.IndexProviderCollection.OfType() - - //if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, - // otherwise if keepIfUnpublished == false then remove from all indexes - - .Where(x => keepIfUnpublished == false || x.SupportUnpublishedContent == false) - .Where(x => x.EnableDefaultEventHandler)); - } - } - - /// - /// Converts a content node to XDocument - /// - /// - /// true if data is going to be returned from cache - /// - [Obsolete("This method is no longer used and will be removed from the core in future versions, the cacheOnly parameter has no effect. Use the other ToXDocument overload instead")] - public static XDocument ToXDocument(Content node, bool cacheOnly) - { - return ToXDocument(node); - } - - /// - /// Converts a content node to Xml - /// - /// - /// - private static XDocument ToXDocument(Content node) - { - if (TypeHelper.IsTypeAssignableFrom(node)) - { - return new XDocument(((Document) node).ContentEntity.ToXml()); - } - - if (TypeHelper.IsTypeAssignableFrom(node)) - { - return new XDocument(((global::umbraco.cms.businesslogic.media.Media) node).MediaItem.ToXml()); - } - - var xDoc = new XmlDocument(); - var xNode = xDoc.CreateNode(XmlNodeType.Element, "node", ""); - node.XmlPopulate(xDoc, ref xNode, false); - - if (xNode.Attributes["nodeTypeAlias"] == null) - { - //we'll add the nodeTypeAlias ourselves - XmlAttribute d = xDoc.CreateAttribute("nodeTypeAlias"); - d.Value = node.ContentType.Alias; - xNode.Attributes.Append(d); - } - - return new XDocument(ExamineXmlExtensions.ToXElement(xNode)); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Examine; +using Examine.LuceneEngine; +using Lucene.Net.Documents; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Scoping; +using Umbraco.Core.Sync; +using Umbraco.Web.Cache; +using UmbracoExamine; +using Content = umbraco.cms.businesslogic.Content; +using Document = umbraco.cms.businesslogic.web.Document; + +namespace Umbraco.Web.Search +{ + /// + /// Used to wire up events for Examine + /// + public sealed class ExamineEvents : ApplicationEventHandler + { + // the default enlist priority is 100 + // enlist with a lower priority to ensure that anything "default" runs after us + // but greater that SafeXmlReaderWriter priority which is 60 + private const int EnlistPriority = 80; + + /// + /// Once the application has started we should bind to all events and initialize the providers. + /// + /// + /// + /// + /// We need to do this on the Started event as to guarantee that all resolvers are setup properly. + /// + protected override void ApplicationStarted(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext) + { + LogHelper.Info("Initializing Examine and binding to business logic events"); + + var registeredProviders = ExamineManager.Instance.IndexProviderCollection + .OfType().Count(x => x.EnableDefaultEventHandler); + + LogHelper.Info("Adding examine event handlers for index providers: {0}", () => registeredProviders); + + //don't bind event handlers if we're not suppose to listen + if (registeredProviders == 0) + return; + + //Bind to distributed cache events - this ensures that this logic occurs on ALL servers that are taking part + // in a load balanced environment. + CacheRefresherBase.CacheUpdated += UnpublishedPageCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += PublishedPageCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += MediaCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += MemberCacheRefresherCacheUpdated; + CacheRefresherBase.CacheUpdated += ContentTypeCacheRefresherCacheUpdated; + + var contentIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalIndexer] as UmbracoContentIndexer; + if (contentIndexer != null) + { + contentIndexer.DocumentWriting += IndexerDocumentWriting; + } + var memberIndexer = ExamineManager.Instance.IndexProviderCollection[Constants.Examine.InternalMemberIndexer] as UmbracoMemberIndexer; + if (memberIndexer != null) + { + memberIndexer.DocumentWriting += IndexerDocumentWriting; + } + } + + /// + /// This is used to refresh content indexers IndexData based on the DataService whenever a content type is changed since + /// properties may have been added/removed, then we need to re-index any required data if aliases have been changed + /// + /// + /// + /// + /// See: http://issues.umbraco.org/issue/U4-4798, http://issues.umbraco.org/issue/U4-7833 + /// + static void ContentTypeCacheRefresherCacheUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs e) + { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + + var indexersToUpdated = ExamineManager.Instance.IndexProviderCollection.OfType(); + foreach (var provider in indexersToUpdated) + { + provider.RefreshIndexerDataFromDataService(); + } + + if (e.MessageType == MessageType.RefreshByJson) + { + var contentTypesChanged = new HashSet(); + var mediaTypesChanged = new HashSet(); + var memberTypesChanged = new HashSet(); + + var payloads = ContentTypeCacheRefresher.DeserializeFromJsonPayload(e.MessageObject.ToString()); + foreach (var payload in payloads) + { + if (payload.IsNew == false + && (payload.WasDeleted || payload.AliasChanged || payload.PropertyRemoved || payload.PropertyTypeAliasChanged)) + { + //if we get here it means that some aliases have changed and the indexes for those particular doc types will need to be updated + if (payload.Type == typeof(IContentType).Name) + { + //if it is content + contentTypesChanged.Add(payload.Alias); + } + else if (payload.Type == typeof(IMediaType).Name) + { + //if it is media + mediaTypesChanged.Add(payload.Alias); + } + else if (payload.Type == typeof(IMemberType).Name) + { + //if it is members + memberTypesChanged.Add(payload.Alias); + } + } + } + + //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up + // the re-indexing process, we don't want to revert to rebuilding the whole thing! + + if (contentTypesChanged.Count > 0) + { + foreach (var alias in contentTypesChanged) + { + var ctType = ApplicationContext.Current.Services.ContentTypeService.GetContentType(alias); + if (ctType != null) + { + var contentItems = ApplicationContext.Current.Services.ContentService.GetContentOfContentType(ctType.Id); + foreach (var contentItem in contentItems) + { + ReIndexForContent(contentItem, contentItem.HasPublishedVersion && contentItem.Trashed == false); + } + } + } + } + if (mediaTypesChanged.Count > 0) + { + foreach (var alias in mediaTypesChanged) + { + var ctType = ApplicationContext.Current.Services.ContentTypeService.GetMediaType(alias); + if (ctType != null) + { + var mediaItems = ApplicationContext.Current.Services.MediaService.GetMediaOfMediaType(ctType.Id); + foreach (var mediaItem in mediaItems) + { + ReIndexForMedia(mediaItem, mediaItem.Trashed == false); + } + } + } + } + if (memberTypesChanged.Count > 0) + { + foreach (var alias in memberTypesChanged) + { + var ctType = ApplicationContext.Current.Services.MemberTypeService.Get(alias); + if (ctType != null) + { + var memberItems = ApplicationContext.Current.Services.MemberService.GetMembersByMemberType(ctType.Id); + foreach (var memberItem in memberItems) + { + ReIndexForMember(memberItem); + } + } + } + } + } + + } + + static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) + { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.MemberService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForMember(c1); + } + break; + case MessageType.RemoveById: + + // This is triggered when the item is permanently deleted + + DeleteIndexForEntity((int)e.MessageObject, false); + break; + case MessageType.RefreshByInstance: + var c3 = e.MessageObject as IMember; + if (c3 != null) + { + ReIndexForMember(c3); + } + break; + case MessageType.RemoveByInstance: + + // This is triggered when the item is permanently deleted + + var c4 = e.MessageObject as IMember; + if (c4 != null) + { + DeleteIndexForEntity(c4.Id, false); + } + break; + case MessageType.RefreshAll: + case MessageType.RefreshByJson: + default: + //We don't support these, these message types will not fire for unpublished content + break; + } + } + + /// + /// Handles index management for all media events - basically handling saving/copying/trashing/deleting + /// + /// + /// + static void MediaCacheRefresherCacheUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs e) + { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForMedia(c1, c1.Trashed == false); + } + break; + case MessageType.RemoveById: + var c2 = ApplicationContext.Current.Services.MediaService.GetById((int)e.MessageObject); + if (c2 != null) + { + //This is triggered when the item has trashed. + // So we need to delete the index from all indexes not supporting unpublished content. + + DeleteIndexForEntity(c2.Id, true); + + //We then need to re-index this item for all indexes supporting unpublished content + + ReIndexForMedia(c2, false); + } + break; + case MessageType.RefreshByJson: + + var jsonPayloads = MediaCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); + if (jsonPayloads.Any()) + { + foreach (var payload in jsonPayloads) + { + switch (payload.Operation) + { + case MediaCacheRefresher.OperationType.Saved: + var media1 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); + if (media1 != null) + { + ReIndexForMedia(media1, media1.Trashed == false); + } + break; + case MediaCacheRefresher.OperationType.Trashed: + + //keep if trashed for indexes supporting unpublished + //(delete the index from all indexes not supporting unpublished content) + + DeleteIndexForEntity(payload.Id, true); + + //We then need to re-index this item for all indexes supporting unpublished content + var media2 = ApplicationContext.Current.Services.MediaService.GetById(payload.Id); + if (media2 != null) + { + ReIndexForMedia(media2, false); + } + + break; + case MediaCacheRefresher.OperationType.Deleted: + + //permanently remove from all indexes + + DeleteIndexForEntity(payload.Id, false); + + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + break; + case MessageType.RefreshByInstance: + case MessageType.RemoveByInstance: + case MessageType.RefreshAll: + default: + //We don't support these, these message types will not fire for media + break; + } + } + + /// + /// Handles index management for all published content events - basically handling published/unpublished + /// + /// + /// + /// + /// This will execute on all servers taking part in load balancing + /// + static void PublishedPageCacheRefresherCacheUpdated(PageCacheRefresher sender, CacheRefresherEventArgs e) + { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); + if (c1 != null) + { + ReIndexForContent(c1, true); + } + break; + case MessageType.RemoveById: + + //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). + + var c2 = ApplicationContext.Current.Services.ContentService.GetById((int)e.MessageObject); + if (c2 != null) + { + // So we need to delete the index from all indexes not supporting unpublished content. + + DeleteIndexForEntity(c2.Id, true); + + // We then need to re-index this item for all indexes supporting unpublished content + + ReIndexForContent(c2, false); + } + break; + case MessageType.RefreshByInstance: + var c3 = e.MessageObject as IContent; + if (c3 != null) + { + ReIndexForContent(c3, true); + } + break; + case MessageType.RemoveByInstance: + + //This is triggered when the item has been unpublished or trashed (which also performs an unpublish). + + var c4 = e.MessageObject as IContent; + if (c4 != null) + { + // So we need to delete the index from all indexes not supporting unpublished content. + + DeleteIndexForEntity(c4.Id, true); + + // We then need to re-index this item for all indexes supporting unpublished content + + ReIndexForContent(c4, false); + } + break; + case MessageType.RefreshAll: + case MessageType.RefreshByJson: + default: + //We don't support these for examine indexing + break; + } + } + + /// + /// Handles index management for all unpublished content events - basically handling saving/copying/deleting + /// + /// + /// + /// + /// This will execute on all servers taking part in load balancing + /// + static void UnpublishedPageCacheRefresherCacheUpdated(UnpublishedPageCacheRefresher sender, CacheRefresherEventArgs e) + { + if (Suspendable.ExamineEvents.CanIndex == false) + return; + + switch (e.MessageType) + { + case MessageType.RefreshById: + var c1 = ApplicationContext.Current.Services.ContentService.GetById((int) e.MessageObject); + if (c1 != null) + { + ReIndexForContent(c1, false); + } + break; + case MessageType.RemoveById: + + // This is triggered when the item is permanently deleted + + DeleteIndexForEntity((int)e.MessageObject, false); + break; + case MessageType.RefreshByInstance: + var c3 = e.MessageObject as IContent; + if (c3 != null) + { + ReIndexForContent(c3, false); + } + break; + case MessageType.RemoveByInstance: + + // This is triggered when the item is permanently deleted + + var c4 = e.MessageObject as IContent; + if (c4 != null) + { + DeleteIndexForEntity(c4.Id, false); + } + break; + case MessageType.RefreshByJson: + + var jsonPayloads = UnpublishedPageCacheRefresher.DeserializeFromJsonPayload((string)e.MessageObject); + if (jsonPayloads.Any()) + { + foreach (var payload in jsonPayloads) + { + switch (payload.Operation) + { + case UnpublishedPageCacheRefresher.OperationType.Deleted: + + //permanently remove from all indexes + + DeleteIndexForEntity(payload.Id, false); + + break; + case UnpublishedPageCacheRefresher.OperationType.Refresh:// RefreshNode or RefreshBranch (maybe trashed) + var c2 = ApplicationContext.Current.Services.ContentService.GetById(payload.Id); + if (c2 != null) + { + ReIndexForContent(c2, false); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + break; + + case MessageType.RefreshAll: + default: + //We don't support these, these message types will not fire for unpublished content + break; + } + } + + private static void ReIndexForMember(IMember member) + { + var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); + if (actions != null) + actions.Add(new DeferedReIndexForMember(member)); + else + DeferedReIndexForMember.Execute(member); + } + + /// + /// Event handler to create a lower cased version of the node name, this is so we can support case-insensitive searching and still + /// use the Whitespace Analyzer + /// + /// + /// + + private static void IndexerDocumentWriting(object sender, DocumentWritingEventArgs e) + { + if (e.Fields.Keys.Contains("nodeName")) + { + //TODO: This logic should really be put into the content indexer instead of hidden here!! + + //add the lower cased version + e.Document.Add(new Field("__nodeName", + e.Fields["nodeName"].ToLower(), + Field.Store.YES, + Field.Index.ANALYZED, + Field.TermVector.NO + )); + } + } + + private static void ReIndexForMedia(IMedia sender, bool isMediaPublished) + { + var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); + if (actions != null) + actions.Add(new DeferedReIndexForMedia(sender, isMediaPublished)); + else + DeferedReIndexForMedia.Execute(sender, isMediaPublished); + } + + /// + /// Remove items from any index that doesn't support unpublished content + /// + /// + /// + /// If true, indicates that we will only delete this item from indexes that don't support unpublished content. + /// If false it will delete this from all indexes regardless. + /// + private static void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); + if (actions != null) + actions.Add(new DeferedDeleteIndex(entityId, keepIfUnpublished)); + else + DeferedDeleteIndex.Execute(entityId, keepIfUnpublished); + } + + /// + /// Re-indexes a content item whether published or not but only indexes them for indexes supporting unpublished content + /// + /// + /// + /// Value indicating whether the item is published or not + /// + private static void ReIndexForContent(IContent sender, bool isContentPublished) + { + var actions = DeferedActions.Get(ApplicationContext.Current.ScopeProvider); + if (actions != null) + actions.Add(new DeferedReIndexForContent(sender, isContentPublished)); + else + DeferedReIndexForContent.Execute(sender, isContentPublished); + } + + private class DeferedActions + { + private readonly List _actions = new List(); + + public static DeferedActions Get(IScopeProvider scopeProvider) + { + var scopeContext = scopeProvider.Context; + if (scopeContext == null) return null; + + return scopeContext.Enlist("examineEvents", + () => new DeferedActions(), // creator + (completed, actions) => // action + { + if (completed) actions.Execute(); + }, EnlistPriority); + } + + public void Add(DeferedAction action) + { + _actions.Add(action); + } + + private void Execute() + { + foreach (var action in _actions) + action.Execute(); + } + } + + private abstract class DeferedAction + { + public virtual void Execute() + { } + } + + private class DeferedReIndexForContent : DeferedAction + { + private readonly IContent _content; + private readonly bool _isPublished; + + public DeferedReIndexForContent(IContent content, bool isPublished) + { + _content = content; + _isPublished = isPublished; + } + + public override void Execute() + { + Execute(_content, _isPublished); + } + + public static void Execute(IContent content, bool isPublished) + { + var xml = content.ToXml(); + //add an icon attribute to get indexed + xml.Add(new XAttribute("icon", content.ContentType.Icon)); + + ExamineManager.Instance.ReIndexNode( + xml, IndexTypes.Content, + ExamineManager.Instance.IndexProviderCollection.OfType() + + //Index this item for all indexers if the content is published, otherwise if the item is not published + // then only index this for indexers supporting unpublished content + + .Where(x => isPublished || (x.SupportUnpublishedContent)) + .Where(x => x.EnableDefaultEventHandler)); + } + } + + private class DeferedReIndexForMedia : DeferedAction + { + private readonly IMedia _media; + private readonly bool _isPublished; + + public DeferedReIndexForMedia(IMedia media, bool isPublished) + { + _media = media; + _isPublished = isPublished; + } + + public override void Execute() + { + Execute(_media, _isPublished); + } + + public static void Execute(IMedia media, bool isPublished) + { + var xml = media.ToXml(); + //add an icon attribute to get indexed + xml.Add(new XAttribute("icon", media.ContentType.Icon)); + + ExamineManager.Instance.ReIndexNode( + xml, IndexTypes.Media, + ExamineManager.Instance.IndexProviderCollection.OfType() + + //Index this item for all indexers if the media is not trashed, otherwise if the item is trashed + // then only index this for indexers supporting unpublished media + + .Where(x => isPublished || (x.SupportUnpublishedContent)) + .Where(x => x.EnableDefaultEventHandler)); + } + } + + private class DeferedReIndexForMember : DeferedAction + { + private readonly IMember _member; + + public DeferedReIndexForMember(IMember member) + { + _member = member; + } + + public override void Execute() + { + Execute(_member); + } + + public static void Execute(IMember member) + { + ExamineManager.Instance.ReIndexNode( + member.ToXml(), IndexTypes.Member, + ExamineManager.Instance.IndexProviderCollection.OfType() + //ensure that only the providers are flagged to listen execute + .Where(x => x.EnableDefaultEventHandler)); + } + } + + private class DeferedDeleteIndex : DeferedAction + { + private readonly int _id; + private readonly bool _keepIfUnpublished; + + public DeferedDeleteIndex(int id, bool keepIfUnpublished) + { + _id = id; + _keepIfUnpublished = keepIfUnpublished; + } + + public override void Execute() + { + Execute(_id, _keepIfUnpublished); + } + + public static void Execute(int id, bool keepIfUnpublished) + { + ExamineManager.Instance.DeleteFromIndex( + id.ToString(CultureInfo.InvariantCulture), + ExamineManager.Instance.IndexProviderCollection.OfType() + + //if keepIfUnpublished == true then only delete this item from indexes not supporting unpublished content, + // otherwise if keepIfUnpublished == false then remove from all indexes + + .Where(x => keepIfUnpublished == false || x.SupportUnpublishedContent == false) + .Where(x => x.EnableDefaultEventHandler)); + } + } + + /// + /// Converts a content node to XDocument + /// + /// + /// true if data is going to be returned from cache + /// + [Obsolete("This method is no longer used and will be removed from the core in future versions, the cacheOnly parameter has no effect. Use the other ToXDocument overload instead")] + public static XDocument ToXDocument(Content node, bool cacheOnly) + { + return ToXDocument(node); + } + + /// + /// Converts a content node to Xml + /// + /// + /// + private static XDocument ToXDocument(Content node) + { + if (TypeHelper.IsTypeAssignableFrom(node)) + { + return new XDocument(((Document) node).ContentEntity.ToXml()); + } + + if (TypeHelper.IsTypeAssignableFrom(node)) + { + return new XDocument(((global::umbraco.cms.businesslogic.media.Media) node).MediaItem.ToXml()); + } + + var xDoc = new XmlDocument(); + var xNode = xDoc.CreateNode(XmlNodeType.Element, "node", ""); + node.XmlPopulate(xDoc, ref xNode, false); + + if (xNode.Attributes["nodeTypeAlias"] == null) + { + //we'll add the nodeTypeAlias ourselves + XmlAttribute d = xDoc.CreateAttribute("nodeTypeAlias"); + d.Value = node.ContentType.Alias; + xNode.Attributes.Append(d); + } + + return new XDocument(ExamineXmlExtensions.ToXElement(xNode)); + } + } +} From 4d5f34a63ddb00507bffbf409366834bdb59c627 Mon Sep 17 00:00:00 2001 From: Kieron Boswell Date: Thu, 16 Jan 2020 16:15:07 +0000 Subject: [PATCH 204/610] Spelling mistake on word Success in instructional comment. (#6966) --- .../Umbraco/PartialViewMacros/Templates/EditProfile.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml index 7034404ca3..74ec033f25 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml @@ -23,7 +23,7 @@ { if (success) { - @* This message will show if RedirectOnSucces is set to false (default) *@ + @* This message will show if profileModel.RedirectUrl is not defined (default) *@

Profile updated

} From f5a5a9a2971fd87829f5a7561e8a0afc0f9d2003 Mon Sep 17 00:00:00 2001 From: Kieron Boswell Date: Thu, 16 Jan 2020 16:19:54 +0000 Subject: [PATCH 205/610] Spelling mistake in comments of partial snippet (#6975) --- .../Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index 17ed95ea31..804e2307f0 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -45,7 +45,7 @@ @if (success) { - @* This message will show if RedirectOnSucces is set to false (default) *@ + @* This message will show if registerModel.RedirectUrl is not defined (default) *@

Registration succeeded.

} else From b272a3b791a80c566459198df8e11ab920b20130 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Jan 2020 19:39:17 +0100 Subject: [PATCH 206/610] https://github.com/umbraco/Umbraco-CMS/issues/7466 - Fixes issue with external ModelsBuilder and Dll or LiveDll mode. The issue is because the Mode is determined in the contructor, and not lazy. --- .../Configuration/ModelsBuilderConfig.cs | 90 ++++++++++++------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs index c6bccdcf87..b5c66b649c 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Configuration/ModelsBuilderConfig.cs @@ -12,6 +12,9 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration ///
public class ModelsBuilderConfig : IModelsBuilderConfig { + private const string prefix = "Umbraco.ModelsBuilder."; + private ModelsMode? _modelsMode; + private bool? _flagOutOfDateModels; public const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; public const string DefaultModelsDirectory = "~/App_Data/Models"; @@ -20,8 +23,6 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration ///
public ModelsBuilderConfig() { - const string prefix = "Umbraco.ModelsBuilder."; - // giant kill switch, default: false // must be explicitely set to true for anything else to happen Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; @@ -34,36 +35,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration // stop here, everything is false if (!Enable) return; - // mode - var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; - if (!string.IsNullOrWhiteSpace(modelsMode)) - { - switch (modelsMode) - { - case nameof(ModelsMode.Nothing): - ModelsMode = ModelsMode.Nothing; - break; - case nameof(ModelsMode.PureLive): - ModelsMode = ModelsMode.PureLive; - break; - case nameof(ModelsMode.AppData): - ModelsMode = ModelsMode.AppData; - break; - case nameof(ModelsMode.LiveAppData): - ModelsMode = ModelsMode.LiveAppData; - break; - default: - throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." - + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); - } - } - // default: false AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); // default: true EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); - FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); // default: initialized above with DefaultModelsNamespace const var value = ConfigurationManager.AppSettings[prefix + "ModelsNamespace"]; @@ -92,9 +68,6 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration DebugLevel = debugLevel; } - // not flagging if not generating, or live (incl. pure) - if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) - FlagOutOfDateModels = false; } /// @@ -111,11 +84,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration int debugLevel = 0) { Enable = enable; - ModelsMode = modelsMode; + _modelsMode = modelsMode; ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; EnableFactory = enableFactory; - FlagOutOfDateModels = flagOutOfDateModels; + _flagOutOfDateModels = flagOutOfDateModels; ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; DebugLevel = debugLevel; @@ -164,7 +137,39 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration /// /// Gets the models mode. /// - public ModelsMode ModelsMode { get; } + public ModelsMode ModelsMode + { + get + { + if (!_modelsMode.HasValue) + { + // mode + var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; + if (!string.IsNullOrWhiteSpace(modelsMode)) + { + switch (modelsMode) + { + case nameof(ModelsMode.Nothing): + _modelsMode = ModelsMode.Nothing; + break; + case nameof(ModelsMode.PureLive): + _modelsMode = ModelsMode.PureLive; + break; + case nameof(ModelsMode.AppData): + _modelsMode = ModelsMode.AppData; + break; + case nameof(ModelsMode.LiveAppData): + _modelsMode = ModelsMode.LiveAppData; + break; + default: + throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." + + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); + } + } + } + return _modelsMode.Value; + } + } /// /// Gets a value indicating whether system.web/compilation/@debug is true. @@ -196,7 +201,24 @@ namespace Umbraco.ModelsBuilder.Embedded.Configuration /// Models become out-of-date when data types or content types are updated. When this /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are /// generated through the dashboard, the files is cleared. Default value is false. - public bool FlagOutOfDateModels { get; } + public bool FlagOutOfDateModels + { + get + { + if (!_flagOutOfDateModels.HasValue) + { + var flagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); + + if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) + { + flagOutOfDateModels = false; + } + + _flagOutOfDateModels = flagOutOfDateModels; + } + return _flagOutOfDateModels.Value; + } + } /// /// Gets the models directory. From f3d9a01800765b26478a2495c02c9c072571f7af Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 23:00:28 +0100 Subject: [PATCH 207/610] Hide the "Add" button for Nested Content in "single mode" (#7327) --- .../nestedcontent/nestedcontent.propertyeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index 91afbf3ba9..da6e466b50 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -40,7 +40,7 @@
- -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index cfb4b2c573..1d4b646e05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -36,7 +36,7 @@
    -
  • +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js index 1d865f2fa0..53a098a26e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { + function PermissionsController($scope, $timeout, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { /* ----------- SCOPE VARIABLES ----------- */ @@ -13,6 +13,7 @@ vm.addChild = addChild; vm.removeChild = removeChild; + vm.sortChildren = sortChildren; vm.toggle = toggle; /* ---------- INIT ---------- */ @@ -71,6 +72,13 @@ $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } + function sortChildren() { + // we need to wait until the next digest cycle for vm.selectedChildren to be updated + $timeout(function () { + $scope.model.allowedContentTypes = _.pluck(vm.selectedChildren, "id"); + }); + } + /** * Toggle the $scope.model.allowAsRoot value to either true or false */ diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html index caa4c4dc11..09e7ab4f41 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html @@ -27,7 +27,8 @@ parent-icon="model.icon" parent-id="model.id" on-add="vm.addChild" - on-remove="vm.removeChild"> + on-remove="vm.removeChild" + on-sort="vm.sortChildren">
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index a566c2cdc7..9c248f186b 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -432,11 +432,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.ContentTypeBaseServices.GetContentTypeOf(contentItem); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.ContentTypeService.GetAll(ids).ToList(); + types = Services.ContentTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Where(type => type.IsElement == false).Select(Mapper.Map).ToList(); @@ -459,7 +459,7 @@ namespace Umbraco.Web.Editors } } - return basics; + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 43569c77e2..3a4026423a 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -240,11 +240,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.MediaTypeService.Get(contentItem.ContentTypeId); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.MediaTypeService.GetAll(ids).ToList(); + types = Services.MediaTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Select(Mapper.Map).ToList(); @@ -255,7 +255,7 @@ namespace Umbraco.Web.Editors basic.Description = TranslateItem(basic.Description); } - return basics.OrderBy(x => x.Name); + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 266cd894b6..f18c481297 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -493,7 +493,7 @@ namespace Umbraco.Web.Models.Mapping target.Udi = MapContentTypeUdi(source); target.UpdateDate = source.UpdateDate; - target.AllowedContentTypes = source.AllowedContentTypes.Select(x => x.Id.Value); + target.AllowedContentTypes = source.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); target.LockedCompositeContentTypes = MapLockedCompositions(source); } From 9a7b72c8c12b3cc1fa8233fae67ef9166382debe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20St=C3=B6cker?= <1537988+MarcStoecker@users.noreply.github.com> Date: Mon, 20 Jan 2020 13:39:22 +0100 Subject: [PATCH 219/610] Fix typo in DE "installFinish" caption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (and improve wording: "Abschließen" (~ completing something) instead of "Quit" or "Close" as it was; EN is "Finishing") --- src/Umbraco.Web.UI/Umbraco/config/lang/de.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml index e45d97e7bd..9c02879db9 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml @@ -1173,7 +1173,7 @@ Installiert Installierte Pakete Lokale Installation - Benenden + Abschließen Diese Paket hat keine Einstellungen Es wurden noche keine Pakete angelegt Sie haben keine Pakete installiert From 7fbe4829196d7f131a4bb5e9a6d8263b25eda8a4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 20 Jan 2020 14:09:13 +0100 Subject: [PATCH 220/610] Revert "https://github.com/umbraco/Umbraco-CMS/issues/7469 - Fixed Ambiguous extension method issue by renaming our extension method from Value to GetValue" This reverts commit baef282b --- .../PublishedElementExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index 2c22caf87d..29429ba74f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -17,14 +17,14 @@ namespace Umbraco.Web /// /// Gets the value of a property. /// - public static TValue ValueByExpression(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); return model.Value(alias, culture, segment, fallback, defaultValue); } - //This cannot be public due to ambiguous issue with external ModelsBuilder if we do not rename. + // fixme that one should be public so ppl can use it private static string GetAlias(TModel model, Expression> property) { if (property.NodeType != ExpressionType.Lambda) @@ -45,7 +45,7 @@ namespace Umbraco.Web var attribute = member.GetCustomAttribute(); if (attribute == null) throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - + return attribute.Alias; } } From 8cc78631a08f0084fbcf19a296c1cec348a1d09a Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 21 Jan 2020 13:33:39 +1100 Subject: [PATCH 221/610] Adds TestData project along with test endpoints for enabling/disabling segments --- .../Properties/AssemblyInfo.cs | 36 +++ src/Umbraco.TestData/SegmentTestController.cs | 77 +++++ src/Umbraco.TestData/Umbraco.TestData.csproj | 70 +++++ .../UmbracoTestDataController.cs | 288 ++++++++++++++++++ src/Umbraco.TestData/readme.md | 51 ++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 + src/Umbraco.Web.UI/web.Template.Debug.config | 5 + src/umbraco.sln | 7 + 8 files changed, 538 insertions(+) create mode 100644 src/Umbraco.TestData/Properties/AssemblyInfo.cs create mode 100644 src/Umbraco.TestData/SegmentTestController.cs create mode 100644 src/Umbraco.TestData/Umbraco.TestData.csproj create mode 100644 src/Umbraco.TestData/UmbracoTestDataController.cs create mode 100644 src/Umbraco.TestData/readme.md diff --git a/src/Umbraco.TestData/Properties/AssemblyInfo.cs b/src/Umbraco.TestData/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3c4251cdf6 --- /dev/null +++ b/src/Umbraco.TestData/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Umbraco.TestData")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.TestData")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fb5676ed-7a69-492c-b802-e7b24144c0fc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs new file mode 100644 index 0000000000..bcfdfc4ac4 --- /dev/null +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.TestData +{ + public class SegmentTestController : SurfaceController + { + + public ActionResult EnableDocTypeSegments(string alias, string propertyTypeAlias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + var propType = ct.PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + if (propType == null) + return Content($"The document type {alias} does not have a property type {propertyTypeAlias ?? "null"}"); + + if (ct.Variations.VariesBySegment()) + return Content($"The document type {alias} already allows segments, nothing has been changed"); + + ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment); + + propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); + } + + public ActionResult DisableDocTypeSegments(string alias) + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + var ct = Services.ContentTypeService.Get(alias); + if (ct == null) + return Content($"No document type found by alias {alias}"); + + if (!ct.VariesBySegment()) + return Content($"The document type {alias} does not allow segments, nothing has been changed"); + + ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment); + + Services.ContentTypeService.Save(ct); + return Content($"The document type {alias} no longer allows segments"); + } + + public ActionResult AddSegmentData(int contentId, string propertyAlias, string value, string segment, string culture = null) + { + var content = Services.ContentService.GetById(contentId); + if (content == null) + return Content($"No content found by id {contentId}"); + + if (!content.HasProperty(propertyAlias)) + return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias}"); + + if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace()) + return Content($"The content by id {contentId} varies by culture but no culture was specified"); + + content.SetValue(propertyAlias, value, culture, segment); + Services.ContentService.Save(content); + + return Content($"Segment value has been set on content {contentId} for property {propertyAlias}"); + } + } +} diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj new file mode 100644 index 0000000000..d61321ebb8 --- /dev/null +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC} + Library + Properties + Umbraco.TestData + Umbraco.TestData + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + 28.4.4 + + + 5.2.7 + + + + \ No newline at end of file diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs new file mode 100644 index 0000000000..402c05cc1c --- /dev/null +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using System.Web.Mvc; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Mvc; +using System.Configuration; +using Bogus; +using Umbraco.Core.Scoping; + +namespace Umbraco.TestData +{ + /// + /// Creates test data + /// + public class UmbracoTestDataController : SurfaceController + { + private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE"; + private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; + private const string TextDataTypeName = "UmbracoTestDataContent.Text"; + private const string TestDataContentTypeAlias = "umbTestDataContent"; + private readonly IScopeProvider _scopeProvider; + private readonly PropertyEditorCollection _propertyEditors; + + public UmbracoTestDataController(IScopeProvider scopeProvider, PropertyEditorCollection propertyEditors, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, UmbracoHelper umbracoHelper) : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger, umbracoHelper) + { + _scopeProvider = scopeProvider; + _propertyEditors = propertyEditors; + } + + /// + /// Creates a content and associated media tree (hierarchy) + /// + /// + /// + /// + /// + /// + /// Each content item created is associated to a media item via a media picker and therefore a relation is created between the two + /// + public ActionResult CreateTree(int count, int depth, string locale = "en") + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + if (!Validate(count, depth, out var message, out var perLevel)) + throw new InvalidOperationException(message); + + var faker = new Faker(locale); + var company = faker.Company.CompanyName(); + + using (var scope = _scopeProvider.CreateScope()) + { + var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); + var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); + + Services.ContentService.SaveAndPublishBranch(root, true); + + scope.Complete(); + } + + + return Content("Done"); + } + + private bool Validate(int count, int depth, out string message, out int perLevel) + { + perLevel = 0; + message = null; + + if (count <= 0) + { + message = "Count must be more than 0"; + return false; + } + + perLevel = count / depth; + if (perLevel < 1) + { + message = "Count not high enough for specified for number of levels required"; + return false; + } + + return true; + } + + /// + /// Utility to create a tree hierarchy + /// + /// + /// + /// + /// + /// + /// A callback that returns a tuple of Content and another callback to produce a Container. + /// For media, a container will be another folder, for content the container will be the Content itself. + /// + /// + private IEnumerable CreateHierarchy( + T parent, int count, int depth, + Func container)> create) + where T: class, IContentBase + { + yield return parent.GetUdi(); + + // This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree. + var totalDescendants = count - 1; + var perLevel = Math.Ceiling(totalDescendants / (double)depth); + var perBranch = Math.Ceiling(perLevel / depth); + + var tracked = new Stack<(T parent, int childCount)>(); + + var currChildCount = 0; + + for (int i = 0; i < count; i++) + { + var created = create(parent); + var contentItem = created.content; + + yield return contentItem.GetUdi(); + + currChildCount++; + + if (currChildCount == perBranch) + { + // move back up... + + var prev = tracked.Pop(); + + // restore child count + currChildCount = prev.childCount; + // restore the parent + parent = prev.parent; + + } + else if (contentItem.Level < depth) + { + // track the current parent and it's current child count + tracked.Push((parent, currChildCount)); + + // not at max depth, create below + parent = created.container(); + + currChildCount = 0; + } + + } + } + + /// + /// Creates the media tree hiearachy + /// + /// + /// + /// + /// + /// + private IEnumerable CreateMediaTree(string company, Faker faker, int count, int depth) + { + var parent = Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder); + + return CreateHierarchy(parent, count, depth, currParent => + { + var imageUrl = faker.Image.PicsumUrl(); + + // we are appending a &ext=.jpg to the end of this for a reason. The result of this url will be something like: + // https://picsum.photos/640/480/?image=106 + // and due to the way that we detect images there must be an extension so we'll change it to + // https://picsum.photos/640/480/?image=106&ext=.jpg + // which will trick our app into parsing this and thinking it's an image ... which it is so that's good. + // if we don't do this we don't get thumbnails in the back office. + imageUrl += "&ext=.jpg"; + + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); + media.SetValue(Constants.Conventions.Media.File, imageUrl); + Services.MediaService.Save(media); + return (media, () => + { + // create a folder to contain child media + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder); + return container; + }); + }); + } + + /// + /// Creates the content tree hiearachy + /// + /// + /// + /// + /// + /// + /// + private IEnumerable CreateContentTree(string company, Faker faker, int count, int depth, List imageIds, out IContent root) + { + var random = new Random(company.GetHashCode()); + + var docType = GetOrCreateContentType(); + + var parent = Services.ContentService.Create(company, -1, docType.Alias); + parent.SetValue("review", faker.Rant.Review()); + parent.SetValue("desc", company); + parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + Services.ContentService.Save(parent); + + root = parent; + + return CreateHierarchy(parent, count, depth, currParent => + { + var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); + content.SetValue("review", faker.Rant.Review()); + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); ; + content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + + Services.ContentService.Save(content); + return (content, () => content); + }); + + } + + private IContentType GetOrCreateContentType() + { + var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias); + if (docType != null) + return docType; + + docType = new ContentType(-1) + { + Alias = TestDataContentTypeAlias, + Name = "Umbraco Test Data Content", + Icon = "icon-science color-green" + }; + docType.AddPropertyGroup("Content"); + docType.AddPropertyType(new PropertyType(GetOrCreateRichText(), "review") + { + Name = "Review" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateMediaPicker(), "media") + { + Name = "Media" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateText(), "desc") + { + Name = "Description" + }); + Services.ContentTypeService.Save(docType); + docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) }; + Services.ContentTypeService.Save(docType); + return docType; + } + + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + + private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); + + private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox); + + private IDataType GetOrCreateDataType(string name, string editorAlias) + { + var dt = Services.DataTypeService.GetDataType(name); + if (dt != null) return dt; + + var editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias); + if (editor == null) + throw new InvalidOperationException($"No {editorAlias} editor found"); + + dt = new DataType(editor) + { + Name = name, + Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject, + DatabaseType = ValueStorageType.Ntext + }; + + Services.DataTypeService.Save(dt); + return dt; + } + } +} diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md new file mode 100644 index 0000000000..f943326303 --- /dev/null +++ b/src/Umbraco.TestData/readme.md @@ -0,0 +1,51 @@ +## Umbraco Test Data + +This project is a utility to be able to generate large amounts of content and media in an +Umbraco installation for testing. + +Currently this project is referenced in the Umbraco.Web.UI project but only when it's being built +in Debug mode (i.e. when testing within Visual Studio). + +## Usage + +You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data. + +It has to be enabled by an appSetting: + +```xml + +``` + +Once this is enabled this endpoint can be executed: + +`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5` + +The query string options are: + +* `count` = the number of content and media nodes to create +* `depth` = how deep the trees created will be +* `locale` (optional, default = "en") = the language that the data will be generated in + +This creates a content and associated media tree (hierarchy). Each content item created is associated +to a media item via a media picker and therefore a relation is created between the two. Each content and +media tree created have the same root node name so it's easy to know which content branch relates to +which media branch. + +All values are generated using the very handy `Bogus` package. + +## Schema + +This will install some schema items: + +* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type +* `UmbracoTestDataContent.RTE` Data Type +* `UmbracoTestDataContent.MediaPicker` Data Type +* `UmbracoTestDataContent.Text` Data Type + +For media, the normal folder and image is used + +## Media + +This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. +This works when viewing the media item in the media section and the image will show up and with recent changes this will also work +when editing content to view the thumbnail for the picked media. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a2dfa0ffd2..96701c1c4e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -126,6 +126,10 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index ff42f098f7..0026f23514 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -21,6 +21,11 @@ + + + + + diff --git a/src/umbraco.sln b/src/umbraco.sln index ba9df633bb..a747f21d19 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -104,6 +104,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder.Embedded", "Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -140,6 +142,10 @@ Global {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -152,6 +158,7 @@ Global {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} + {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From be38b9639188689765632ea71bb9c83c3d22103c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 21 Jan 2020 13:52:53 +1100 Subject: [PATCH 222/610] fixes up the AddSegmentData endpoint --- src/Umbraco.TestData/SegmentTestController.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs index bcfdfc4ac4..33badbbb55 100644 --- a/src/Umbraco.TestData/SegmentTestController.cs +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -62,12 +62,18 @@ namespace Umbraco.TestData if (content == null) return Content($"No content found by id {contentId}"); - if (!content.HasProperty(propertyAlias)) - return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias}"); + if (propertyAlias.IsNullOrWhiteSpace() || !content.HasProperty(propertyAlias)) + return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias ?? "null"}"); if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace()) return Content($"The content by id {contentId} varies by culture but no culture was specified"); + if (value.IsNullOrWhiteSpace()) + return Content("'value' cannot be null"); + + if (segment.IsNullOrWhiteSpace()) + return Content("'segment' cannot be null"); + content.SetValue(propertyAlias, value, culture, segment); Services.ContentService.Save(content); From 466f8ca18584f1ed4c02eb4c93b0d1604927cbc8 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 21 Jan 2020 07:56:51 +0000 Subject: [PATCH 223/610] V8: Email Marketing Opt In (#7366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enable BackOfficeTours to have a bool to hide them in the help drawer * New hidden tour to display the email marketing option on login to backoffice * Update to tourService to use the new bool property of hidden to show/hide the tour in the help drawer * AngularJS Resource to call the Azure Function EmailService proxy code - currently set to DEV * New method on userService.addUserToEmailMarketing that in turn calls the new emailMarketingResource * New AngularJS view & controller in the tour step to deal with user clicking yes/accept to the email opt-in * Modifies the init script to auto launch the hidden email marketing tour at login If it has been accepted or dismissed before we then try to launch the original intro tour * Only show the email marketing tour when the intro tour has been dismissed or completed and will appear for one time only the next time you login * When using X to close email tour, it does not disable and never show it again but just closes it, similar to intro tour * Adds new localStorageService key for 'emailMarketingTourShown' to prevent the tour being shown again in the same logged in session, if you refresh the backoffice in your browser * Update URL to email function * Adding new COMA copy for email marketing tour - needs fine tuning pixel pushing from Niels L * Prettified layout of e-mail marketing promotion tour * fixing whitespace * text=auto * adding xml to gitattributes * Ensures the email tour is not shown if you dismiss the intro tour and manually refresh the page Co-authored-by: Niels Lyngsø --- .gitattributes | 16 ++++--- .../resources/emailmarketing.resource.js | 34 ++++++++++++++ .../src/common/services/tour.service.js | 12 +++-- .../src/common/services/user.service.js | 7 ++- src/Umbraco.Web.UI.Client/src/init.js | 30 +++++++++++-- src/Umbraco.Web.UI.Client/src/less/belle.less | 2 + .../less/components/application/umb-tour.less | 14 ++++++ .../less/components/umbemailmarketing.less | 44 +++++++++++++++++++ .../src/main.controller.js | 7 ++- .../emails/emails.controller.js | 24 ++++++++++ .../umbEmailMarketing/emails/emails.html | 26 +++++++++++ .../components/application/umb-tour.html | 2 +- .../Umbraco/js/main.controller.js | 7 ++- .../BackOfficeTours/getting-started.json | 18 ++++++++ src/Umbraco.Web/Models/BackOfficeTour.cs | 9 ++++ 15 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html diff --git a/.gitattributes b/.gitattributes index a664be3a85..c8987ade67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,7 +13,7 @@ *.png binary *.gif binary -*.cs text=auto diff=csharp +*.cs text=auto diff=csharp *.vb text=auto *.c text=auto *.cpp text=auto @@ -41,9 +41,13 @@ *.fs text=auto *.fsx text=auto *.hs text=auto +*.json text=auto +*.xml text=auto -*.csproj text=auto merge=union -*.vbproj text=auto merge=union -*.fsproj text=auto merge=union -*.dbproj text=auto merge=union -*.sln text=auto eol=crlf merge=union +*.csproj text=auto merge=union +*.vbproj text=auto merge=union +*.fsproj text=auto merge=union +*.dbproj text=auto merge=union +*.sln text=auto eol=crlf merge=union + +*.gitattributes text=auto diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js new file mode 100644 index 0000000000..4ac56ad13b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/emailmarketing.resource.js @@ -0,0 +1,34 @@ +/** + * @ngdoc service + * @name umbraco.resources.emailMarketingResource + * @description Used to add a backoffice user to Umbraco's email marketing system, if user opts in + * + * + **/ +function emailMarketingResource($http, umbRequestHelper) { + + // LOCAL + // http://localhost:7071/api/EmailProxy + + // LIVE + // https://emailcollector.umbraco.io/api/EmailProxy + + const emailApiUrl = 'https://emailcollector.umbraco.io/api/EmailProxy'; + + //the factory object returned + return { + + postAddUserToEmailMarketing: (user) => { + return umbRequestHelper.resourcePromise( + $http.post(emailApiUrl, + { + name: user.name, + email: user.email, + usergroup: user.userGroups // [ "admin", "sensitiveData" ] + }), + 'Failed to add user to email marketing list'); + } + }; +} + +angular.module('umbraco.resources').factory('emailMarketingResource', emailMarketingResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index e102da5d34..62af17146c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -147,7 +147,10 @@ group.groupOrder = item.groupOrder } groupExists = true; - group.tours.push(item) + + if(item.hidden === false){ + group.tours.push(item); + } } }); @@ -157,8 +160,11 @@ if(item.groupOrder) { newGroup.groupOrder = item.groupOrder } - newGroup.tours.push(item); - groupedTours.push(newGroup); + + if(item.hidden === false){ + newGroup.tours.push(item); + groupedTours.push(newGroup); + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 7723c8f4bb..afd7b606e7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -1,5 +1,5 @@ angular.module('umbraco.services') - .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, $timeout, angularHelper) { + .factory('userService', function ($rootScope, eventsService, $q, $location, requestRetryQueue, authResource, emailMarketingResource, $timeout, angularHelper) { var currentUser = null; var lastUserId = null; @@ -262,6 +262,11 @@ angular.module('umbraco.services') /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */ setUserTimeout: function (newTimeout) { setUserTimeoutInternal(newTimeout); + }, + + /** Calls out to a Remote Azure Function to deal with email marketing service */ + addUserToEmailMarketing: (user) => { + return emailMarketingResource.postAddUserToEmailMarketing(user); } }; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index 7d199c5c4f..d5c5166d21 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,6 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', - function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService) { +app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', 'localStorageService', + function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService, localStorageService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -23,11 +23,35 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', appReady(data); tourService.registerAllTours().then(function () { - // Auto start intro tour + + // Start intro tour tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) { // start intro tour if it hasn't been completed or disabled if (introTour && introTour.disabled !== true && introTour.completed !== true) { tourService.startTour(introTour); + localStorageService.set("introTourShown", true); + } + else { + + const introTourShown = localStorageService.get("introTourShown"); + if(!introTourShown){ + // Go & show email marketing tour (ONLY when intro tour is completed or been dismissed) + tourService.getTourByAlias("umbEmailMarketing").then(function (emailMarketingTour) { + // Only show the email marketing tour one time - dismissing it or saying no will make sure it never appears again + // Unless invoked from tourService JS Client code explicitly. + // Accepted mails = Completed and Declicned mails = Disabled + if (emailMarketingTour && emailMarketingTour.disabled !== true && emailMarketingTour.completed !== true) { + + // Only show the email tour once per logged in session + // The localstorage key is removed on logout or user session timeout + const emailMarketingTourShown = localStorageService.get("emailMarketingTourShown"); + if(!emailMarketingTourShown){ + tourService.startTour(emailMarketingTour); + localStorageService.set("emailMarketingTourShown", true); + } + } + }); + } } }); }); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index b5e032f9fb..0921f46aac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -194,6 +194,8 @@ @import "components/contextdialogs/umb-dialog-datatype-delete.less"; +@import "components/umbemailmarketing.less"; + // Utilities @import "utilities/layout/_display.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 42403c65b1..bf2f030cea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -119,3 +119,17 @@ border: none; padding: 0; } + +.umb-tour__popover--promotion { + width: 800px; + min-height: 400px; + padding: 40px; + border-radius: @baseBorderRadius * 2; + .umb-tour-step__close { + top: 40px; + right: 40px; + } + a { + text-decoration: underline; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less new file mode 100644 index 0000000000..f4b3183045 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umbemailmarketing.less @@ -0,0 +1,44 @@ +.umb-email-marketing { + + h2 { + font-weight: 800; + max-width: 26ex; + margin-top: 20px; + } + + .layout { + display: flex; + align-items: center; + align-content: stretch; + + .primary { + flex-basis: 50%; + padding-right: 40px; + padding-top: 20px; + padding-bottom: 20px; + .notice { + color: @gray-5; + font-style: italic; + a { + color: @gray-5; + &:hover { + color: @ui-action-type-hover; + } + } + } + } + + .secondary { + flex-basis: 50%; + svg { + height: 200px; + width: 100%; + margin-top: -60px; + } + } + } + + .cta { + text-align: right; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 93870f8a56..883907d1dc 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -67,13 +67,18 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; $scope.showLoginScreen(isTimedOut); + + // Remove the localstorage items for tours shown + // Means that when next logged in they can be re-shown if not already dismissed etc + localStorageService.remove("emailMarketingTourShown"); + localStorageService.remove("introTourShown"); })); evts.push(eventsService.on("app.userRefresh", function(evt) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js new file mode 100644 index 0000000000..8ecc737278 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function EmailsController($scope, userService) { + + var vm = this; + + vm.optIn = function() { + // Get the current user in backoffice + userService.getCurrentUser().then(function(user){ + // Send this user along to opt in + // It's a fire & forget - not sure we need to check the response + userService.addUserToEmailMarketing(user); + }); + + // Mark Tour as complete + // This is also can help us indicate that the user accepted + // Where disabled is set if user closes modal or chooses NO + $scope.model.completeTour(); + } + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbEmailMarketing.EmailsController", EmailsController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html new file mode 100644 index 0000000000..887624ed05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbEmailMarketing/emails/emails.html @@ -0,0 +1,26 @@ +
+ + + +

{{ model.currentStep.title }}

+ +
+ +
+
+
+ + +
+ paperplane +
+
+ +
+ + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index 5dd56941a9..e358d75b9e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -4,7 +4,7 @@
-
+
diff --git a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js index 93870f8a56..883907d1dc 100644 --- a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js +++ b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js @@ -67,13 +67,18 @@ function MainController($scope, $location, appState, treeService, notificationsS }; var evts = []; - + //when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function (evt, data) { $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; $scope.showLoginScreen(isTimedOut); + + // Remove the localstorage items for tours shown + // Means that when next logged in they can be re-shown if not already dismissed etc + localStorageService.remove("emailMarketingTourShown"); + localStorageService.remove("introTourShown"); })); evts.push(eventsService.on("app.userRefresh", function(evt) { diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index e300e6562e..7b3f2a2184 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -1,4 +1,22 @@ [ + { + "name": "Email Marketing", + "alias": "umbEmailMarketing", + "group": "Email Marketing", + "groupOrder": 10, + "hidden": true, + "requiredSections": [ + "content" + ], + "steps": [ + { + "title": "Do you want to stay updated on everything Umbraco?", + "content": "

Thank you for using Umbraco! Would you like to stay up-to-date with Umbraco product updates, security advisories, community news and special offers? Sign up for our newsletter and never miss out on the latest Umbraco news.

By signing up, you agree that we can use your info according to our privacy policy.

", + "view": "emails", + "type": "promotion" + } + ] + }, { "name": "Introduction", "alias": "umbIntroIntroduction", diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index d5987ec5bc..7391765193 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -16,16 +16,25 @@ namespace Umbraco.Web.Models [DataMember(Name = "name")] public string Name { get; set; } + [DataMember(Name = "alias")] public string Alias { get; set; } + [DataMember(Name = "group")] public string Group { get; set; } + [DataMember(Name = "groupOrder")] public int GroupOrder { get; set; } + + [DataMember(Name = "hidden")] + public bool Hidden { get; set; } + [DataMember(Name = "allowDisable")] public bool AllowDisable { get; set; } + [DataMember(Name = "requiredSections")] public List RequiredSections { get; set; } + [DataMember(Name = "steps")] public BackOfficeTourStep[] Steps { get; set; } From 0d12852cd8520dd21c4e33136a93d4747506d816 Mon Sep 17 00:00:00 2001 From: abi Date: Tue, 21 Jan 2020 09:07:10 +0000 Subject: [PATCH 224/610] Address Shannon comments Add code documentation, return empty enumerable instead of allocate empty list --- .../Search/IUmbracoTreeSearcherFields.cs | 16 +++++++++++++++- .../Search/UmbracoTreeSearcherFields.cs | 7 +++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs index eaa5d743a1..c5a6c53d19 100644 --- a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -2,12 +2,26 @@ using System.Collections.Generic; namespace Umbraco.Web.Search { + /// + /// Used to propagate hardcoded internal Field lists + /// public interface IUmbracoTreeSearcherFields { + /// + /// Propagate list of searchable fields for all node types + /// IEnumerable GetBackOfficeFields(); + /// + /// Propagate list of searchable fields for Members + /// IEnumerable GetBackOfficeMembersFields(); - + /// + /// Propagate list of searchable fields for Media + /// IEnumerable GetBackOfficeMediaFields(); + /// + /// Propagate list of searchable fields for Documents + /// IEnumerable GetBackOfficeDocumentFields(); } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index d1c663024b..f90d7bc6b6 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; +using System.Linq; using Umbraco.Examine; -using Umbraco.Web.Search; -namespace Umbraco.Web +namespace Umbraco.Web.Search { public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields { @@ -23,10 +23,9 @@ namespace Umbraco.Web { return _backOfficeMediaFields; } - private IReadOnlyList _backOfficeDocumentFields = new List (); public IEnumerable GetBackOfficeDocumentFields() { - return _backOfficeDocumentFields; + return Enumerable.Empty(); } } } From 204e0841afde1cdca8d9d8b4f79b3bb127fad6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Dessureault?= Date: Tue, 21 Jan 2020 10:48:21 -0500 Subject: [PATCH 225/610] Fix typo in contentTypeSavedHeader French label "documet" should read "document" --- src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml index 18a5fe9c5a..e3cfa32d62 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml @@ -1180,7 +1180,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Type de propriété créé Type de données : %1%]]> Type de propriété supprimé - Type de documet sauvegardé + Type de document sauvegardé Onglet créé Onglet supprimé Onglet avec l'ID : %0% supprimé From 163c1f7028eb419eda33e8a0c0548f66fd1ee8e2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 21 Jan 2020 19:16:07 +0100 Subject: [PATCH 226/610] Fix automatic merge gone wrong --- src/Umbraco.Web.UI.Client/src/common/services/tour.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 91b41cc68d..1c2da43814 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -168,12 +168,12 @@ } } - }); + } deferred.resolve(groupedTours); }); return deferred.promise; - } + }); /** * @ngdoc method From 80309011847af5664d3765d824dd2ec3e93985c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Jan 2020 08:36:52 +0100 Subject: [PATCH 227/610] Fix mistakes made in commit a4a6b77 --- .../src/common/services/tour.service.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 1c2da43814..8fcab445b3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -162,18 +162,19 @@ newGroup.groupOrder = item.groupOrder; } - if(item.hidden === false){ - newGroup.tours.push(item); - groupedTours.push(newGroup); + if(item.hidden === false){ + newGroup.tours.push(item); + groupedTours.push(newGroup); + } } } - } + }); deferred.resolve(groupedTours); }); return deferred.promise; - }); + } /** * @ngdoc method From 686e1beb74d0d0b8c9059deed6feb7c33ba9d922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Jan 2020 10:40:56 +0100 Subject: [PATCH 228/610] Apply segments for validation, client side. --- .../content/umbcontentnodeinfo.directive.js | 2 +- .../umbeditorcontentheader.directive.js | 6 +- .../validation/valpropertymsg.directive.js | 8 +- .../validation/valserver.directive.js | 6 +- .../validation/valserverfield.directive.js | 1 + .../src/common/services/formhelper.service.js | 20 ++- .../services/servervalidationmgr.service.js | 129 ++++++++++++------ .../server-validation-manager.spec.js | 98 ++++++------- 8 files changed, 164 insertions(+), 106 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 78a2111fc5..9b2dea8ae0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -322,7 +322,7 @@ // find the urls for the currently selected language if (scope.node.variants.length > 1) { // nodes with variants - scope.currentUrls = _.filter(scope.node.urls, (url) => scope.currentVariant.language.culture === url.culture); + scope.currentUrls = _.filter(scope.node.urls, (url) => !scope.currentVariant.language || scope.currentVariant.language.culture === url.culture); } else { // invariant nodes scope.currentUrls = scope.node.urls; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index 0593595906..86a5fdbd79 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -67,7 +67,7 @@ // find default. angular.forEach(scope.content.variants, function (variant) { - if (variant.language.isDefault) { + if (variant.language !== null && variant.language.isDefault) { scope.vm.defaultVariant = variant; } }); @@ -82,10 +82,10 @@ angular.forEach(scope.content.variants, function (variant) { - unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation)); + unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, variant.segment, null, onCultureValidation)); }); - unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation)); + unsubscribe.push(serverValidationManager.subscribe(null, null, null, null, onCultureValidation)); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 0369b4bd2e..3a53f9e9ce 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -38,7 +38,8 @@ function valPropertyMsg(serverValidationManager, localizationService) { var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; - var currentCulture = currentProperty.culture; + var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; var labels = {}; localizationService.localize("errors_propertyHasErrors").then(function (data) { @@ -51,7 +52,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { var currentVariant = umbVariantCtrl.editor.content; // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -67,7 +68,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { //this can be null if no property was assigned if (scope.currentProperty) { //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, ""); + var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, null, ""); //if there's an error message use it if (err && err.errorMsg) { return err.errorMsg; @@ -203,6 +204,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { if (scope.currentProperty) { //this can be null if no property was assigned unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias, currentCulture, + currentSegment, "", function(isValid, propertyErrors, allErrors) { hasError = !isValid; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index a0cc7e3033..a214f72d8f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -24,6 +24,7 @@ function valServer(serverValidationManager) { var currentProperty = umbPropCtrl.property; var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive @@ -31,7 +32,7 @@ function valServer(serverValidationManager) { var currentVariant = umbVariantCtrl.editor.content; // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -75,7 +76,7 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); //clear the server validation entry - serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName); + serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, currentSegment, fieldName); stopWatch(); } }, true); @@ -92,6 +93,7 @@ function valServer(serverValidationManager) { //subscribe to the server validation changes unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias, currentCulture, + currentSegment, fieldName, function(isValid, propertyErrors, allErrors) { if (!isValid) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js index 7f5427da8b..16b12c1a94 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js @@ -35,6 +35,7 @@ function valServerField(serverValidationManager) { //subscribe to the server validation changes unsubscribe.push(serverValidationManager.subscribe(null, + null, null, fieldName, function(isValid, fieldErrors, allErrors) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 0555318bae..45db0c7abc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -179,16 +179,24 @@ function formHelper(angularHelper, serverValidationManager, notificationsService } } - //if it contains 3 '.' then we will wire it up to a property's html field + htmlFieldReference = ""; if (parts.length > 3) { - //add an error with a reference to the field for which the validation belongs too - serverValidationManager.addPropertyError(propertyAlias, culture, parts[3], modelState[e][0]); + htmlFieldReference = parts[3] || ""; } - else { - //add a generic error for the property, no reference to a specific html field - serverValidationManager.addPropertyError(propertyAlias, culture, "", modelState[e][0]); + + // SEGMENTS_TODO: Need to investigate wether we have updated validation to handle segments, plus could it be the third parameter, so we leave the HTML Field ref as optional and last? + var segment = null; + if (parts.length > 4) { + segment = parts[4]; + //special check in case the string is formatted this way + if (segment === "null") { + segment = null; + } } + // add a generic error for the property + serverValidationManager.addPropertyError(propertyAlias, culture, segment, htmlFieldReference, modelState[e][0]); + } else { //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index b9bfa51122..00c1d0d690 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -13,13 +13,14 @@ function serverValidationManager($timeout) { var callbacks = []; /** calls the callback specified with the errors specified, used internally */ - function executeCallback(self, errorsForCallback, callback, culture) { + function executeCallback(self, errorsForCallback, callback, culture, segment) { callback.apply(self, [ false, // pass in a value indicating it is invalid errorsForCallback, // pass in the errors for this item self.items, // pass in all errors in total - culture // pass the culture that we are listing for. + culture, // pass the culture that we are listing for. + segment // pass the segment that we are listing for. ] ); } @@ -35,7 +36,7 @@ function serverValidationManager($timeout) { }); } - function getPropertyErrors(self, propertyAlias, culture, fieldName) { + function getPropertyErrors(self, propertyAlias, culture, segment, fieldName) { if (!angular.isString(propertyAlias)) { throw "propertyAlias must be a string"; } @@ -46,22 +47,28 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + if (!segment) { + segment = null; + } //find all errors for this property return _.filter(self.items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } - function getCultureErrors(self, culture) { + function getVariantErrors(self, culture, segment) { if (!culture) { culture = "invariant"; } + if (!segment) { + segment = null; + } //find all errors for this property return _.filter(self.items, function (item) { - return (item.culture === culture); + return (item.culture === culture && item.segment === segment); }); } @@ -71,21 +78,21 @@ function serverValidationManager($timeout) { //its a field error callback var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); if (fieldErrors.length > 0) { - executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture); + executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } else if (callbacks[cb].propertyAlias != null) { //its a property error - var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].fieldName); + var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].segment, callbacks[cb].fieldName); if (propErrors.length > 0) { - executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture); + executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } else { - //its a culture error - var cultureErrors = getCultureErrors(self, callbacks[cb].culture); - if (cultureErrors.length > 0) { - executeCallback(self, cultureErrors, callbacks[cb].callback, callbacks[cb].culture); + //its a variant error + var variantErrors = getVariantErrors(self, callbacks[cb].culture, callbacks[cb].segment); + if (variantErrors.length > 0) { + executeCallback(self, variantErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } } @@ -150,20 +157,27 @@ function serverValidationManager($timeout) { * field alias to listen for. * If propertyAlias is null, then this subscription is for a field property (not a user defined property). */ - subscribe: function (propertyAlias, culture, fieldName, callback) { + subscribe: function (propertyAlias, culture, segment, fieldName, callback) { if (!callback) { return; } var id = String.CreateGuid(); + + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } if (propertyAlias === null) { callbacks.push({ propertyAlias: null, culture: culture, + segment: segment, fieldName: fieldName, callback: callback, id: id @@ -175,6 +189,7 @@ function serverValidationManager($timeout) { callbacks.push({ propertyAlias: propertyAlias, culture: culture, + segment: segment, fieldName: fieldName, callback: callback, id: id @@ -199,25 +214,29 @@ function serverValidationManager($timeout) { * @param {} fieldName * @returns {} */ - unsubscribe: function (propertyAlias, culture, fieldName) { + unsubscribe: function (propertyAlias, culture, segment, fieldName) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } if (propertyAlias === null) { //remove all callbacks for the content field callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === null && item.culture === culture && item.fieldName === fieldName; + return item.propertyAlias === null && item.culture === culture && item.segment === segment && item.fieldName === fieldName; }); } else if (propertyAlias !== undefined) { //remove all callbacks for the content property callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === propertyAlias && item.culture === culture && + return item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || ((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === ""))); }); @@ -238,14 +257,18 @@ function serverValidationManager($timeout) { */ getPropertyCallbacks: function (propertyAlias, culture, fieldName) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly against the field and for only the property - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); }); return found; }, @@ -262,7 +285,7 @@ function serverValidationManager($timeout) { getFieldCallbacks: function (fieldName) { var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly against the field - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return found; }, @@ -303,6 +326,7 @@ function serverValidationManager($timeout) { this.items.push({ propertyAlias: null, culture: "invariant", + segment: null, fieldName: fieldName, errorMsg: errorMsg }); @@ -327,7 +351,7 @@ function serverValidationManager($timeout) { * @description * Adds an error message for the content property */ - addPropertyError: function (propertyAlias, culture, fieldName, errorMsg) { + addPropertyError: function (propertyAlias, culture, segment, fieldName, errorMsg) { if (!propertyAlias) { return; } @@ -336,31 +360,36 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } //only add the item if it doesn't exist - if (!this.hasPropertyError(propertyAlias, culture, fieldName)) { + if (!this.hasPropertyError(propertyAlias, culture, segment, fieldName)) { this.items.push({ propertyAlias: propertyAlias, culture: culture, + segment: segment, fieldName: fieldName, errorMsg: errorMsg }); } //find all errors for this item - var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, fieldName); + var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, segment, fieldName); //we should now call all of the call backs registered for this error - var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName); + var cbs = this.getPropertyCallbacks(propertyAlias, culture, segment, fieldName); //call each callback for this error for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, culture); + executeCallback(this, errorsForCallback, cbs[cb].callback, culture, segment); } - //execute culture specific callbacks here too when a propery error is added - var cultureCbs = this.getCultureCallbacks(culture); + //execute variant specific callbacks here too when a propery error is added + var variantCbs = this.getVariantCallbacks(culture, segment); //call each callback for this error - for (var cb in cultureCbs) { - executeCallback(this, errorsForCallback, cultureCbs[cb].callback, culture); + for (var cb in variantCbs) { + executeCallback(this, errorsForCallback, variantCbs[cb].callback, culture, segment); } }, @@ -373,7 +402,7 @@ function serverValidationManager($timeout) { * @description * Removes an error message for the content property */ - removePropertyError: function (propertyAlias, culture, fieldName) { + removePropertyError: function (propertyAlias, culture, segment, fieldName) { if (!propertyAlias) { return; @@ -383,10 +412,14 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } //remove the item this.items = _.reject(this.items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); }, @@ -431,16 +464,20 @@ function serverValidationManager($timeout) { * @description * Gets the error message for the content property */ - getPropertyError: function (propertyAlias, culture, fieldName) { + getPropertyError: function (propertyAlias, culture, segment, fieldName) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); return err; }, @@ -457,7 +494,7 @@ function serverValidationManager($timeout) { getFieldError: function (fieldName) { var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return err; }, @@ -471,16 +508,20 @@ function serverValidationManager($timeout) { * @description * Checks if the content property + culture + field name combo has an error */ - hasPropertyError: function (propertyAlias, culture, fieldName) { + hasPropertyError: function (propertyAlias, culture, segment, fieldName) { //normalize culture to null if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); return err ? true : false; }, @@ -497,7 +538,7 @@ function serverValidationManager($timeout) { hasFieldError: function (fieldName) { var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return err ? true : false; }, @@ -505,22 +546,26 @@ function serverValidationManager($timeout) { /** * @ngdoc function - * @name hasCultureError + * @name hasVariantError * @methodOf umbraco.services.serverValidationManager * @function * * @description * Checks if the given culture has an error */ - hasCultureError: function (culture) { + hasVariantError: function (culture, segment) { //normalize culture to null if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { - return item.culture === culture; + return (item.culture === culture && item.segment === segment); }); return err ? true : false; }, diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 966731f0f7..8d6efde7b0 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -61,12 +61,12 @@ it('can retrieve property validation errors for a sub field', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, null, "value2", "Another value 2"); //act - var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1"); - var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2"); + var err1 = serverValidationManager.getPropertyError("myProperty", null, null, "value1"); + var err2 = serverValidationManager.getPropertyError("myProperty", null, null, "value2"); //assert expect(err1).not.toBeUndefined(); @@ -85,14 +85,14 @@ it('can retrieve property validation errors for a sub field for culture', function () { //arrange - serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", "en-US", null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", null, "value2", "Another value 2"); //act - var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "value1"); - var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2"); - var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2"); - var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2"); + var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", null, "value1"); + var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, null, "value2"); + var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", null, "value2"); + var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, null, "value2"); //assert @@ -115,8 +115,8 @@ it('can add a property errors with multiple sub fields and it the first will be retreived with only the property alias', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, null, "value2", "Another value 2"); //act var err = serverValidationManager.getPropertyError("myProperty"); @@ -132,10 +132,10 @@ it('will return null for a non-existing property error', function () { //arrage - serverValidationManager.addPropertyError("myProperty", null, "value", "Required"); + serverValidationManager.addPropertyError("myProperty", null, null, "value", "Required"); //act - var err = serverValidationManager.getPropertyError("DoesntExist", null, "value"); + var err = serverValidationManager.getPropertyError("DoesntExist", null, null, "value"); //assert expect(err).toBeUndefined(); @@ -145,15 +145,15 @@ it('detects if a property error exists', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, null, "value2", "Another value 2"); //act var err1 = serverValidationManager.hasPropertyError("myProperty"); - var err2 = serverValidationManager.hasPropertyError("myProperty", null, "value1"); - var err3 = serverValidationManager.hasPropertyError("myProperty", null, "value2"); + var err2 = serverValidationManager.hasPropertyError("myProperty", null, null, "value1"); + var err3 = serverValidationManager.hasPropertyError("myProperty", null, null, "value2"); var err4 = serverValidationManager.hasPropertyError("notFound"); - var err5 = serverValidationManager.hasPropertyError("myProperty", null, "notFound"); + var err5 = serverValidationManager.hasPropertyError("myProperty", null, null, "notFound"); //assert expect(err1).toBe(true); @@ -167,15 +167,15 @@ it('can remove a property error with a sub field specified', function () { //arrage - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, null, "value2", "Another value 2"); //act - serverValidationManager.removePropertyError("myProperty", null, "value1"); + serverValidationManager.removePropertyError("myProperty", null, null, "value1"); //assert - expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false); - expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(true); + expect(serverValidationManager.hasPropertyError("myProperty", null, null, "value1")).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, null, "value2")).toBe(true); }); @@ -189,8 +189,8 @@ serverValidationManager.removePropertyError("myProperty"); //assert - expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false); - expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, null, "value1")).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, null, "value2")).toBe(false); }); @@ -201,16 +201,16 @@ it('can retrieve culture validation errors', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); - serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", "en-US", null, "value1", "Some value 2"); + serverValidationManager.addPropertyError("myProperty", null, null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", null, "value2", "Another value 3"); //assert - expect(serverValidationManager.hasCultureError(null)).toBe(true); - expect(serverValidationManager.hasCultureError("en-US")).toBe(true); - expect(serverValidationManager.hasCultureError("fr-FR")).toBe(true); - expect(serverValidationManager.hasCultureError("es-ES")).toBe(false); + expect(serverValidationManager.hasVariantError(null, null)).toBe(true); + expect(serverValidationManager.hasVariantError("en-US", null)).toBe(true); + expect(serverValidationManager.hasVariantError("fr-FR", null)).toBe(true); + expect(serverValidationManager.hasVariantError("es-ES", null)).toBe(false); }); @@ -222,7 +222,7 @@ var args; //arrange - serverValidationManager.subscribe(null, null, "Name", function (isValid, propertyErrors, allErrors) { + serverValidationManager.subscribe(null, null, null, "Name", function (isValid, propertyErrors, allErrors) { args = { isValid: isValid, propertyErrors: propertyErrors, @@ -232,7 +232,7 @@ //act serverValidationManager.addFieldError("Name", "Required"); - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); //assert expect(args).not.toBeUndefined(); @@ -249,8 +249,8 @@ }; var cb2 = function () { }; - serverValidationManager.subscribe(null, null, "Name", cb1); - serverValidationManager.subscribe(null, null, "Title", cb2); + serverValidationManager.subscribe(null, null, null, "Name", cb1); + serverValidationManager.subscribe(null, null, null, "Title", cb2); //act serverValidationManager.addFieldError("Name", "Required"); @@ -278,7 +278,7 @@ var numCalled = 0; //arrange - serverValidationManager.subscribe("myProperty", null, "value1", function (isValid, propertyErrors, allErrors) { + serverValidationManager.subscribe("myProperty", null, null, "value1", function (isValid, propertyErrors, allErrors) { args1 = { isValid: isValid, propertyErrors: propertyErrors, @@ -286,7 +286,7 @@ }; }); - serverValidationManager.subscribe("myProperty", null, "", function (isValid, propertyErrors, allErrors) { + serverValidationManager.subscribe("myProperty", null, null, "", function (isValid, propertyErrors, allErrors) { numCalled++; args2 = { isValid: isValid, @@ -296,9 +296,9 @@ }); //act - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Some value 2"); - serverValidationManager.addPropertyError("myProperty", null, "", "Some value 3"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, null, "value2", "Some value 2"); + serverValidationManager.addPropertyError("myProperty", null, null, "", "Some value 3"); //assert expect(args1).not.toBeUndefined(); @@ -328,7 +328,7 @@ var numCalled = 0; //arrange - serverValidationManager.subscribe(null, "en-US", null, function (isValid, propertyErrors, allErrors) { + serverValidationManager.subscribe(null, "en-US", null, null, function (isValid, propertyErrors, allErrors) { numCalled++; args1 = { isValid: isValid, @@ -337,7 +337,7 @@ }; }); - serverValidationManager.subscribe(null, "es-ES", null, function (isValid, propertyErrors, allErrors) { + serverValidationManager.subscribe(null, "es-ES", null, null, function (isValid, propertyErrors, allErrors) { numCalled++; args2 = { isValid: isValid, @@ -347,10 +347,10 @@ }); //act - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "en-US", "value2", "Some value 2"); - serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3"); + serverValidationManager.addPropertyError("myProperty", null, null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", "en-US", null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", "en-US", null, "value2", "Some value 2"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", null, "", "Some value 3"); //assert expect(args1).not.toBeUndefined(); From 10f15a7648c939c0469892bf04f9ae6e5930ef6f Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:10:28 +0100 Subject: [PATCH 229/610] Remove duplicate VariesBySegment overload after merge --- src/Umbraco.Core/ContentVariationExtensions.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 8939f7133b..6430c75686 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -27,11 +27,6 @@ namespace Umbraco.Core /// public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); - /// - /// Determines whether the content type varies by segment. - /// - public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); - /// /// Determines whether the content type is invariant. /// From 6c2e1b29fd7221a259be97e39eb0ee3ae0f657cd Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:10:56 +0100 Subject: [PATCH 230/610] Use SetVariesBy extension methods instead of SetFlag/UnsetFlag --- src/Umbraco.TestData/SegmentTestController.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs index 33badbbb55..650820760e 100644 --- a/src/Umbraco.TestData/SegmentTestController.cs +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -30,9 +30,8 @@ namespace Umbraco.TestData if (ct.Variations.VariesBySegment()) return Content($"The document type {alias} already allows segments, nothing has been changed"); - ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment); - - propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment); + ct.SetVariesBy(ContentVariation.Segment); + propType.SetVariesBy(ContentVariation.Segment); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); @@ -50,7 +49,7 @@ namespace Umbraco.TestData if (!ct.VariesBySegment()) return Content($"The document type {alias} does not allow segments, nothing has been changed"); - ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment); + ct.SetVariesBy(ContentVariation.Segment, false); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} no longer allows segments"); From ac675d14e952788881e4980e0f20c57aae967d9c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 22 Jan 2020 11:13:49 +0100 Subject: [PATCH 231/610] Fixes build errors --- src/Umbraco.Core/ContentVariationExtensions.cs | 13 ++++--------- src/Umbraco.TestData/SegmentTestController.cs | 8 ++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 8939f7133b..4fc6f418aa 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -30,6 +30,10 @@ namespace Umbraco.Core /// /// Determines whether the content type varies by segment. /// + /// The content type. + /// + /// A value indicating whether the content type varies by segment. + /// public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); /// @@ -122,15 +126,6 @@ namespace Umbraco.Core /// public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; - /// - /// Determines whether the content type varies by segment. - /// - /// The content type. - /// - /// A value indicating whether the content type varies by segment. - /// - public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment(); - /// /// Determines whether the content type varies by segment. /// diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs index 33badbbb55..c78d12ab0e 100644 --- a/src/Umbraco.TestData/SegmentTestController.cs +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -30,9 +30,9 @@ namespace Umbraco.TestData if (ct.Variations.VariesBySegment()) return Content($"The document type {alias} already allows segments, nothing has been changed"); - ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment); - - propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment); + ct.Variations = ct.Variations.SetVariesBy(ContentVariation.Segment); + + propType.Variations = propType.Variations.SetVariesBy(ContentVariation.Segment); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); @@ -50,7 +50,7 @@ namespace Umbraco.TestData if (!ct.VariesBySegment()) return Content($"The document type {alias} does not allow segments, nothing has been changed"); - ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment); + ct.Variations = ct.Variations.SetVariesBy(ContentVariation.Nothing); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} no longer allows segments"); From 1cc6a8cb4511c74ad5d5a96fed9ca1805bf7831a Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:14:31 +0100 Subject: [PATCH 232/610] Rename SetVariesBy to SetFlag on the enum overload --- src/Umbraco.Core/ContentVariationExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 6430c75686..29442b78e6 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -234,7 +234,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(variation, value); + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetFlag(variation, value); /// /// Sets or removes the property type variation depending on the specified value. @@ -245,7 +245,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); + public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetFlag(variation, value); /// /// Returns the variations with the variation set or removed depending on the specified value. @@ -259,7 +259,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) + public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true) { return value ? variations | variation // Set flag using bitwise logical OR From 5acca8c4165eb5161adba0a7ef778bf8cfeb8182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Jan 2020 11:40:53 +0100 Subject: [PATCH 233/610] adding missing segment argument --- .../src/common/services/servervalidationmgr.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 00c1d0d690..10ab2b79cc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -255,7 +255,7 @@ function serverValidationManager($timeout) { * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an * explicit field name set. */ - getPropertyCallbacks: function (propertyAlias, culture, fieldName) { + getPropertyCallbacks: function (propertyAlias, culture, segment, fieldName) { //normalize culture to "invariant" if (!culture) { From 4a028ed22cf992fa677c1f94911a1c29c7c65e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Jan 2020 11:47:48 +0100 Subject: [PATCH 234/610] renaming getCultureCallbacks to getVariantCallbacks, for segment support. --- .../src/common/services/servervalidationmgr.service.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 10ab2b79cc..7c36b5e4eb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -292,17 +292,17 @@ function serverValidationManager($timeout) { /** * @ngdoc function - * @name getCultureCallbacks + * @name getVariantCallbacks * @methodOf umbraco.services.serverValidationManager * @function * * @description - * Gets all callbacks that has been registered using the subscribe method for the culture. + * Gets all callbacks that has been registered using the subscribe method for the culture and segment. */ - getCultureCallbacks: function (culture) { + getVariantCallbacks: function (culture, segment) { var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly/ONLY against the culture - return (item.culture === culture && item.propertyAlias === null && item.fieldName === null); + return (item.culture === culture && item.segment === segment && item.propertyAlias === null && item.fieldName === null); }); return found; }, From 1047929880c72ce5382d0fb127cb82ef933245fc Mon Sep 17 00:00:00 2001 From: LiamL Date: Wed, 22 Jan 2020 13:22:09 +0000 Subject: [PATCH 235/610] simplifies tablename for dictionary dto --- .../V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs index 6c574bd3c9..e46f78dd72 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs @@ -15,16 +15,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 /// if it doesn't already exist /// public override void Migrate() - { - var tableInfo = Context.Database.PocoDataFactory.ForType(typeof(DictionaryDto)).TableInfo; - tableInfo.TableName = tableInfo.TableName; - var indexName = "IX_" + tableInfo.TableName + "_Parent"; + { + var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; if (IndexExists(indexName) == false) { Create .Index(indexName) - .OnTable(tableInfo.TableName) + .OnTable(DictionaryDto.TableName) .OnColumn("parent") .Ascending() .WithOptions().NonClustered() From d61090a225c9e99606f096c18971e5beb5c6c9a7 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Tue, 14 Jan 2020 14:02:10 +0100 Subject: [PATCH 236/610] #7452 set save state correctly for variant when setting schedule publishing --- .../src/common/directives/components/content/edit.controller.js | 1 + 1 file changed, 1 insertion(+) 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 cab71842b1..7429f60e0d 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 @@ -791,6 +791,7 @@ $scope.content.variants[i].expireDate = model.variants[i].expireDate; $scope.content.variants[i].releaseDateFormatted = model.variants[i].releaseDateFormatted; $scope.content.variants[i].expireDateFormatted = model.variants[i].expireDateFormatted; + $scope.content.variants[i].save = model.variants[i].save; } model.submitButtonState = "busy"; From 201927580c299b223c5eb17f798e1aaa263944aa Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 16:23:27 +1100 Subject: [PATCH 237/610] Fixing accidental api signature breaking change --- .../Persistence/SqlSyntax/SqlServerSyntaxProvider.cs | 6 +++--- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 6dda49cd5e..bb50fa98a1 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -251,10 +251,10 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) public override void WriteLock(IDatabase db, params int[] lockIds) { - WriteLock(db, 1800, lockIds); + WriteLock(db, TimeSpan.FromMilliseconds(1800), lockIds); } - public void WriteLock(IDatabase db, int millisecondsTimeout, params int[] lockIds) + public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -265,7 +265,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks foreach (var lockId in lockIds) { - db.Execute($"SET LOCK_TIMEOUT {millisecondsTimeout};"); + db.Execute($"SET LOCK_TIMEOUT {timeout.TotalMilliseconds};"); var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); if (i == 0) // ensure we are actually locking! throw new ArgumentException($"LockObject with id={lockId} does not exist."); diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 31f8b36ed0..418e4c13fc 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -39,6 +39,11 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) + throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); + + _sqlServerSyntax = sqlServerSyntaxProvider; + _logger.Debug("Acquiring lock..."); var db = GetDatabase(); @@ -52,7 +57,7 @@ namespace Umbraco.Core.Runtime try { // wait to get a write lock - _sqlServerSyntax.WriteLock(db, millisecondsTimeout, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } catch (Exception ex) { @@ -136,7 +141,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that // we are still the maindom. An empty value might be better because then we won't have any orphan rows @@ -209,7 +214,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); From 8d7ed7dd32b6ff0bb415d126b56f4c0a57b49491 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 16:26:13 +1100 Subject: [PATCH 238/610] adds const fixing naming --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 418e4c13fc..37db2899c6 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Runtime { private string _lockId; private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); @@ -197,7 +198,7 @@ namespace Umbraco.Core.Runtime /// private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { - var updatedTempId = tempId + "_updated"; + var updatedTempId = tempId + UpdatedSuffix; return Task.Run(() => { @@ -343,11 +344,11 @@ namespace Umbraco.Core.Runtime private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + private bool _disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposedValue) { if (disposing) { @@ -374,7 +375,7 @@ namespace Umbraco.Core.Runtime if (_mainDomChanging) { _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); } else { @@ -396,7 +397,7 @@ namespace Umbraco.Core.Runtime } } - disposedValue = true; + _disposedValue = true; } } From 52b93bfc9c0c11a1225383681de5f6a716b67730 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 18:33:55 +1100 Subject: [PATCH 239/610] ensures only _sqlServerSyntax is used --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 37db2899c6..4433a8e307 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -142,7 +142,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that // we are still the maindom. An empty value might be better because then we won't have any orphan rows @@ -215,7 +215,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); From 109d9e1f7b7154940440e29b8ac36487fcca4b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 11:26:55 +0100 Subject: [PATCH 240/610] implement segment support --- .../components/content/edit.controller.js | 1 + .../content/umbvariantcontent.directive.js | 1 - .../umbvariantcontenteditors.directive.js | 82 ++++++++++++------- .../editor/umbbreadcrumbs.directive.js | 2 +- .../umbeditorcontentheader.directive.js | 40 +++++---- .../culturerequest.interceptor.js | 1 + .../services/contenteditinghelper.service.js | 2 +- .../src/common/services/formhelper.service.js | 2 +- .../src/common/services/navigation.service.js | 2 +- .../common/services/varianthelper.service.js | 22 ----- .../src/views/components/content/edit.html | 9 +- .../content/umb-variant-content-editors.html | 3 +- .../content/umb-variant-content.html | 1 - .../editor/umb-editor-content-header.html | 14 ++-- .../apps/content/content.controller.js | 42 ++++++++-- .../content/content.create.controller.js | 3 + .../views/content/content.edit.controller.js | 2 + .../src/views/content/edit.html | 1 + .../contentblueprints/edit.controller.js | 2 + .../src/views/contentblueprints/edit.html | 3 +- 20 files changed, 145 insertions(+), 90 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 cab71842b1..07c1913773 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 @@ -1026,6 +1026,7 @@ getMethod: "&", getScaffoldMethod: "&?", culture: "=?", + segment: "=?", infiniteModel: "=?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index 5dc8903e65..24bcefa6b8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -12,7 +12,6 @@ editor: "<", editorIndex: "<", editorCount: "<", - openVariants: "<", onCloseSplitView: "&", onSelectVariant: "&", onOpenSplitView: "&", 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 1808d1bffa..17bf716e39 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 @@ -9,7 +9,8 @@ bindings: { page: "<", content: "<", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant - variantId: "<", + culture: "<", + segment: "<", onSelectApp: "&?", onSelectAppAnchor: "&?", onBack: "&?", @@ -42,7 +43,6 @@ //Used to track the open variants across the split views // The values are the variant ids of the currently open variants. // See variantHelper.getId() for the current format. - vm.openVariants = []; /** Called when the component initializes */ function onInit() { @@ -61,7 +61,10 @@ */ function onChanges(changes) { - if (changes.variantId && !changes.variantId.isFirstChange() && changes.variantId.currentValue !== changes.variantId.previousValue) { + if (changes.culture && !changes.culture.isFirstChange() && changes.culture.currentValue !== changes.culture.previousValue) { + setActiveVariant(); + } + if (changes.segment && !changes.segment.isFirstChange() && changes.segment.currentValue !== changes.segment.previousValue) { setActiveVariant(); } } @@ -81,13 +84,17 @@ } /** - * Set the active variant based on the current culture + segment (query string) + * Set the active variant based on the current culture or segment (query string) */ function setActiveVariant() { // set the active variant var activeVariant = null; _.each(vm.content.variants, function (v) { - if (variantHelper.getId(v) === vm.variantId) { + if ( + (!v.language || v.language.culture === vm.culture) + && + (v.segment === vm.segment) + ) { v.active = true; activeVariant = v; } @@ -107,10 +114,9 @@ if (vm.editors.length > 1) { //now re-sync any other editor content (i.e. if split view is open) for (var s = 1; s < vm.editors.length; s++) { - var editorVariantId = variantHelper.getId(vm.editors[s].content); //get the variant from the scope model var variant = _.find(vm.content.variants, function (v) { - return variantHelper.getId(v) === editorVariantId; + return (!v.language || v.language.culture === vm.editors[s].content.language.culture) && v.segment === vm.editors[s].content.segment; }); vm.editors[s].content = initVariant(variant, s); } @@ -125,19 +131,25 @@ */ function insertVariantEditor(index, variant) { - var variantId = variantHelper.getId(variant); - //check if the variant at the index is the same, if it's null an editor will be added - var currentVariantId = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].variantId; + var variantCulture = variant.language ? variant.language.culture : "invariant"; + var variantSegment = variant.segment; + + //check if the culture at the index is the same, if it's null an editor will be added + var currentCulture = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].culture; + //check if the segment at the index is the same, if it's null an editor will be added + var currentSegment = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].segment; + + if (currentCulture !== variantCulture || currentSegment !== variantSegment) { - if (currentVariantId !== variantId) { //Not the current culture which means we need to modify the array. //NOTE: It is not good enough to just replace the `content` object at a given index in the array // since that would mean that directives are not re-initialized. vm.editors.splice(index, 1, { content: variant, //used for "track-by" ng-repeat - variantId: variantId + culture: variantCulture, + segment: variantSegment }); } else { @@ -147,6 +159,7 @@ } function initVariant(variant, editorIndex) { + //The model that is assigned to the editor contains the current content variant along //with a copy of the contentApps. This is required because each editor renders it's own //header and content apps section and the content apps contains the view for editing content itself @@ -158,8 +171,8 @@ variant.apps = angular.copy(vm.content.apps); } - //if this is a variant has a culture/language than we need to assign the language drop down info - if (variant.language) { + //if this is a variant it has a culture/language or segment than we need to assign the variant drop down + if (variant.language || variant.segment !== null) { //if the variant list that defines the header drop down isn't assigned to the variant then assign it now if (!variant.variants) { variant.variants = _.map(vm.content.variants, @@ -178,8 +191,11 @@ //ensure the current culture is set as the active one for (var i = 0; i < variant.variants.length; i++) { - if (variant.variants[i].language.culture === variant.language.culture && - variant.variants[i].segment === variant.segment) { + if ( + (!variant.variants[i].language || variant.variants[i].language.culture === variant.language.culture) + && + variant.variants[i].segment === variant.segment + ) { variant.variants[i].active = true; } else { @@ -187,6 +203,8 @@ } } + /* + //SEGMENTS_TODO: Remove this part if not used. var variantId = variantHelper.getId(variant); // keep track of the open variants across the different split views // push the first variant then update the variant index based on the editor index @@ -195,7 +213,7 @@ } else { vm.openVariants[editorIndex] = variantId; } - + */ } //then assign the variant to a view model to the content app @@ -221,16 +239,19 @@ return variant; } + + function getCultureFromVariant(variant) { + return variant.language ? variant.language.culture : null; + } /** * Adds a new editor to the editors array to show content in a split view * @param {any} selectedVariant */ function openSplitView(selectedVariant) { - var variant = variantHelper.getId(selectedVariant); - + //Find the whole variant model based on the culture that was chosen var variant = _.find(vm.content.variants, function (v) { - return variantHelper.getId(v) === variant; + return getCultureFromVariant(v) === getCultureFromVariant(selectedVariant) && v.segment === selectedVariant.segment; }); insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); @@ -273,9 +294,12 @@ $timeout(function () { vm.editors.splice(editorIndex, 1); //remove variant from open variants - vm.openVariants.splice(editorIndex, 1); + + // SEGMENTS_TODO: Test this scenario. + //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) - $location.search("cculture", vm.openVariants[0]); + //$location.search({"cculture": vm.openVariants[0].language, "csegment": vm.openVariants[0]}); + $location.search({"cculture": vm.editors[0].content.language ? vm.editors[0].content.language.culture : null, "csegment": vm.editors[0].content.segment}); splitViewChanged(); }, 400); } @@ -287,10 +311,10 @@ */ function selectVariant(variant, editorIndex) { - var variantId = variantHelper.getId(variant); - - // prevent variants already open in a split view to be opened - if (vm.openVariants.indexOf(variantId) !== -1) { + var variantCulture = variant.language ? variant.language.culture : "invariant"; + var variantSegment = variant.segment || null; + + if (vm.editors.find((editor) => (!editor.content.language || editor.content.language.culture === variantCulture) && editor.content.segment === variantSegment)) { return; } @@ -299,7 +323,7 @@ if (editorIndex === 0) { //If we've made it this far, then update the query string. //The editor will respond to this query string changing. - $location.search("cculture", variantId); + $location.search({"cculture": variantCulture, "csegment": variant.segment}); } else { @@ -311,12 +335,14 @@ } variant.active = true; + //get the variant content model and initialize the editor with that var contentVariant = _.find(vm.content.variants, function (v) { - return variantHelper.getId(v) === variantId; + return (!v.language || v.language.culture === variantCulture) && v.segment === variantSegment; }); editor.content = initVariant(contentVariant, editorIndex); + //update the editors collection insertVariantEditor(editorIndex, contentVariant); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js index 7755d9d63b..f80b3ceb3e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js @@ -73,7 +73,7 @@ Use this directive to generate a list of breadcrumbs. var path = scope.pathTo(ancestor); $location.path(path); - navigationService.clearSearch(["cculture"]); + navigationService.clearSearch(["cculture", "csegment"]); } scope.pathTo = function (ancestor) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index 86a5fdbd79..c52e309580 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -3,7 +3,7 @@ function EditorContentHeader(serverValidationManager, localizationService, editorState, variantHelper) { - function link(scope, el, attr, ctrl) { + function link(scope) { var unsubscribe = []; @@ -42,22 +42,24 @@ function checkErrorsOnOtherVariants() { var check = false; angular.forEach(scope.content.variants, function (variant) { - if (scope.openVariants.indexOf(variant.language.culture) === -1 && scope.variantHasError(variant.language.culture)) { + // SEGMENTS_TODO: Check that this correction is okay, can we even see the active var here? + if (variant.active !== true && scope.variantHasError(variant)) { check = true; } }); scope.vm.errorsOnOtherVariants = check; } - function onCultureValidation(valid, errors, allErrors, culture) { - var index = scope.vm.variantsWithError.indexOf(culture); + function onVariantValidation(valid, errors, allErrors, culture, segment) { + // SEGMENTS_TODO: See wether we can use errors, allErrors? + var index = scope.vm.variantsWithError.findIndex((item) => item.culture === culture && item.segment === segment) if(valid === true) { if (index !== -1) { scope.vm.variantsWithError.splice(index, 1); } } else { if (index === -1) { - scope.vm.variantsWithError.push(culture); + scope.vm.variantsWithError.push({"culture": culture, "segment": segment}); } } checkErrorsOnOtherVariants(); @@ -82,10 +84,10 @@ angular.forEach(scope.content.variants, function (variant) { - unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, variant.segment, null, onCultureValidation)); + unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, variant.segment, null, onVariantValidation)); }); - unsubscribe.push(serverValidationManager.subscribe(null, null, null, null, onCultureValidation)); + unsubscribe.push(serverValidationManager.subscribe(null, null, null, null, onVariantValidation)); @@ -100,6 +102,10 @@ }); } + function getCultureFromVariant(variant) { + return variant.language ? variant.language.culture : null; + } + scope.getVariantDisplayName = variantHelper.getDisplayName; scope.goBack = function () { @@ -143,25 +149,30 @@ /** * keep track of open variants - this is used to prevent the same variant to be open in more than one split view - * @param {any} culture + * @param {any} variant */ scope.variantIsOpen = function (variant) { - var variantId = variantHelper.getId(variant); - return (scope.openVariants.indexOf(variantId) !== -1); + + // SEGMENTS_TODO: ... does this work? + if (scope.content.variants.find((v) => variant.active === true && (getCultureFromVariant(v) === getCultureFromVariant(variant)) && variant.segment === variant.segment)) { + console.log("VARIANT IS OPEN") + return; + } + console.log("VARIANT IS closed", scope.content.variants) } /** * Check whether a variant has a error, used to display errors in variant switcher. * @param {any} culture */ - scope.variantHasError = function(culture) { + scope.variantHasError = function(variant) { // if we are looking for the default language we also want to check for invariant. - if (culture === scope.vm.defaultVariant.language.culture) { - if(scope.vm.variantsWithError.indexOf("invariant") !== -1) { + if (variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) { + if(scope.vm.variantsWithError.find((item) => item.culture === "invariant" && item.segment === null) !== undefined) { return true; } } - if(scope.vm.variantsWithError.indexOf(culture) !== -1) { + if(scope.vm.variantsWithError.find((item) => item.culture === variant.language.culture && item.segment === variant.segment) !== undefined) { return true; } return false; @@ -205,7 +216,6 @@ menu: "=", hideActionsMenu: " 3) { htmlFieldReference = parts[3] || ""; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 8d1caab850..a3554e59ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -29,7 +29,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService //A list of query strings defined that when changed will not cause a reload of the route - var nonRoutingQueryStrings = ["mculture", "cculture", "lq", "sr"]; + var nonRoutingQueryStrings = ["mculture", "cculture", "csegment", "lq", "sr"]; var retainedQueryStrings = ["mculture"]; function setMode(mode) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/varianthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/varianthelper.service.js index d1eb6e8ad9..996e2afc9e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/varianthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/varianthelper.service.js @@ -8,27 +8,6 @@ function variantHelper() { * Returns the id for this variant * @param {any} variant */ - function getId(variant) { - var hasLanguage = variant.language && !!variant.language.culture; - var hasSegment = !!variant.segment; - - var sep = ";"; - - if (!hasLanguage && !hasSegment) { - // Invariant - return ""; - } else if (hasLanguage && !hasSegment) { - // Culture only - return variant.language.culture; - } else if (!hasLanguage && hasSegment) { - // Segment only - return sep + variant.segment; - } else { - // Culture and Segment - return variant.language.culture + sep + variant.segment; - } - } - function getDisplayName(variant) { if (variant == null) { return ""; @@ -54,7 +33,6 @@ function variantHelper() { } return { - getId, getDisplayName } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html index da91e0bee5..55f702d1c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -9,7 +9,8 @@ - -
- +   - + - + - + - +
Open in split view
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js index 2a3f67a7e3..572a083cbe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js @@ -7,6 +7,10 @@ //if we make the viewModel the variant itself, we end up with a circular reference in the models which isn't ideal // (i.e. variant.apps[contentApp].viewModel = variant) //so instead since we already have access to the content, we can just get the variant directly by the index. + + var unbindLanguageWatcher; + var unbindSegmentWatcher; + var timeout = null; var vm = this; vm.loading = true; @@ -16,26 +20,50 @@ vm.content = $scope.content.variants[$scope.model.viewModel]; serverValidationManager.notify(); vm.loading = false; + timeout = null;// ensure timeout is set to null, so we know that its not running anymore. //if this variant has a culture/language assigned, then we need to watch it since it will change //if the language drop down changes and we need to re-init if (vm.content.language) { - $scope.$watch(function () { + unbindLanguageWatcher = $scope.$watch(function () { return vm.content.language.culture; }, function (newVal, oldVal) { if (newVal !== oldVal) { - vm.loading = true; - - // TODO: Can we minimize the flicker? - $timeout(function () { - onInit(); - }, 100); + requestUpdate(); } }); + } else { + unbindLanguageWatcher = function() {} + } + + unbindSegmentWatcher = $scope.$watch(function () { + return vm.content.segment; + }, function (newVal, oldVal) { + if (newVal !== oldVal) { + requestUpdate(); + } + }); + + } + + function requestUpdate() { + if (timeout === null) { + vm.loading = true; + + // TODO: Can we minimize the flicker? + timeout = $timeout(function () { + onInit(); + }, 100); } } onInit(); + + $scope.$on("$destroy", function() { + unbindLanguageWatcher(); + unbindSegmentWatcher(); + $timeout.cancel(timeout); + }); } angular.module("umbraco").controller("Umbraco.Editors.Content.Apps.ContentController", ContentAppContentController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 64f601f8b7..db91f62a9b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -60,6 +60,9 @@ function contentCreateController($scope, language as what is selected in the tree */ .search("cculture", mainCulture) /* when we create a new node we must make sure that any previously + opened segments is reset */ + .search("csegment", null) + /* when we create a new node we must make sure that any previously used blueprint is reset */ .search("blueprintId", null); close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index ad79bf01d1..2c7c53e31f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -28,6 +28,7 @@ function ContentEditController($scope, $routeParams, contentResource) { $scope.isNew = infiniteMode ? $scope.model.create : $routeParams.create; //load the default culture selected in the main tree if any $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + $scope.segment = $routeParams.csegment ? $routeParams.csegment : null; //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change @@ -36,6 +37,7 @@ function ContentEditController($scope, $routeParams, contentResource) { //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.segment = next.params.csegment ? next.params.csegment : null; $scope.isNew = next.params.create === "true"; $scope.contentId = infiniteMode ? $scope.model.id : $routeParams.id; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index ddd9dd35ec..94e8c25b89 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -8,6 +8,7 @@ tree-alias="content" is-new="isNew" culture="culture" + segment="segment" infinite-model="model">
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 982af76d69..15112650c6 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 @@ -44,6 +44,7 @@ function ContentBlueprintEditController($scope, $routeParams, contentResource) { //load the default culture selected in the main tree if any $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + $scope.segment = $routeParams.csegment ? $routeParams.csegment : null; //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change @@ -52,6 +53,7 @@ function ContentBlueprintEditController($scope, $routeParams, contentResource) { //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.segment = next.params.csegment ? next.params.csegment : null; $scope.isNew = $routeParams.id === "-1"; $scope.contentId = $routeParams.id; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html index 191dd4db5c..751bbe5970 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html @@ -5,6 +5,7 @@ get-scaffold-method="getScaffoldMethod" is-new="isNew" tree-alias="contentblueprints" - culture="culture"> + culture="culture" + segment="segment">
From 30be9269492707c3a9bbaf364a2f2624d026d85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 14:50:58 +0100 Subject: [PATCH 241/610] Refactorying of umb-editor, splitview, variants --- .../content/umbtabbedcontent.directive.js | 30 ++- .../content/umbvariantcontent.directive.js | 10 +- .../umbvariantcontenteditors.directive.js | 184 +++--------------- .../umbeditorcontentheader.directive.js | 58 ++---- .../components/editor/umbeditorsubview.js | 3 +- .../content/umb-content-node-info.html | 10 +- .../content/umb-tabbed-content.html | 10 +- .../content/umb-variant-content-editors.html | 3 +- .../content/umb-variant-content.html | 13 +- .../editor/umb-editor-content-header.html | 20 +- .../apps/content/content.controller.js | 9 +- .../views/content/apps/content/content.html | 5 +- 12 files changed, 101 insertions(+), 254 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 06f426889f..1d17b044f9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -4,7 +4,7 @@ /** This directive is used to render out the current variant tabs and properties and exposes an API for other directives to consume */ function tabbedContentDirective($timeout) { - function link($scope, $element, $attrs) { + function link($scope, $element) { var appRootNode = $element[0]; @@ -19,8 +19,8 @@ var viewFocusY = scrollableNode.scrollTop + scrollableNode.clientHeight * .5; - for(var i in $scope.content.tabs) { - var group = $scope.content.tabs[i]; + for(var i in $scope.variant.tabs) { + var group = $scope.variant.tabs[i]; var node = propertyGroupNodesDictionary[group.id]; if (viewFocusY >= node.offsetTop && viewFocusY <= node.offsetTop + node.clientHeight) { setActiveAnchor(group); @@ -32,18 +32,18 @@ function setActiveAnchor(tab) { if (tab.active !== true) { - var i = $scope.content.tabs.length; + var i = $scope.variant.tabs.length; while(i--) { - $scope.content.tabs[i].active = false; + $scope.variant.tabs[i].active = false; } tab.active = true; } } function getActiveAnchor() { - var i = $scope.content.tabs.length; + var i = $scope.variant.tabs.length; while(i--) { - if ($scope.content.tabs[i].active === true) - return $scope.content.tabs[i]; + if ($scope.variant.tabs[i].active === true) + return $scope.variant.tabs[i]; } return false; } @@ -120,14 +120,11 @@ //expose the property/methods for other directives to use this.content = $scope.content; - this.activeVariant = _.find(this.content.variants, variant => { - return variant.active; - }); - - $scope.activeVariant = this.activeVariant; + console.log($scope) + console.log(this.content); $scope.defaultVariant = _.find(this.content.variants, variant => { - return variant.language.isDefault; + return variant.language && variant.language.isDefault; }); $scope.unlockInvariantValue = function(property) { @@ -137,7 +134,7 @@ $scope.$watch("tabbedContentForm.$dirty", function (newValue, oldValue) { if (newValue === true) { - $scope.content.isDirty = true; + $scope.variant.isDirty = true; } } ); @@ -150,7 +147,8 @@ controller: controller, link: link, scope: { - content: "=" + content: "=", + variant: "=" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index 24bcefa6b8..3c7ffa2a54 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -7,9 +7,9 @@ var umbVariantContent = { templateUrl: 'views/components/content/umb-variant-content.html', bindings: { - content: "<", - page: "<", - editor: "<", + content: "=", + page: "=", + editor: "=", editorIndex: "<", editorCount: "<", onCloseSplitView: "&", @@ -24,7 +24,7 @@ controller: umbVariantContentController }; - function umbVariantContentController($scope, $element, $location) { + function umbVariantContentController($scope) { var unsubscribe = []; @@ -43,7 +43,7 @@ function onInit() { // disable the name field if the active content app is not "Content" vm.nameDisabled = false; - angular.forEach(vm.editor.content.apps, function(app){ + angular.forEach(vm.content.apps, function(app){ if(app.active && app.alias !== "umbContent" && app.alias !== "umbInfo" && app.alias !== "umbListView") { vm.nameDisabled = true; } 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 17bf716e39..b47c95f207 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 @@ -8,7 +8,7 @@ templateUrl: 'views/components/content/umb-variant-content-editors.html', bindings: { page: "<", - content: "<", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant + content: "=", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant culture: "<", segment: "<", onSelectApp: "&?", @@ -47,6 +47,9 @@ /** Called when the component initializes */ function onInit() { prevContentDateUpdated = angular.copy(vm.content.updateDate); + _.each(vm.content.variants, function (v) { + v.active = false;// needs to be set before used for it to be re-active. + }); setActiveVariant(); } @@ -95,21 +98,14 @@ && (v.segment === vm.segment) ) { - v.active = true; activeVariant = v; } - else { - v.active = false; - } }); if (!activeVariant) { - // Set the first variant to active if we can't find it. - // If the content item is invariant, then only one item exists in the array. - vm.content.variants[0].active = true; activeVariant = vm.content.variants[0]; } - insertVariantEditor(0, initVariant(activeVariant, 0)); + insertVariantEditor(0, activeVariant); if (vm.editors.length > 1) { //now re-sync any other editor content (i.e. if split view is open) @@ -118,7 +114,7 @@ var variant = _.find(vm.content.variants, function (v) { return (!v.language || v.language.culture === vm.editors[s].content.language.culture) && v.segment === vm.editors[s].content.segment; }); - vm.editors[s].content = initVariant(variant, s); + vm.editors[s].content = variant; } } @@ -131,6 +127,10 @@ */ function insertVariantEditor(index, variant) { + if (vm.editors[index]) { + vm.editors[index].content.active = false; + } + variant.active = true; var variantCulture = variant.language ? variant.language.culture : "invariant"; var variantSegment = variant.segment; @@ -140,6 +140,7 @@ //check if the segment at the index is the same, if it's null an editor will be added var currentSegment = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].segment; + if (currentCulture !== variantCulture || currentSegment !== variantSegment) { //Not the current culture which means we need to modify the array. @@ -157,151 +158,30 @@ vm.editors[index].content = variant; } } - - function initVariant(variant, editorIndex) { - - //The model that is assigned to the editor contains the current content variant along - //with a copy of the contentApps. This is required because each editor renders it's own - //header and content apps section and the content apps contains the view for editing content itself - //and we need to assign a view model to the subView so that it is scoped to the current - //editor so that split views work. - - //copy the apps from the main model if not assigned yet to the variant - if (!variant.apps) { - variant.apps = angular.copy(vm.content.apps); - } - - //if this is a variant it has a culture/language or segment than we need to assign the variant drop down - if (variant.language || variant.segment !== null) { - //if the variant list that defines the header drop down isn't assigned to the variant then assign it now - if (!variant.variants) { - variant.variants = _.map(vm.content.variants, - function (v) { - return _.pick(v, "active", "language", "segment", "state"); - }); - } - else { - //merge the scope variants on top of the header variants collection (handy when needing to refresh) - angular.extend(variant.variants, - _.map(vm.content.variants, - function (v) { - return _.pick(v, "active", "language", "segment", "state"); - })); - } - - //ensure the current culture is set as the active one - for (var i = 0; i < variant.variants.length; i++) { - if ( - (!variant.variants[i].language || variant.variants[i].language.culture === variant.language.culture) - && - variant.variants[i].segment === variant.segment - ) { - variant.variants[i].active = true; - } - else { - variant.variants[i].active = false; - } - } - - /* - //SEGMENTS_TODO: Remove this part if not used. - var variantId = variantHelper.getId(variant); - // keep track of the open variants across the different split views - // push the first variant then update the variant index based on the editor index - if (vm.openVariants && vm.openVariants.length === 0) { - vm.openVariants.push(variantId); - } else { - vm.openVariants[editorIndex] = variantId; - } - */ - } - - //then assign the variant to a view model to the content app - var contentApp = _.find(variant.apps, function (a) { - return a.alias === "umbContent"; - }); - - 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) { - angular.forEach(variant.apps, function(app) { - app.active = false; - if(app.alias === activeAppAlias) { - app.active = true; - } - }); - } - - return variant; - } - function getCultureFromVariant(variant) { - return variant.language ? variant.language.culture : null; - } /** * Adds a new editor to the editors array to show content in a split view * @param {any} selectedVariant */ function openSplitView(selectedVariant) { - //Find the whole variant model based on the culture that was chosen - var variant = _.find(vm.content.variants, function (v) { - return getCultureFromVariant(v) === getCultureFromVariant(selectedVariant) && v.segment === selectedVariant.segment; - }); - - insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); - - //only the content app can be selected since no other apps are shown, and because we copy all of these apps - //to the "editors" we need to update this across all editors - for (var e = 0; e < vm.editors.length; e++) { - var editor = vm.editors[e]; - for (var i = 0; i < editor.content.apps.length; i++) { - var app = editor.content.apps[i]; - if (app.alias === "umbContent") { - app.active = true; - // tell the world that the app has changed (but do it only once) - if (e === 0) { - selectApp(app); - } - } - else { - app.active = false; - } - } - } - - // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular - editor.collapsed = true; - editor.loading = true; - $timeout(function () { - editor.collapsed = false; - editor.loading = false; - splitViewChanged(); - }, 100); + insertVariantEditor(vm.editors.length, selectedVariant); + + splitViewChanged(); + } /** Closes the split view */ function closeSplitView(editorIndex) { // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular var editor = vm.editors[editorIndex]; - editor.loading = true; - editor.collapsed = true; - $timeout(function () { - vm.editors.splice(editorIndex, 1); - //remove variant from open variants - - // SEGMENTS_TODO: Test this scenario. - - //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) - //$location.search({"cculture": vm.openVariants[0].language, "csegment": vm.openVariants[0]}); - $location.search({"cculture": vm.editors[0].content.language ? vm.editors[0].content.language.culture : null, "csegment": vm.editors[0].content.segment}); - splitViewChanged(); - }, 400); + vm.editors.splice(editorIndex, 1); + editor.content.active = false; + + //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) + + $location.search({"cculture": vm.editors[0].content.language ? vm.editors[0].content.language.culture : null, "csegment": vm.editors[0].content.segment}); + splitViewChanged(); } /** @@ -314,6 +194,7 @@ var variantCulture = variant.language ? variant.language.culture : "invariant"; var variantSegment = variant.segment || null; + // Check if we already have this editor open, if so, do nothing. if (vm.editors.find((editor) => (!editor.content.language || editor.content.language.culture === variantCulture) && editor.content.segment === variantSegment)) { return; } @@ -327,25 +208,8 @@ } else { - //Update the 'active' variant for this editor - var editor = vm.editors[editorIndex]; - //set all variant drop down items as inactive for this editor and then set the selected one as active - for (var i = 0; i < editor.content.variants.length; i++) { - editor.content.variants[i].active = false; - } - variant.active = true; - - - //get the variant content model and initialize the editor with that - var contentVariant = _.find(vm.content.variants, - function (v) { - return (!v.language || v.language.culture === variantCulture) && v.segment === variantSegment; - }); - editor.content = initVariant(contentVariant, editorIndex); - - //update the editors collection - insertVariantEditor(editorIndex, contentVariant); + insertVariantEditor(editorIndex, variant); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index c52e309580..cbc090c821 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -5,6 +5,9 @@ function link(scope) { + console.log("scope.content", scope.content); + console.log("scope.content.variants", scope.content.variants); + var unsubscribe = []; if (!scope.serverValidationNameField) { @@ -14,7 +17,7 @@ scope.serverValidationAliasField = "Alias"; } - scope.isNew = scope.content.state == "NotCreated"; + scope.isNew = scope.editorContent.state == "NotCreated"; localizationService.localizeMany([ scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit", @@ -23,7 +26,7 @@ scope.a11yMessage = data[0]; scope.a11yName = data[1]; if (!scope.isNew) { - scope.a11yMessage += " " + scope.content.name; + scope.a11yMessage += " " + scope.editorContent.name; } else { var name = editorState.current.contentTypeName; @@ -32,8 +35,8 @@ } }); scope.vm = {}; + scope.vm.hasVariants = false; scope.vm.dropdownOpen = false; - scope.vm.currentVariant = ""; scope.vm.variantsWithError = []; scope.vm.defaultVariant = null; @@ -42,7 +45,6 @@ function checkErrorsOnOtherVariants() { var check = false; angular.forEach(scope.content.variants, function (variant) { - // SEGMENTS_TODO: Check that this correction is okay, can we even see the active var here? if (variant.active !== true && scope.variantHasError(variant)) { check = true; } @@ -51,7 +53,6 @@ } function onVariantValidation(valid, errors, allErrors, culture, segment) { - // SEGMENTS_TODO: See wether we can use errors, allErrors? var index = scope.vm.variantsWithError.findIndex((item) => item.culture === culture && item.segment === segment) if(valid === true) { if (index !== -1) { @@ -67,14 +68,17 @@ function onInit() { - // find default. + // find default + check if we have variants. angular.forEach(scope.content.variants, function (variant) { if (variant.language !== null && variant.language.isDefault) { scope.vm.defaultVariant = variant; } + if (variant.language !== null || variant.segment !== null) { + scope.vm.hasVariants = true; + } }); - setCurrentVariant(); + checkErrorsOnOtherVariants(); angular.forEach(scope.content.apps, (app) => { if (app.alias === "umbContent") { @@ -93,15 +97,6 @@ } - function setCurrentVariant() { - angular.forEach(scope.content.variants, function (variant) { - if (variant.active) { - scope.vm.currentVariant = variant; - checkErrorsOnOtherVariants(); - } - }); - } - function getCultureFromVariant(variant) { return variant.language ? variant.language.culture : null; } @@ -146,40 +141,28 @@ scope.onOpenInSplitView({ "variant": variant }); } }; - - /** - * keep track of open variants - this is used to prevent the same variant to be open in more than one split view - * @param {any} variant - */ - scope.variantIsOpen = function (variant) { - - // SEGMENTS_TODO: ... does this work? - if (scope.content.variants.find((v) => variant.active === true && (getCultureFromVariant(v) === getCultureFromVariant(variant)) && variant.segment === variant.segment)) { - console.log("VARIANT IS OPEN") - return; - } - console.log("VARIANT IS closed", scope.content.variants) - } /** * Check whether a variant has a error, used to display errors in variant switcher. * @param {any} culture */ scope.variantHasError = function(variant) { - // if we are looking for the default language we also want to check for invariant. - if (variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) { - if(scope.vm.variantsWithError.find((item) => item.culture === "invariant" && item.segment === null) !== undefined) { - return true; + if(variant.language) { + // if we are looking for the variant with default language then we also want to check for invariant variant. + if (variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) { + if(scope.vm.variantsWithError.find((item) => item.culture === "invariant" && item.segment === null) !== undefined) { + return true; + } } } - if(scope.vm.variantsWithError.find((item) => item.culture === variant.language.culture && item.segment === variant.segment) !== undefined) { + if(scope.vm.variantsWithError.find((item) => (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment) !== undefined) { return true; } return false; } onInit(); - + /* //watch for the active culture changing, if it changes, update the current variant if (scope.content.variants) { scope.$watch(function () { @@ -196,7 +179,7 @@ } }); } - + */ scope.$on('$destroy', function () { for (var u in unsubscribe) { unsubscribe[u](); @@ -216,6 +199,7 @@ menu: "=", hideActionsMenu: " - +
- @@ -79,7 +79,7 @@
- {{ item.comment }} - +
@@ -126,7 +126,7 @@
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index 8d7bdad873..85ba05ee16 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -1,6 +1,6 @@ 
-
+
{{ group.label }}
@@ -11,13 +11,13 @@ data-element="property-{{property.alias}}" ng-repeat="property in group.properties track by property.alias" property="property" - show-inherit="content.variants.length > 1 && ((!activeVariant.language.isDefault && !property.culture) || (activeVariant.segment && !property.segment)) && !property.unlockInvariantValue" + show-inherit="content.variants.length > 1 && ((!variant.language.isDefault && !property.culture) || (variant.segment && !property.segment)) && !property.unlockInvariantValue" inherits-from="defaultVariant.language.name"> -
+
+ preview="content.variants.length > 1 && ((!variant.language.isDefault && !property.culture) || (variant.segment && !property.segment)) && !property.unlockInvariantValue">
@@ -27,7 +27,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html index 45517d4f70..61860f1855 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html @@ -1,7 +1,6 @@ 
+ ng-repeat="editor in vm.editors track by editor.culture+'.'+editor.segment"> - + - +
@@ -33,14 +34,14 @@
-
- +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 7c871de069..d06ca697cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -39,25 +39,25 @@ autocomplete="off" maxlength="255" /> - - + - - + + - - +
Open in split view
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js index 572a083cbe..00eca43059 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js @@ -16,17 +16,16 @@ vm.loading = true; function onInit() { - //get the variant by index (see notes above) - vm.content = $scope.content.variants[$scope.model.viewModel]; + serverValidationManager.notify(); vm.loading = false; timeout = null;// ensure timeout is set to null, so we know that its not running anymore. //if this variant has a culture/language assigned, then we need to watch it since it will change //if the language drop down changes and we need to re-init - if (vm.content.language) { + if ($scope.variant.language) { unbindLanguageWatcher = $scope.$watch(function () { - return vm.content.language.culture; + return $scope.variant.language.culture; }, function (newVal, oldVal) { if (newVal !== oldVal) { requestUpdate(); @@ -37,7 +36,7 @@ } unbindSegmentWatcher = $scope.$watch(function () { - return vm.content.segment; + return $scope.variant.segment; }, function (newVal, oldVal) { if (newVal !== oldVal) { requestUpdate(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html index d391f2cd95..d49b78b8a4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html @@ -2,7 +2,8 @@ + variant="variant" + content="content"> - +
From aaf53921eb1a09aa6297599432e79ab37d16119d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 15:15:48 +0100 Subject: [PATCH 242/610] upgrade to Angular 1.7.9 + npm 6.13.6 --- src/Umbraco.Web.UI.Client/package-lock.json | 143 +++++++++++--------- src/Umbraco.Web.UI.Client/package.json | 4 +- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index d69ef62f16..dd58b5ca18 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1099,9 +1099,9 @@ "dev": true }, "angular": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.5.tgz", - "integrity": "sha512-760183yxtGzni740IBTieNuWLtPNAoMqvmC0Z62UoU0I3nqk+VJuO3JbQAXOyvo3Oy/ZsdNQwrSTh/B0OQZjNw==" + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.9.tgz", + "integrity": "sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ==" }, "angular-animate": { "version": "1.7.5", @@ -9324,9 +9324,9 @@ "dev": true }, "nouislider": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.0.2.tgz", - "integrity": "sha512-N4AQStV4frh+XcLUwMI/hZpBP6tRboDE/4LZ7gzfxMVXFi/2J9URphnm40Ff4KEyrAVGSGaWApvljoMzTNWBlA==" + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.1.1.tgz", + "integrity": "sha512-3/+Z/pTBoWoJf2YXSEWRmS27LW2XxOBmGEzkPyRzB/J6QvL+0mS3QwcQp0SmWhgO5CMzbSxPmb1lDDD4HP12bg==" }, "now-and-later": { "version": "2.0.1", @@ -9338,9 +9338,9 @@ } }, "npm": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.12.0.tgz", - "integrity": "sha512-juj5VkB3/k+PWbJUnXD7A/8oc8zLusDnK/sV9PybSalsbOVOTIp5vSE0rz5rQ7BsmUgQS47f/L2GYQnWXaKgnQ==", + "version": "6.13.6", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.13.6.tgz", + "integrity": "sha512-NomC08kv7HIl1FOyLOe9Hp89kYsOsvx52huVIJ7i8hFW8Xp65lDwe/8wTIrh9q9SaQhA8hTrfXPh3BEL3TmMpw==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -9348,12 +9348,12 @@ "ansistyles": "~0.1.3", "aproba": "^2.0.0", "archy": "~1.0.0", - "bin-links": "^1.1.3", + "bin-links": "^1.1.6", "bluebird": "^3.5.5", "byte-size": "^5.0.1", "cacache": "^12.0.3", "call-limit": "^1.1.1", - "chownr": "^1.1.2", + "chownr": "^1.1.3", "ci-info": "^2.0.0", "cli-columns": "^3.1.2", "cli-table3": "^0.5.1", @@ -9369,9 +9369,9 @@ "find-npm-prefix": "^1.0.2", "fs-vacuum": "~1.2.10", "fs-write-stream-atomic": "~1.0.10", - "gentle-fs": "^2.2.1", + "gentle-fs": "^2.3.0", "glob": "^7.1.4", - "graceful-fs": "^4.2.2", + "graceful-fs": "^4.2.3", "has-unicode": "~2.0.1", "hosted-git-info": "^2.8.5", "iferr": "^1.0.2", @@ -9384,7 +9384,7 @@ "is-cidr": "^3.0.0", "json-parse-better-errors": "^1.0.2", "lazy-property": "~1.0.0", - "libcipm": "^4.0.4", + "libcipm": "^4.0.7", "libnpm": "^3.0.1", "libnpmaccess": "^3.0.2", "libnpmhook": "^5.0.3", @@ -9418,25 +9418,25 @@ "npm-install-checks": "^3.0.2", "npm-lifecycle": "^3.1.4", "npm-package-arg": "^6.1.1", - "npm-packlist": "^1.4.4", + "npm-packlist": "^1.4.7", "npm-pick-manifest": "^3.0.2", "npm-profile": "^4.0.2", - "npm-registry-fetch": "^4.0.0", + "npm-registry-fetch": "^4.0.2", "npm-user-validate": "~1.0.0", "npmlog": "~4.1.2", "once": "~1.4.0", "opener": "^1.5.1", "osenv": "^0.1.5", - "pacote": "^9.5.8", + "pacote": "^9.5.12", "path-is-inside": "~1.0.2", "promise-inflight": "~1.0.1", "qrcode-terminal": "^0.12.0", "query-string": "^6.8.2", "qw": "~1.0.1", "read": "~1.0.7", - "read-cmd-shim": "^1.0.4", + "read-cmd-shim": "^1.0.5", "read-installed": "~4.0.3", - "read-package-json": "^2.1.0", + "read-package-json": "^2.1.1", "read-package-tree": "^5.3.1", "readable-stream": "^3.4.0", "readdir-scoped-modules": "^1.1.0", @@ -9451,7 +9451,7 @@ "sorted-union-stream": "~2.1.3", "ssri": "^6.0.1", "stringify-package": "^1.0.1", - "tar": "^4.4.12", + "tar": "^4.4.13", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "uid-number": "0.0.6", @@ -9459,7 +9459,7 @@ "unique-filename": "^1.1.1", "unpipe": "~1.0.0", "update-notifier": "^2.5.0", - "uuid": "^3.3.2", + "uuid": "^3.3.3", "validate-npm-package-license": "^3.0.4", "validate-npm-package-name": "~3.0.0", "which": "^1.3.1", @@ -9607,13 +9607,14 @@ } }, "bin-links": { - "version": "1.1.3", + "version": "1.1.6", "bundled": true, "requires": { "bluebird": "^3.5.3", "cmd-shim": "^3.0.0", - "gentle-fs": "^2.0.1", + "gentle-fs": "^2.3.0", "graceful-fs": "^4.1.15", + "npm-normalize-package-bin": "^1.0.0", "write-file-atomic": "^2.3.0" } }, @@ -9705,7 +9706,7 @@ } }, "chownr": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true }, "ci-info": { @@ -10266,7 +10267,7 @@ }, "dependencies": { "minipass": { - "version": "2.8.6", + "version": "2.9.0", "bundled": true, "requires": { "safe-buffer": "^5.1.2", @@ -10362,11 +10363,12 @@ "bundled": true }, "gentle-fs": { - "version": "2.2.1", + "version": "2.3.0", "bundled": true, "requires": { "aproba": "^1.1.2", "chownr": "^1.1.2", + "cmd-shim": "^3.0.3", "fs-vacuum": "^1.2.10", "graceful-fs": "^4.1.11", "iferr": "^0.1.5", @@ -10448,7 +10450,7 @@ } }, "graceful-fs": { - "version": "4.2.2", + "version": "4.2.3", "bundled": true }, "har-schema": { @@ -10508,7 +10510,7 @@ } }, "https-proxy-agent": { - "version": "2.2.2", + "version": "2.2.4", "bundled": true, "requires": { "agent-base": "^4.3.0", @@ -10534,7 +10536,7 @@ "bundled": true }, "ignore-walk": { - "version": "3.0.1", + "version": "3.0.3", "bundled": true, "requires": { "minimatch": "^3.0.4" @@ -10748,7 +10750,7 @@ } }, "libcipm": { - "version": "4.0.4", + "version": "4.0.7", "bundled": true, "requires": { "bin-links": "^1.1.2", @@ -11017,14 +11019,14 @@ } }, "make-fetch-happen": { - "version": "5.0.0", + "version": "5.0.2", "bundled": true, "requires": { "agentkeepalive": "^3.4.1", "cacache": "^12.0.0", "http-cache-semantics": "^3.8.1", "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", + "https-proxy-agent": "^2.2.3", "lru-cache": "^5.1.1", "mississippi": "^3.0.0", "node-fetch-npm": "^2.0.2", @@ -11070,27 +11072,23 @@ "version": "0.0.8", "bundled": true }, - "minipass": { - "version": "2.3.3", + "minizlib": { + "version": "1.3.3", "bundled": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "minipass": "^2.9.0" }, "dependencies": { - "yallist": { - "version": "3.0.2", - "bundled": true + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } } } }, - "minizlib": { - "version": "1.2.2", - "bundled": true, - "requires": { - "minipass": "^2.2.1" - } - }, "mississippi": { "version": "3.0.0", "bundled": true, @@ -11215,8 +11213,11 @@ } }, "npm-bundled": { - "version": "1.0.6", - "bundled": true + "version": "1.1.1", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } }, "npm-cache-filename": { "version": "1.0.2", @@ -11247,6 +11248,10 @@ "version": "1.2.1", "bundled": true }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true + }, "npm-package-arg": { "version": "6.1.1", "bundled": true, @@ -11258,7 +11263,7 @@ } }, "npm-packlist": { - "version": "1.4.4", + "version": "1.4.7", "bundled": true, "requires": { "ignore-walk": "^3.0.1", @@ -11284,7 +11289,7 @@ } }, "npm-registry-fetch": { - "version": "4.0.0", + "version": "4.0.2", "bundled": true, "requires": { "JSONStream": "^1.3.4", @@ -11292,7 +11297,14 @@ "figgy-pudding": "^3.4.1", "lru-cache": "^5.1.1", "make-fetch-happen": "^5.0.0", - "npm-package-arg": "^6.1.0" + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "bundled": true + } } }, "npm-run-path": { @@ -11409,7 +11421,7 @@ } }, "pacote": { - "version": "9.5.8", + "version": "9.5.12", "bundled": true, "requires": { "bluebird": "^3.5.3", @@ -11426,6 +11438,7 @@ "mississippi": "^3.0.0", "mkdirp": "^0.5.1", "normalize-package-data": "^2.4.0", + "npm-normalize-package-bin": "^1.0.0", "npm-package-arg": "^6.1.0", "npm-packlist": "^1.1.12", "npm-pick-manifest": "^3.0.0", @@ -11444,7 +11457,7 @@ }, "dependencies": { "minipass": { - "version": "2.3.5", + "version": "2.9.0", "bundled": true, "requires": { "safe-buffer": "^5.1.2", @@ -11644,7 +11657,7 @@ } }, "read-cmd-shim": { - "version": "1.0.4", + "version": "1.0.5", "bundled": true, "requires": { "graceful-fs": "^4.1.2" @@ -11664,14 +11677,14 @@ } }, "read-package-json": { - "version": "2.1.0", + "version": "2.1.1", "bundled": true, "requires": { "glob": "^7.1.1", "graceful-fs": "^4.1.2", "json-parse-better-errors": "^1.0.1", "normalize-package-data": "^2.0.0", - "slash": "^1.0.0" + "npm-normalize-package-bin": "^1.0.0" } }, "read-package-tree": { @@ -11824,24 +11837,20 @@ "version": "3.0.2", "bundled": true }, - "slash": { - "version": "1.0.0", - "bundled": true - }, "slide": { "version": "1.1.6", "bundled": true }, "smart-buffer": { - "version": "4.0.2", + "version": "4.1.0", "bundled": true }, "socks": { - "version": "2.3.2", + "version": "2.3.3", "bundled": true, "requires": { - "ip": "^1.1.5", - "smart-buffer": "4.0.2" + "ip": "1.1.5", + "smart-buffer": "^4.1.0" } }, "socks-proxy-agent": { @@ -12056,7 +12065,7 @@ } }, "tar": { - "version": "4.4.12", + "version": "4.4.13", "bundled": true, "requires": { "chownr": "^1.1.1", @@ -12069,7 +12078,7 @@ }, "dependencies": { "minipass": { - "version": "2.8.6", + "version": "2.9.0", "bundled": true, "requires": { "safe-buffer": "^5.1.2", @@ -12231,7 +12240,7 @@ } }, "uuid": { - "version": "3.3.2", + "version": "3.3.3", "bundled": true }, "validate-npm-package-license": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 0f02aba5e2..d60da6e790 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "ace-builds": "1.4.2", - "angular": "1.7.5", + "angular": "^1.7.9", "angular-animate": "1.7.5", "angular-aria": "1.7.5", "angular-chart.js": "^1.1.1", @@ -39,7 +39,7 @@ "moment": "2.22.2", "ng-file-upload": "12.2.13", "nouislider": "14.1.1", - "npm": "6.12.0", + "npm": "^6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.2", From 7fc0f01887cf57b3a6e393155939a56e15bf6314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 15:23:32 +0100 Subject: [PATCH 243/610] fixed versions for angular + npm --- src/Umbraco.Web.UI.Client/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index d60da6e790..beff2b45d1 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "ace-builds": "1.4.2", - "angular": "^1.7.9", + "angular": "1.7.9", "angular-animate": "1.7.5", "angular-aria": "1.7.5", "angular-chart.js": "^1.1.1", @@ -39,7 +39,7 @@ "moment": "2.22.2", "ng-file-upload": "12.2.13", "nouislider": "14.1.1", - "npm": "^6.13.6", + "npm": "6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.2", From f302b373acda50405d8842bc46c83acf611b8306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 17:00:14 +0100 Subject: [PATCH 244/610] basic tests for segments validation --- .../server-validation-manager.spec.js | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 8d6efde7b0..691d158c78 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -90,7 +90,7 @@ //act var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", null, "value1"); - var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, null, "value2"); + var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, null, "value1"); var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", null, "value2"); var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, null, "value2"); @@ -111,6 +111,71 @@ expect(err2.errorMsg).toEqual("Another value 2"); expect(err2.culture).toEqual("fr-FR"); }); + + it('can retrieve property validation errors for a sub field for segments', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", null, "segment1", "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, "segment2", "value2", "Another value 2"); + + //act + var err1 = serverValidationManager.getPropertyError("myProperty", null, "segment1", "value1"); + var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, null, "value1"); + var err2 = serverValidationManager.getPropertyError("myProperty", null, "segment2", "value2"); + var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, null, "value2"); + + + //assert + expect(err1NotFound).toBeUndefined(); + expect(err2NotFound).toBeUndefined(); + + expect(err1).not.toBeUndefined(); + expect(err1.propertyAlias).toEqual("myProperty"); + expect(err1.fieldName).toEqual("value1"); + expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.segment).toEqual("segment1"); + + expect(err2).not.toBeUndefined(); + expect(err2.propertyAlias).toEqual("myProperty"); + expect(err2.fieldName).toEqual("value2"); + expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.segment).toEqual("segment2"); + }); + + + it('can retrieve property validation errors for a sub field for culture with segments', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", "en-US", "segment1", "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "segment2", "value2", "Another value 2"); + + //act + var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "segment1", "value1"); + expect(serverValidationManager.getPropertyError("myProperty", null, null, "value1")).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", "en-US", null, "value1")).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", null, "segment1", "value1")).toBeUndefined(); + var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "segment2", "value2"); + expect(serverValidationManager.getPropertyError("myProperty", null, null, "value2")).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", "fr-FR", null, "value2")).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", null, "segment2", "value2")).toBeUndefined(); + + + //assert + + expect(err1).not.toBeUndefined(); + expect(err1.propertyAlias).toEqual("myProperty"); + expect(err1.fieldName).toEqual("value1"); + expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.culture).toEqual("en-US"); + expect(err1.segment).toEqual("segment1"); + + expect(err2).not.toBeUndefined(); + expect(err2.propertyAlias).toEqual("myProperty"); + expect(err2.fieldName).toEqual("value2"); + expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.culture).toEqual("fr-FR"); + expect(err2.segment).toEqual("segment2"); + }); it('can add a property errors with multiple sub fields and it the first will be retreived with only the property alias', function () { From d4d7425c24f2e15fd78d220e75504da5ac6f4a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:09:48 +0100 Subject: [PATCH 245/610] Refactoring part 2 of umb-editor, splitview, variants. --- .../content/umbtabbedcontent.directive.js | 33 +++++++++---------- .../components/editor/umbeditorsubview.js | 2 +- .../content/umb-tabbed-content.html | 8 ++--- .../content/umb-variant-content.html | 2 +- .../apps/content/content.controller.js | 28 ++++++++-------- .../views/content/apps/content/content.html | 4 +-- 6 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 1d17b044f9..3131fbc6d3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -19,8 +19,8 @@ var viewFocusY = scrollableNode.scrollTop + scrollableNode.clientHeight * .5; - for(var i in $scope.variant.tabs) { - var group = $scope.variant.tabs[i]; + for(var i in $scope.content.tabs) { + var group = $scope.content.tabs[i]; var node = propertyGroupNodesDictionary[group.id]; if (viewFocusY >= node.offsetTop && viewFocusY <= node.offsetTop + node.clientHeight) { setActiveAnchor(group); @@ -32,18 +32,18 @@ function setActiveAnchor(tab) { if (tab.active !== true) { - var i = $scope.variant.tabs.length; + var i = $scope.content.tabs.length; while(i--) { - $scope.variant.tabs[i].active = false; + $scope.content.tabs[i].active = false; } tab.active = true; } } function getActiveAnchor() { - var i = $scope.variant.tabs.length; + var i = $scope.content.tabs.length; while(i--) { - if ($scope.variant.tabs[i].active === true) - return $scope.variant.tabs[i]; + if ($scope.content.tabs[i].active === true) + return $scope.content.tabs[i]; } return false; } @@ -115,18 +115,17 @@ } - function controller($scope, $element, $attrs) { + function controller($scope) { //expose the property/methods for other directives to use this.content = $scope.content; - console.log($scope) - console.log(this.content); - - $scope.defaultVariant = _.find(this.content.variants, variant => { - return variant.language && variant.language.isDefault; - }); - + if($scope.variantNodeModel) { + $scope.defaultVariant = _.find($scope.variantNodeModel.variants, variant => { + return variant.language && variant.language.isDefault; + }); + } + $scope.unlockInvariantValue = function(property) { property.unlockInvariantValue = !property.unlockInvariantValue; }; @@ -134,7 +133,7 @@ $scope.$watch("tabbedContentForm.$dirty", function (newValue, oldValue) { if (newValue === true) { - $scope.variant.isDirty = true; + $scope.content.isDirty = true; } } ); @@ -148,7 +147,7 @@ link: link, scope: { content: "=", - variant: "=" + variantNodeModel: "=?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorsubview.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorsubview.js index ebb53415c3..614f421699 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorsubview.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorsubview.js @@ -20,7 +20,7 @@ templateUrl: 'views/components/editor/umb-editor-sub-view.html', scope: { model: "=", - variant: "=?", + variantContent: "=?", content: "=" }, link: link diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index 85ba05ee16..56f2835d3f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -1,6 +1,6 @@ 
-
+
{{ group.label }}
@@ -11,13 +11,13 @@ data-element="property-{{property.alias}}" ng-repeat="property in group.properties track by property.alias" property="property" - show-inherit="content.variants.length > 1 && ((!variant.language.isDefault && !property.culture) || (variant.segment && !property.segment)) && !property.unlockInvariantValue" + show-inherit="variantNodeModel.variants.length > 1 && ((!content.language.isDefault && !property.culture) || (content.segment && !property.segment)) && !property.unlockInvariantValue" inherits-from="defaultVariant.language.name"> -
+
+ preview="variantNodeModel.variants.length > 1 && ((!content.language.isDefault && !property.culture) || (content.segment && !property.segment)) && !property.unlockInvariantValue">
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 bf1fe34d07..ba3cf9fc92 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 @@ -35,7 +35,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js index 00eca43059..bbe09e1e14 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js @@ -23,25 +23,27 @@ //if this variant has a culture/language assigned, then we need to watch it since it will change //if the language drop down changes and we need to re-init - if ($scope.variant.language) { - unbindLanguageWatcher = $scope.$watch(function () { - return $scope.variant.language.culture; + if ($scope.variantContent) { + if ($scope.variantContent.language) { + unbindLanguageWatcher = $scope.$watch(function () { + return $scope.variantContent.language.culture; + }, function (newVal, oldVal) { + if (newVal !== oldVal) { + requestUpdate(); + } + }); + } else { + unbindLanguageWatcher = function() {} + } + + unbindSegmentWatcher = $scope.$watch(function () { + return $scope.variantContent.segment; }, function (newVal, oldVal) { if (newVal !== oldVal) { requestUpdate(); } }); - } else { - unbindLanguageWatcher = function() {} } - - unbindSegmentWatcher = $scope.$watch(function () { - return $scope.variant.segment; - }, function (newVal, oldVal) { - if (newVal !== oldVal) { - requestUpdate(); - } - }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html index d49b78b8a4..eebed6bae9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html @@ -2,8 +2,8 @@ + variant-node-model="content" + content="variantContent">
From 2f2fc72500800cc1158bd585936547de1b9c8a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:14:14 +0100 Subject: [PATCH 246/610] correct binding --- .../components/content/umbvariantcontent.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index 3c7ffa2a54..b754a88f53 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -8,7 +8,7 @@ templateUrl: 'views/components/content/umb-variant-content.html', bindings: { content: "=", - page: "=", + page: "<", editor: "=", editorIndex: "<", editorCount: "<", From 9215575f192add11e4d9fcbc8da342ce8c234385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:14:22 +0100 Subject: [PATCH 247/610] remove comment --- .../components/content/umbvariantcontenteditors.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b47c95f207..60654e3062 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 @@ -8,7 +8,7 @@ templateUrl: 'views/components/content/umb-variant-content-editors.html', bindings: { page: "<", - content: "=", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant + content: "=", culture: "<", segment: "<", onSelectApp: "&?", From 3672048c60d265f2ee2bd3ce4ed7ddb4568206ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:17:40 +0100 Subject: [PATCH 248/610] clean up --- .../content/umbvariantcontenteditors.directive.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) 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 60654e3062..1c41c7d0f0 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 @@ -20,12 +20,11 @@ controller: umbVariantContentEditorsController }; - function umbVariantContentEditorsController($scope, $location, $timeout, variantHelper) { + function umbVariantContentEditorsController($scope, $location) { var prevContentDateUpdated = null; var vm = this; - var activeAppAlias = null; vm.$onInit = onInit; vm.$onChanges = onChanges; @@ -40,9 +39,6 @@ //Used to track how many content views there are (for split view there will be 2, it could support more in theory) vm.editors = []; - //Used to track the open variants across the split views - // The values are the variant ids of the currently open variants. - // See variantHelper.getId() for the current format. /** Called when the component initializes */ function onInit() { @@ -229,14 +225,6 @@ vm.onSelectAppAnchor({"app": app, "anchor": anchor}); } } - - - $scope.$on("editors.apps.appChanged", function($event, $args) { - var app = $args.app; - if(app && app.alias) { - activeAppAlias = app.alias; - } - }); } From 535b762f6eeddfd30d7c433867e3a6ba3271ef01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:24:33 +0100 Subject: [PATCH 249/610] reverting these lines from refactoring process --- .../components/content/umbvariantcontenteditors.directive.js | 3 --- 1 file changed, 3 deletions(-) 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 1c41c7d0f0..d1f70a50a5 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 @@ -43,9 +43,6 @@ /** Called when the component initializes */ function onInit() { prevContentDateUpdated = angular.copy(vm.content.updateDate); - _.each(vm.content.variants, function (v) { - v.active = false;// needs to be set before used for it to be re-active. - }); setActiveVariant(); } From e02624baf57209e38706098537e051b6d5ddd186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:32:31 +0100 Subject: [PATCH 250/610] remove spaces --- .../components/content/umbvariantcontenteditors.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d1f70a50a5..2450e04ac3 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 @@ -84,7 +84,7 @@ */ function setActiveVariant() { // set the active variant - var activeVariant = null; + var activeVariant = null; _.each(vm.content.variants, function (v) { if ( (!v.language || v.language.culture === vm.culture) From 03b05a05d15a6cf31478261c9b136b54189d9daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:40:41 +0100 Subject: [PATCH 251/610] clean up --- .../umbvariantcontenteditors.directive.js | 8 ++--- .../umbeditorcontentheader.directive.js | 32 +++---------------- 2 files changed, 8 insertions(+), 32 deletions(-) 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 2450e04ac3..7a266163be 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 @@ -86,15 +86,13 @@ // set the active variant var activeVariant = null; _.each(vm.content.variants, function (v) { - if ( - (!v.language || v.language.culture === vm.culture) - && - (v.segment === vm.segment) - ) { + if ((!v.language || v.language.culture === vm.culture) && v.segment === vm.segment) { activeVariant = v; } }); if (!activeVariant) { + // Set the first variant to active if we can't find it. + // If the content item is invariant, then only one item exists in the array. activeVariant = vm.content.variants[0]; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index cbc090c821..ec5d04dbb3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -4,9 +4,6 @@ function EditorContentHeader(serverValidationManager, localizationService, editorState, variantHelper) { function link(scope) { - - console.log("scope.content", scope.content); - console.log("scope.content.variants", scope.content.variants); var unsubscribe = []; @@ -19,9 +16,11 @@ scope.isNew = scope.editorContent.state == "NotCreated"; - localizationService.localizeMany([ + localizationService.localizeMany( + [ scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit", - "placeholders_a11yName"] + "placeholders_a11yName" + ] ).then(function (data) { scope.a11yMessage = data[0]; scope.a11yName = data[1]; @@ -97,10 +96,6 @@ } - function getCultureFromVariant(variant) { - return variant.language ? variant.language.culture : null; - } - scope.getVariantDisplayName = variantHelper.getDisplayName; scope.goBack = function () { @@ -162,24 +157,7 @@ } onInit(); - /* - //watch for the active culture changing, if it changes, update the current variant - if (scope.content.variants) { - scope.$watch(function () { - for (var i = 0; i < scope.content.variants.length; i++) { - var v = scope.content.variants[i]; - if (v.active) { - return v.language.culture; - } - } - return scope.vm.currentVariant.language.culture; //should never get here - }, function (newValue, oldValue) { - if (newValue !== scope.vm.currentVariant.language.culture) { - setCurrentVariant(); - } - }); - } - */ + scope.$on('$destroy', function () { for (var u in unsubscribe) { unsubscribe[u](); From 133bec9b65dc6c2a278abb1b6c64e63e739404a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:45:17 +0100 Subject: [PATCH 252/610] remove space --- .../common/directives/validation/valpropertymsg.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 3a53f9e9ce..972f27874c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -39,7 +39,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; var currentCulture = currentProperty.culture; - var currentSegment = currentProperty.segment; + var currentSegment = currentProperty.segment; var labels = {}; localizationService.localize("errors_propertyHasErrors").then(function (data) { From f2edd9682b458c2272c68213eea5ad668afab60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:51:06 +0100 Subject: [PATCH 253/610] Remove this part until we see the need for this --- .../src/common/services/umbdataformatter.service.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index f7a7a52ee8..e23dfe74a7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -427,12 +427,6 @@ _.each(invariantProperties, function (invProp) { var tab = variant.tabs[invProp.tabIndex]; - var prop = tab.properties[invProp.propIndex]; - - if (prop.segment) { - // Do not touch segmented properties - return; - } tab.properties[invProp.propIndex] = invProp.property; }); From ba6c5a48400606a02923357bab2e4d0571370bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 19:56:03 +0100 Subject: [PATCH 254/610] rename variantHelper to contentVariantUtilities --- .../components/editor/umbeditorcontentheader.directive.js | 4 ++-- ...lper.service.js => contentvariantutilities.service.js} | 8 ++++---- .../src/views/content/overlays/publish.controller.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename src/Umbraco.Web.UI.Client/src/common/services/{varianthelper.service.js => contentvariantutilities.service.js} (75%) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index ec5d04dbb3..59c0dd3270 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function EditorContentHeader(serverValidationManager, localizationService, editorState, variantHelper) { + function EditorContentHeader(serverValidationManager, localizationService, editorState, contentVariantUtilities) { function link(scope) { @@ -96,7 +96,7 @@ } - scope.getVariantDisplayName = variantHelper.getDisplayName; + scope.getVariantDisplayName = contentVariantUtilities.getDisplayName; scope.goBack = function () { if (scope.onBack) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/varianthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contentvariantutilities.service.js similarity index 75% rename from src/Umbraco.Web.UI.Client/src/common/services/varianthelper.service.js rename to src/Umbraco.Web.UI.Client/src/common/services/contentvariantutilities.service.js index 996e2afc9e..1e7ac874cc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/varianthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contentvariantutilities.service.js @@ -1,9 +1,9 @@ /** * @ngdoc service -* @name umbraco.services.variantHelper -* @description A helper service for dealing with variants +* @name umbraco.services.contentVariantUtilities +* @description A utility service for dealing with variants **/ -function variantHelper() { +function contentVariantUtilities() { /** * Returns the id for this variant * @param {any} variant @@ -36,4 +36,4 @@ function variantHelper() { getDisplayName } } -angular.module('umbraco.services').factory('variantHelper', variantHelper); +angular.module('umbraco.services').factory('contentVariantUtilities', contentVariantUtilities); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index a5619aab9a..8a61025295 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function PublishController($scope, localizationService, contentEditingHelper, variantHelper) { + function PublishController($scope, localizationService, contentEditingHelper, contentVariantUtilities) { var vm = this; vm.loading = true; @@ -12,7 +12,7 @@ vm.dirtyVariantFilter = dirtyVariantFilter; vm.pristineVariantFilter = pristineVariantFilter; - $scope.getVariantDisplayName = variantHelper.getDisplayName; + $scope.getVariantDisplayName = contentVariantUtilities.getDisplayName; /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { From b8ded53e6a09c8be51259520724d240144293160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Jan 2020 20:00:06 +0100 Subject: [PATCH 255/610] revert to content --- .../src/views/components/content/umb-tabbed-content.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index 56f2835d3f..d8a98e8ee1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -27,7 +27,7 @@
From 384ecbe22a217fd9487ee48d04a18a130280ab70 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 08:25:03 +0100 Subject: [PATCH 256/610] Removed reference from Umbraco.Web.UI to Umbraco.TestData --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ead0cd1592..8f0db76c1d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -113,12 +113,6 @@ - - - {fb5676ed-7a69-492c-b802-e7b24144c0fc} - Umbraco.TestData - - {31785bc3-256c-4613-b2f5-a1b0bdded8c1} @@ -132,10 +126,6 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded - - {fb5676ed-7a69-492c-b802-e7b24144c0fc} - Umbraco.TestData - {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web @@ -439,4 +429,4 @@ - + \ No newline at end of file From 05fe604e221b9eae4c0b63bc5538347b5988fbbf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 09:19:29 +0100 Subject: [PATCH 257/610] https://github.com/umbraco/Umbraco-CMS/issues/7469 Renamed extension method to avoid ambiguous signature --- .../PublishedElementExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index 29429ba74f..8a0a688942 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web /// /// Gets the value of a property. /// - public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + public static TValue ValueFor(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); @@ -45,7 +45,7 @@ namespace Umbraco.Web var attribute = member.GetCustomAttribute(); if (attribute == null) throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - + return attribute.Alias; } } From 7bdf97c4313aca414e243553cdd9b48434b9b037 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 09:20:16 +0100 Subject: [PATCH 258/610] https://github.com/umbraco/Umbraco-CMS/issues/7469 Unsealed class to allow override --- .../ImplementPropertyTypeAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs index 0359c49654..b7b2695a08 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs @@ -7,7 +7,7 @@ namespace Umbraco.ModelsBuilder.Embedded ///
/// And therefore it should not be generated. [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - public sealed class ImplementPropertyTypeAttribute : Attribute + public class ImplementPropertyTypeAttribute : Attribute { public ImplementPropertyTypeAttribute(string alias) { From 4cf78d6ef049b58e513da4bf9ccc95180b36eebb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 24 Jan 2020 09:22:34 +0100 Subject: [PATCH 259/610] Bump version to 8.5.3 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 9dba3adef1..841e986f15 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.5.2")] -[assembly: AssemblyInformationalVersion("8.5.2")] +[assembly: AssemblyFileVersion("8.5.3")] +[assembly: AssemblyInformationalVersion("8.5.3")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0c80ccb70b..e12eb8d19b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8520 + 8530 / - http://localhost:8520 + http://localhost:8530 False False From 3ad1bb9e12f1d6b04e546f9c830e0ad9335e0bf4 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 24 Jan 2020 09:06:33 +0000 Subject: [PATCH 260/610] Added "rc" to AssemblyInformationalVersion for 8.6.0-rc --- src/SolutionInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 363677b826..641c06bcfc 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2019")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -19,4 +19,4 @@ using System.Resources; // these are FYI and changed automatically [assembly: AssemblyFileVersion("8.6.0")] -[assembly: AssemblyInformationalVersion("8.6.0")] +[assembly: AssemblyInformationalVersion("8.6.0-rc")] From 8bf457a88d3e3ca632339af9b727e93f38d544de Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 24 Jan 2020 09:10:54 +0000 Subject: [PATCH 261/610] Set v8/dev to 8.7.0 with powershell .\build setumbracoversion 8.7.0 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 363677b826..a0d969c1b8 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.6.0")] -[assembly: AssemblyInformationalVersion("8.6.0")] +[assembly: AssemblyFileVersion("8.7.0")] +[assembly: AssemblyInformationalVersion("8.7.0")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a2dfa0ffd2..9914528e58 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8600 + 8700 / - http://localhost:8600 + http://localhost:8700 False False @@ -429,4 +429,4 @@ - + \ No newline at end of file From 0180b9db7d565393773ac4e7c701e9ed528ca3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 24 Jan 2020 11:05:59 +0100 Subject: [PATCH 262/610] fixing broken style --- .../src/less/components/umb-editor-navigation-item.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index 6a6a8f9f5b..8ceb318425 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -182,7 +182,7 @@ .show-validation .umb-sub-views-nav-item > a.-has-error { color: @red; - &::after { + &::before { background-color: @red; } } From 6ab054868c788d9f8ea2956f382a326b1daecbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 24 Jan 2020 11:06:11 +0100 Subject: [PATCH 263/610] use umb-outline for focus outline --- .../src/views/components/editor/umb-editor-content-header.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index d06ca697cb..5be03d244c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -39,7 +39,7 @@ autocomplete="off" maxlength="255" /> - From abd8b3a1a782aea31ba46a611d017d658a736034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 24 Jan 2020 12:04:32 +0100 Subject: [PATCH 264/610] one way binding --- .../components/content/umbvariantcontent.directive.js | 4 ++-- .../components/content/umbvariantcontenteditors.directive.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index b754a88f53..b23be77fc1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -7,9 +7,9 @@ var umbVariantContent = { templateUrl: 'views/components/content/umb-variant-content.html', bindings: { - content: "=", + content: "<", page: "<", - editor: "=", + editor: "<", editorIndex: "<", editorCount: "<", onCloseSplitView: "&", 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 7a266163be..7b0b3508be 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 @@ -8,7 +8,7 @@ templateUrl: 'views/components/content/umb-variant-content-editors.html', bindings: { page: "<", - content: "=", + content: "<", culture: "<", segment: "<", onSelectApp: "&?", From 8e736f87d03785601331ac270758091eb4baa416 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 24 Jan 2020 12:38:21 +0100 Subject: [PATCH 265/610] Hide the "blob:" URL for images uploaded via the RTE (#6926) --- .../mediapicker/mediapicker.controller.js | 14 ++++++++++++++ .../infiniteeditors/mediapicker/mediapicker.html | 4 ++-- 2 files changed, 16 insertions(+), 2 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 01f61a16ae..4419805194 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 @@ -23,6 +23,7 @@ angular.module("umbraco") vm.clickHandler = clickHandler; vm.clickItemName = clickItemName; vm.gotoFolder = gotoFolder; + vm.shouldShowUrl = shouldShowUrl; var dialogOptions = $scope.model; @@ -531,6 +532,19 @@ angular.module("umbraco") } } + function shouldShowUrl() { + if (!$scope.target) { + return false; + } + if ($scope.target.id) { + return false; + } + if ($scope.target.url && $scope.target.url.toLower().indexOf("blob:") === 0) { + return false; + } + return true; + } + function submit() { if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); 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 917010dbb6..82612d5e19 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 @@ -138,8 +138,8 @@
- -
+ +
From b2ef64c6a8d899467ec6ea0ad47c63eac9f2c138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 24 Jan 2020 13:18:21 +0100 Subject: [PATCH 266/610] still have a copy of apps for each variant, but just create it when its needed. --- .../components/content/umbvariantcontent.directive.js | 2 ++ .../editor/umbeditorcontentheader.directive.js | 8 ++++---- .../less/components/umb-editor-navigation-item.less | 2 +- .../views/components/content/umb-variant-content.html | 4 ++-- .../components/editor/umb-editor-content-header.html | 10 +++++----- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index b23be77fc1..15e3dc02ba 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -48,6 +48,8 @@ vm.nameDisabled = true; } }); + console.log("what do we have on vm.editor.variantApps", vm.editor.variantApps) + vm.editor.variantApps = angular.copy(vm.content.apps); } function showBackButton() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index 59c0dd3270..06e3735abc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -14,7 +14,7 @@ scope.serverValidationAliasField = "Alias"; } - scope.isNew = scope.editorContent.state == "NotCreated"; + scope.isNew = scope.editor.content.state == "NotCreated"; localizationService.localizeMany( [ @@ -25,7 +25,7 @@ scope.a11yMessage = data[0]; scope.a11yName = data[1]; if (!scope.isNew) { - scope.a11yMessage += " " + scope.editorContent.name; + scope.a11yMessage += " " + scope.editor.content.name; } else { var name = editorState.current.contentTypeName; @@ -79,7 +79,7 @@ checkErrorsOnOtherVariants(); - angular.forEach(scope.content.apps, (app) => { + angular.forEach(scope.editor.variantApps, (app) => { if (app.alias === "umbContent") { app.anchors = scope.content.tabs; } @@ -177,7 +177,7 @@ menu: "=", hideActionsMenu: " a.-has-error { color: @red; 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 ba3cf9fc92..7db4f785f5 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 @@ -11,7 +11,7 @@ hide-menu="vm.page.hideActionsMenu" name="vm.editor.content.name" name-disabled="vm.nameDisabled" - editor-content="vm.editor.content" + editor="vm.editor" content="vm.content" on-select-navigation-item="vm.selectApp(item)" on-select-anchor-item="vm.selectAppAnchor(item, anchor)" @@ -34,7 +34,7 @@
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 5be03d244c..72163e8b99 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -40,19 +40,19 @@ - +
-
+
From ebed80791db2d388a7e1d987ac8eace39826eba1 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Mon, 27 Jan 2020 05:12:01 +0000 Subject: [PATCH 267/610] Changed delete button style on media type folder delete to danger style (#7393) --- src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html index c059710ebb..fb0c4c3a52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html @@ -9,7 +9,7 @@
+ on-cancel="cancel" confirm-button-style="danger">
From 902d4196cf2637809a6a36d94f9f7f6087afd8f1 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Mon, 27 Jan 2020 15:29:04 +1000 Subject: [PATCH 268/610] =?UTF-8?q?add=20dialog=20for=20change=20password,?= =?UTF-8?q?=20shift=20form=20from=20user=20details=20view=20int=E2=80=A6?= =?UTF-8?q?=20(#7450)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/common/resources/users.resource.js | 38 ++++++++++ .../services/umbdataformatter.service.js | 3 +- .../changepassword/changepassword.html | 6 ++ .../src/views/users/user.controller.js | 75 ++++++++++++++----- .../src/views/users/views/user/details.html | 17 +---- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4 +- .../Umbraco/config/lang/en_us.xml | 2 + src/Umbraco.Web/Editors/UsersController.cs | 70 +++++++++++------ .../Models/ChangingPasswordModel.cs | 6 ++ 9 files changed, 161 insertions(+), 60 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/overlays/changepassword/changepassword.html diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 0fd308ffd0..2b9e0c0fd5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -405,6 +405,43 @@ formattedSaveData), "Failed to save user"); } + + /** + * @ngdoc method + * @name umbraco.resources.usersResource#changePassword + * @methodOf umbraco.resources.usersResource + * + * @description + * Changes a user's password + * + * ##usage + *
+          * usersResource.changePassword(changePasswordModel)
+          *    .then(function() {
+          *        // password changed
+          *    });
+          * 
+ * + * @param {Object} model object to save + * @returns {Promise} resourcePromise object containing the updated user. + * + */ + function changePassword(changePasswordModel) { + if (!changePasswordModel) { + throw "password model not specified"; + } + + //need to convert the password data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedPasswordData = umbDataFormatter.formatChangePasswordModel(changePasswordModel); + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "userApiBaseUrl", + "PostChangePassword"), + formattedPasswordData), + "Failed to save user"); + } /** * @ngdoc method @@ -447,6 +484,7 @@ createUser: createUser, inviteUser: inviteUser, saveUser: saveUser, + changePassword: changePassword, deleteNonLoggedInUser: deleteNonLoggedInUser, clearAvatar: clearAvatar }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index a03a71febe..d9aef1ddba 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -152,8 +152,7 @@ formatUserPostData: function (displayModel) { //create the save model from the display model - var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message', 'changePassword'); - saveModel.changePassword = this.formatChangePasswordModel(saveModel.changePassword); + var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message'); //make sure the userGroups are just a string array var currGroups = saveModel.userGroups; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/changepassword/changepassword.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/changepassword/changepassword.html new file mode 100644 index 0000000000..db69a32f7f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/changepassword/changepassword.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index c935852bf8..9534eea9fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -21,7 +21,8 @@ //create the initial model for change password vm.changePasswordModel = { config: {}, - isChanging: false + isChanging: false, + value: {} }; vm.goToPage = goToPage; @@ -37,7 +38,8 @@ vm.changeAvatar = changeAvatar; vm.clearAvatar = clearAvatar; vm.save = save; - + + vm.changePassword = changePassword; vm.toggleChangePassword = toggleChangePassword; function init() { @@ -125,23 +127,34 @@ } function toggleChangePassword() { - vm.changePasswordModel.isChanging = !vm.changePasswordModel.isChanging; - //reset it - vm.user.changePassword = null; + //reset it + vm.user.changePassword = null; + + localizationService.localizeMany(["general_cancel", "general_confirm", "general_changePassword"]) + .then(function (data) { + const overlay = { + view: "changepassword", + title: data[2], + changePassword: vm.user.changePassword, + config: vm.changePasswordModel.config, + closeButtonLabel: data[0], + submitButtonLabel: data[1], + submitButtonStyle: 'success', + close: () => overlayService.close(), + submit: model => { + overlayService.close(); + vm.changePasswordModel.value = model.changePassword; + changePassword(); + } + }; + overlayService.open(overlay); + }); } function save() { if (formHelper.submitForm({ scope: $scope })) { - //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here - if (vm.user.changePassword) { - //NOTE: the check for allowManuallyChangingPassword is due to this legacy user membership provider setting, if that is true, then the current user - //can change their own password without entering their current one (this is a legacy setting since that is a security issue but we need to maintain compat). - //if allowManuallyChangingPassword=false, then we are using default settings and the user will need to enter their old password to change their own password. - vm.user.changePassword.reset = (!vm.user.changePassword.oldPassword && !vm.user.isCurrentUser) || vm.changePasswordModel.config.allowManuallyChangingPassword; - } - vm.page.saveButtonState = "busy"; vm.user.resetPasswordValue = null; @@ -161,11 +174,7 @@ //restore vm.user.navigation = currentNav; setUserDisplayState(); - formatDatesToLocal(vm.user); - - vm.changePasswordModel.isChanging = false; - //the user has a password if they are not states: Invited, NoCredentials - vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; + formatDatesToLocal(vm.user); vm.page.saveButtonState = "success"; @@ -181,6 +190,36 @@ } } + /** + * + */ + function changePassword() { + //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here + if (vm.changePasswordModel.value) { + //NOTE: the check for allowManuallyChangingPassword is due to this legacy user membership provider setting, if that is true, then the current user + //can change their own password without entering their current one (this is a legacy setting since that is a security issue but we need to maintain compat). + //if allowManuallyChangingPassword=false, then we are using default settings and the user will need to enter their old password to change their own password. + vm.changePasswordModel.value.reset = (!vm.changePasswordModel.value.oldPassword && !vm.user.isCurrentUser) || vm.changePasswordModel.config.allowManuallyChangingPassword; + } + + // since we don't send the entire user model, the id is required + vm.changePasswordModel.value.id = vm.user.id; + + usersResource.changePassword(vm.changePasswordModel.value) + .then(() => { + vm.changePasswordModel.isChanging = false; + vm.changePasswordModel.value = {}; + + //the user has a password if they are not states: Invited, NoCredentials + vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; + }, err => { + contentEditingHelper.handleSaveError({ + err: err, + showNotifications: true + }); + }); + } + /** * Used to emit the save event and await any async operations being performed by editor extensions * @param {any} savedUser diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index a1dcafd421..e0cf09da50 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -258,7 +258,7 @@
-
- - - - - - - - - -


Password reset to value: {{model.user.resetPasswordValue}}

diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 85b23f53ed..93a97414c9 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -737,6 +737,7 @@ Sort Status Submit + Success Type Type to search... under @@ -1843,7 +1844,8 @@ To manage your website, simply open the Umbraco back office and start adding con Password Reset password Your password has been changed! - Please confirm the new password + Password changed + Please confirm the new password Enter your new password Your new password cannot be blank! Current password 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 fb1ed6fa63..91fad336c7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -738,6 +738,7 @@ Sort Status Submit + Success Type Type to search... under @@ -1852,6 +1853,7 @@ To manage your website, simply open the Umbraco back office and start adding con Password Reset password Your password has been changed! + Password changed Please confirm the new password Enter your new password Your new password cannot be blank! diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 15e5b9404f..c27d7c9f48 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -26,6 +26,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Editors.Filters; +using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -549,29 +550,7 @@ namespace Umbraco.Web.Editors if (Current.Configs.Settings().Security.UsernameIsEmail && found.Username == found.Email && userSave.Username != userSave.Email) { userSave.Username = userSave.Email; - } - - if (userSave.ChangePassword != null) - { - var passwordChanger = new PasswordChanger(Logger, Services.UserService, UmbracoContext.HttpContext); - - //this will change the password and raise appropriate events - var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, userSave.ChangePassword, UserManager); - if (passwordChangeResult.Success) - { - //need to re-get the user - found = Services.UserService.GetUserById(intId.Result); - } - else - { - hasErrors = true; - - foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames) - { - ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); - } - } - } + } if (hasErrors) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); @@ -587,6 +566,51 @@ namespace Umbraco.Web.Editors return display; } + /// + /// + /// + /// + /// + public async Task> PostChangePassword(ChangingPasswordModel changingPasswordModel) + { + changingPasswordModel = changingPasswordModel ?? throw new ArgumentNullException(nameof(changingPasswordModel)); + + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + var intId = changingPasswordModel.Id.TryConvertTo(); + if (intId.Success == false) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var found = Services.UserService.GetUserById(intId.Result); + if (found == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var passwordChanger = new PasswordChanger(Logger, Services.UserService, UmbracoContext.HttpContext); + var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, found, changingPasswordModel, UserManager); + + if (passwordChangeResult.Success) + { + var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); + result.AddSuccessNotification(Services.TextService.Localize("general/success"), Services.TextService.Localize("user/passwordChangedGeneric")); + return result; + } + + foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames) + { + ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); + } + + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + + /// /// Disables the users with the given user ids /// diff --git a/src/Umbraco.Web/Models/ChangingPasswordModel.cs b/src/Umbraco.Web/Models/ChangingPasswordModel.cs index 355b358049..633c42a056 100644 --- a/src/Umbraco.Web/Models/ChangingPasswordModel.cs +++ b/src/Umbraco.Web/Models/ChangingPasswordModel.cs @@ -49,5 +49,11 @@ namespace Umbraco.Web.Models /// [DataMember(Name = "generatedPassword")] public string GeneratedPassword { get; set; } + + /// + /// The id of the user - required to allow changing password without the entire UserSave model + /// + [DataMember(Name = "id")] + public int Id { get; set; } } } From 8efa9739926d10182288d2a2a1aad21043a940dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 27 Jan 2020 11:17:56 +0100 Subject: [PATCH 269/610] sync active app in variants --- .../components/content/edit.controller.js | 25 +++++++------ .../content/umbvariantcontent.directive.js | 36 +++++++++++-------- 2 files changed, 37 insertions(+), 24 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 4a3afec5a7..83adc49091 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 @@ -31,7 +31,7 @@ $scope.page.hideActionsMenu = infiniteMode ? true : false; $scope.page.hideChangeVariant = false; $scope.allowOpen = true; - $scope.app = null; + $scope.activeApp = null; //initializes any watches function startWatches(content) { @@ -74,11 +74,11 @@ var isAppPresent = false; // on first init, we dont have any apps. but if we are re-initializing, we do, but ... - if ($scope.app) { + if ($scope.activeApp) { // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) _.forEach(content.apps, function (app) { - if (app === $scope.app) { + if (app.alias === $scope.activeApp.alias) { isAppPresent = true; } }); @@ -86,9 +86,8 @@ // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { _.forEach(content.apps, function (app) { - if (app.alias === $scope.app.alias) { + if (app.alias === $scope.activeApp.alias) { isAppPresent = true; - app.active = true; $scope.appChanged(app); } }); @@ -98,7 +97,6 @@ // if we still dont have a app, lets show the first one: if (isAppPresent === false && content.apps.length) { - content.apps[0].active = true; $scope.appChanged(content.apps[0]); } // otherwise make sure the save options are up to date with the current content state @@ -274,7 +272,7 @@ $scope.page.saveButtonStyle = content.trashed || content.isElement || content.isBlueprint ? "primary" : "info"; // only create the save/publish/preview buttons if the // content app is "Conent" - if ($scope.app && $scope.app.alias !== "umbContent" && $scope.app.alias !== "umbInfo" && $scope.app.alias !== "umbListView") { + if ($scope.activeApp && $scope.activeApp.alias !== "umbContent" && $scope.activeApp.alias !== "umbInfo" && $scope.activeApp.alias !== "umbListView") { $scope.defaultButton = null; $scope.subButtons = null; $scope.page.showSaveButton = false; @@ -961,11 +959,18 @@ * Call back when a content app changes * @param {any} app */ - $scope.appChanged = function (app) { + $scope.appChanged = function (activeApp) { - $scope.app = app; + $scope.activeApp = activeApp; + + _.forEach($scope.content.apps, function (app) { + app.active = false; + if (app.alias === $scope.activeApp.alias) { + app.active = true; + } + }); - $scope.$broadcast("editors.apps.appChanged", { app: app }); + $scope.$broadcast("editors.apps.appChanged", { app: activeApp }); createButtons($scope.content); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index 15e3dc02ba..764ae344dd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -41,15 +41,14 @@ vm.showBackButton = showBackButton; function onInit() { - // disable the name field if the active content app is not "Content" - vm.nameDisabled = false; - angular.forEach(vm.content.apps, function(app){ - if(app.active && app.alias !== "umbContent" && app.alias !== "umbInfo" && app.alias !== "umbListView") { - vm.nameDisabled = true; - } - }); - console.log("what do we have on vm.editor.variantApps", vm.editor.variantApps) + + // Make copy of apps, so we can have a variant specific model for the App. (needed for validation etc.) vm.editor.variantApps = angular.copy(vm.content.apps); + + var activeApp = vm.content.apps.find((app) => app.active); + + onAppChanged(activeApp); + } function showBackButton() { @@ -95,14 +94,23 @@ } $scope.$on("editors.apps.appChanged", function($event, $args) { - var app = $args.app; - // disable the name field if the active content app is not "Content" or "Info" - vm.nameDisabled = false; - if(app && app.alias !== "umbContent" && app.alias !== "umbInfo" && app.alias !== "umbListView") { - vm.nameDisabled = true; - } + var activeApp = $args.app; + + // sync varaintApps active with new active. + _.forEach(vm.editor.variantApps, function (app) { + app.active = (app.alias === activeApp.alias); + }); + + onAppChanged(activeApp); }); + function onAppChanged(activeApp) { + + // disable the name field if the active content app is not "Content" or "Info" + vm.nameDisabled = (activeApp && activeApp.alias !== "umbContent" && activeApp.alias !== "umbInfo" && activeApp.alias !== "umbListView"); + + } + /** * Used to proxy a callback * @param {any} item From 9df2711d5ed77c358d1acc9333a99919a67ce075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 27 Jan 2020 11:18:31 +0100 Subject: [PATCH 270/610] enforce the content content-app in split view mode. --- .../components/content/umbvariantcontenteditors.directive.js | 5 +++++ 1 file changed, 5 insertions(+) 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 7b0b3508be..d929098175 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 @@ -155,6 +155,11 @@ * @param {any} selectedVariant */ function openSplitView(selectedVariant) { + // enforce content contentApp in splitview. + var contentApp = vm.content.apps.find((app) => app.alias === "umbContent"); + if(contentApp) { + selectApp(contentApp); + } insertVariantEditor(vm.editors.length, selectedVariant); From af7f03e1f7e37bbd8ecd7812e5b3548484033c51 Mon Sep 17 00:00:00 2001 From: Lars-Erik Aabech Date: Tue, 28 Jan 2020 22:28:54 +0100 Subject: [PATCH 271/610] Don't downcast `IPublishedSnapshot` unnecessarily in `PublishedContent` --- src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index bf4975714d..3c3c3ac54f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -326,7 +326,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // beware what you use that one for - you don't want to cache its result private IAppCache GetAppropriateCache() { - var publishedSnapshot = (PublishedSnapshot)_publishedSnapshotAccessor.PublishedSnapshot; + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; var cache = publishedSnapshot == null ? null : ((IsPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && (ContentType.ItemType != PublishedItemType.Member) @@ -337,7 +337,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private IAppCache GetCurrentSnapshotCache() { - var publishedSnapshot = (PublishedSnapshot)_publishedSnapshotAccessor.PublishedSnapshot; + var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; return publishedSnapshot?.SnapshotCache; } From 537674a0b392d4a035beae1bbd858ba1d96fd838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksandr=20=C5=A0mailov?= Date: Wed, 29 Jan 2020 09:53:08 +0200 Subject: [PATCH 272/610] Improve RTE performance when using base64 images (#7530) --- .../Templates/HtmlImageSourceParserTests.cs | 84 +++++++++++++++++-- .../Templates/HtmlImageSourceParser.cs | 33 +++++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs index 3bef495507..bce9bd4155 100644 --- a/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs +++ b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs @@ -1,7 +1,6 @@ using Umbraco.Core.Logging; using Moq; using NUnit.Framework; -using Umbraco.Core.Services; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web.Templates; using Umbraco.Web; @@ -13,12 +12,10 @@ using System; using System.Linq; using Umbraco.Core.Models; using Umbraco.Core; -using Umbraco.Web.PropertyEditors; +using System.Diagnostics; namespace Umbraco.Tests.Templates { - - [TestFixture] public class HtmlImageSourceParserTests { @@ -31,10 +28,9 @@ namespace Umbraco.Tests.Templates

"; - var logger = Mock.Of(); var umbracoContextAccessor = new TestUmbracoContextAccessor(); var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor); - + var result = imageSourceParser.FindUdisFromDataAttributes(input).ToList(); Assert.AreEqual(2, result.Count); Assert.AreEqual(Udi.Parse("umb://media/D4B18427A1544721B09AC7692F35C264"), result[0]); @@ -44,7 +40,6 @@ namespace Umbraco.Tests.Templates [Test] public void Remove_Image_Sources() { - var logger = Mock.Of(); var umbracoContextAccessor = new TestUmbracoContextAccessor(); var imageSourceParser = new HtmlImageSourceParser(umbracoContextAccessor); @@ -111,10 +106,83 @@ namespace Umbraco.Tests.Templates

", result); - } + } + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Empty source is not updated with no data-udi set" + )] + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Empty source is updated with data-udi set" + )] + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Filled source is overwritten with data-udi set" + )] + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Attributes are persisted" + )] + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Source is trimmed and parameters are prefixed" + )] + [TestCase( + @"
", + ExpectedResult = @"
", + TestName = "Parameters are prefixed" + )] + [TestCase( + @"
+ + + +
", + ExpectedResult = + @"
+ + + +
", + TestName = "Multiple img tags are handled" + )] + [Category("Ensure image sources")] + public string Ensure_ImageSources_Processing(string sourceHtml) + { + var fakeMediaUrl = "/media/1001/image.jpg"; + var parser = new HtmlImageSourceParser((guid) => fakeMediaUrl); + var actual = parser.EnsureImageSources(sourceHtml); + + return actual; + } + + [Category("Ensure image sources")] + [Test] + public void Ensure_Large_Html_Is_Processed_Quickly() + { + int symbolCount = 25000; + int maxMsToRun = 200; + + var longText = new string('*', symbolCount); + var text = $@""; + + var fakeMediaUrl = "/media/1001/image.jpg"; + var parser = new HtmlImageSourceParser((guid) => fakeMediaUrl); + + var timer = new Stopwatch(); + timer.Start(); + var actual = parser.EnsureImageSources(text); + timer.Stop(); + + Assert.IsTrue(timer.ElapsedMilliseconds <= maxMsToRun); } } } diff --git a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs index b0d6980ef3..dcc5318b89 100644 --- a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using Umbraco.Core; @@ -7,18 +8,29 @@ namespace Umbraco.Web.Templates public sealed class HtmlImageSourceParser { - public HtmlImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor) + public HtmlImageSourceParser(Func getMediaUrl) { - _umbracoContextAccessor = umbracoContextAccessor; + this._getMediaUrl = getMediaUrl; } - private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)([^""]*""[^>]*data-udi="")([^""]*)(""[^>]*>)", + public HtmlImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor) + { + if (umbracoContextAccessor?.UmbracoContext?.UrlProvider == null) + { + return; + } + + _getMediaUrl = (guid) => umbracoContextAccessor.UmbracoContext.UrlProvider.GetMediaUrl(guid); + } + + private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + private readonly Func _getMediaUrl; + /// /// Parses out media UDIs from an html string based on 'data-udi' html attributes /// @@ -45,14 +57,13 @@ namespace Umbraco.Web.Templates /// Umbraco image tags are identified by their data-udi attributes public string EnsureImageSources(string text) { - // don't attempt to proceed without a context - if (_umbracoContextAccessor?.UmbracoContext?.UrlProvider == null) + // no point in doing any processing if we don't have + // a function to retrieve Urls + if (_getMediaUrl == null) { return text; } - var urlProvider = _umbracoContextAccessor.UmbracoContext.UrlProvider; - return ResolveImgPattern.Replace(text, match => { // match groups: @@ -66,7 +77,7 @@ namespace Umbraco.Web.Templates { return match.Value; } - var mediaUrl = urlProvider.GetMediaUrl(guidUdi.Guid); + var mediaUrl = _getMediaUrl(guidUdi.Guid); if (mediaUrl == null) { // image does not exist - we could choose to remove the image entirely here (return empty string), @@ -86,7 +97,5 @@ namespace Umbraco.Web.Templates public string RemoveImageSources(string text) // see comment in ResolveMediaFromTextString for group reference => ResolveImgPattern.Replace(text, "$1$3$4$5"); - - } } From 7fcd598361a905985cbb430d65f2a1d397b3e310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 29 Jan 2020 10:08:23 +0100 Subject: [PATCH 273/610] restructure data for variant picker --- .../umbeditorcontentheader.directive.js | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index 06e3735abc..083feeb5c2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -35,10 +35,11 @@ }); scope.vm = {}; scope.vm.hasVariants = false; + scope.vm.hasCulture = false; + scope.vm.hasSegments = false; scope.vm.dropdownOpen = false; scope.vm.variantsWithError = []; scope.vm.defaultVariant = null; - scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors. function checkErrorsOnOtherVariants() { @@ -72,12 +73,38 @@ if (variant.language !== null && variant.language.isDefault) { scope.vm.defaultVariant = variant; } - if (variant.language !== null || variant.segment !== null) { - scope.vm.hasVariants = true; + if (variant.language !== null) { + scope.vm.hasCulture = true; + } + if (variant.segment !== null) { + scope.vm.hasSegment = true; } }); + + scope.vm.hasVariants = (scope.vm.hasCulture || scope.vm.hasSegment); checkErrorsOnOtherVariants(); + + scope.vm.variantMenu = []; + if (scope.vm.hasCulture) { + angular.forEach(scope.content.variants, (v) => { + if (v.language !== null && v.segment === null) { + var variantMenuEntry = { + open: v.language && v.language.culture === scope.editor.culture, + variant: v, + subVariants: scope.content.variants.filter( (subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null) + }; + scope.vm.variantMenu.push(variantMenuEntry); + } + }); + } else { + angular.forEach(scope.content.variants, (v) => { + scope.vm.variantMenu.push({ + variant: v + }); + }); + } + angular.forEach(scope.editor.variantApps, (app) => { if (app.alias === "umbContent") { @@ -85,7 +112,6 @@ } }); - angular.forEach(scope.content.variants, function (variant) { unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, variant.segment, null, onVariantValidation)); }); From 0c0861321e0e1462a3351eab6209421008b0016d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 29 Jan 2020 10:09:07 +0100 Subject: [PATCH 274/610] display segments in variant picker --- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../editor/umb-variant-switcher.less | 321 ++++++++++++++++++ .../editor/umb-editor-content-header.html | 38 ++- 3 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 0921f46aac..f6fadc9a43 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -107,6 +107,7 @@ @import "components/overlays.less"; @import "components/card.less"; @import "components/editor/umb-editor.less"; +@import "components/editor/umb-variant-switcher.less"; @import "components/umb-sub-views.less"; @import "components/umb-editor-navigation.less"; @import "components/umb-editor-navigation-item.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less new file mode 100644 index 0000000000..c021391775 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -0,0 +1,321 @@ +/* variant switcher */ +.umb-variant-switcher__toggle { + position: relative; + display: flex; + align-items: center; + padding: 0 10px; + margin: 1px 1px; + right: 0; + height: 30px; + text-decoration: none !important; + font-size: 13px; + color: @ui-action-discreet-type; + background: transparent; + border: none; + + max-width: 50%; + white-space: nowrap; + + user-select: none; + + span { + text-overflow: ellipsis; + overflow: hidden; + } +} + +button.umb-variant-switcher__toggle { + transition: color 0.2s ease-in-out; + &:hover { + //background-color: @gray-10; + color: @ui-action-discreet-type-hover; + .umb-variant-switcher__expand { + color: @ui-action-discreet-type-hover; + } + } + + &.--error { + &::before { + content: '!'; + position: absolute; + top: -8px; + right: -10px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 10px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__expand { + color: @ui-action-discreet-type; + margin-top: 3px; + margin-left: 5px; + margin-right: -5px; + transition: color 0.2s ease-in-out; +} + + +.umb-variant-switcher { + min-width: 100%; + max-height: 80vh; + overflow-y: auto; + margin-top: 5px; + user-select: none; +} + +.umb-variant-switcher__item { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid @gray-9; + position: relative; + .umb-variant-switcher__name-wrapper:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +.umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::before { + content: "+"; + display: block; + float: left; + font-size: 15px; + font-weight: 900; + padding: 8px 16px 8px 6px; + color: @gray-5; + } + .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper::before { + padding: 8px 16px 8px 20px; + } + .umb-variant-switcher__name { + color: @gray-5; + } + .umb-variant-switcher__state { + color: @gray-6; + } + .umb-variant-switcher__name-wrapper::after { + content: ""; + position: absolute; + z-index: 1; + border: 1px dashed @gray-9; + top: 7px; + bottom: 7px; + left: 7px; + right: 7px; + border-radius: 3px; + pointer-events: none; + } + + .umb-variant-switcher__name-wrapper:hover { + &::before { + color: @blueMid; + } + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +/* +.umb-variant-switcher__item.--state-draft { + .umb-variant-switcher__name { + color: @gray-5; + } + &:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + } +} +*/ + +.umb-variant-switcher.--has-sub-variants { + .umb-variant-switcher__item { + + } +} + +.umb-variant-switcher__item-expand-button { + text-decoration: none; + display: inline-block; + flex: 0; + align-self: stretch; + + padding-left: 22px !important; + padding-right: 14px !important; + + font-size: 12px; + + * { + pointer-events: none; + } +} + +.umb-variant-switcher__item:last-child { + border-bottom: none; +} + +.umb-variant-switcher__item.--current { + //color: @ui-light-active-type; + //background-color: @pinkExtraLight; + .umb-variant-switcher__name { + //color: @ui-light-active-type; + font-weight: 700; + } + &::before { + content: ''; + position: absolute; + border-radius: 0 4px 4px 0; + background-color: @ui-active-border; + width: 4px; + top:8px; + bottom: 8px; + left:0; + z-index:1; + pointer-events: none; + } +} + +.umb-variant-switcher__item:hover { + outline: none; +} + +.umb-variant-switcher__item.--active:not(.--current) .umb-variant-switcher__name-wrapper:hover { + //background-color: @white !important; + cursor: default; +} + +.umb-variant-switcher__item:hover .umb-variant-switcher__split-view { + display: block; + cursor: pointer; +} + +.umb-variant-switcher__item.--error { + .umb-variant-switcher__name { + color: @red; + &::after { + content: '!'; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 5px; + top: -3px; + width: 14px; + height: 14px; + border-radius: 7px; + font-size: 8px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__name-wrapper { + font-size: 14px; + text-align: left; + flex: 1; + cursor: pointer; + background-color: transparent; + border: none; +} +.dropdown-menu>li { + > .umb-variant-switcher__name-wrapper { + padding-top: 10px; + padding-bottom: 10px; + } + + > .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper { + padding-left: 5px; + } +} + + +.umb-variant-switcher__name { + display: block; + font-weight: 600; + margin-bottom: -2px; +} + +.umb-variant-switcher__state { + font-size: 12px; + color: @gray-4; +} + +.umb-variant-switcher__split-view { + font-size: 12px; + display: none; + padding: 18px 20px; + position: absolute; + right: 0; + top: 0; + bottom: 0; + background-color: @white; + + &:hover { + background-color: @ui-option-hover; + color: @ui-option-type-hover; + } +} + + +.umb-variant-switcher__sub-variants { + + position: relative; + border-bottom: 1px solid @gray-9; + background-color: @gray-13; + /* + &::before { + content: ""; + position: absolute; + z-index: 1; + top: 0px; + left: 20px; + width: 4px; + bottom: 14px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + background-color: @gray-8; + } + */ + .umb-variant-switcher__item { + border-bottom-color: @gray-10; + } + + .umb-variant-switcher__name-wrapper { + + margin-left: 48px; + padding-left: 20px; + + padding-top: 10px; + padding-bottom: 10px; + + &:hover { + color: @ui-option-type-hover; + background-color: @ui-option-hover; + } + + .umb-variant-switcher__name { + //margin-right: 20px; + } + .umb-variant-switcher__state { + //flex: 0 0 200px; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 72163e8b99..e6030517b7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -36,7 +36,8 @@ required aria-required="true" aria-invalid="{{contentForm.headerNameForm.headerName.$invalid ? true : false}}" - autocomplete="off" maxlength="255" /> + autocomplete="off" + maxlength="255" /> -
Open in split view
+ +
Open in split view
+
+ + +
Open in split view
+
+
From 3152b35402fe612334d7a97875eb792a70667fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 29 Jan 2020 10:09:35 +0100 Subject: [PATCH 275/610] removed .umb-variant-switcher from editor.less into its own file. --- .../src/less/components/editor.less | 155 ------------------ 1 file changed, 155 deletions(-) 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 bc84b0d35e..ac55c6ffb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -162,161 +162,6 @@ a.umb-editor-header__close-split-view:hover { } } -/* variant switcher */ -.umb-variant-switcher__toggle { - position: relative; - display: flex; - align-items: center; - padding: 0 10px; - margin: 1px 1px; - right: 0; - height: 30px; - text-decoration: none !important; - font-size: 13px; - color: @ui-action-discreet-type; - background: transparent; - border: none; - - max-width: 50%; - white-space: nowrap; - - user-select: none; - - span { - text-overflow: ellipsis; - overflow: hidden; - } -} - -button.umb-variant-switcher__toggle { - transition: color 0.2s ease-in-out; - &:hover { - //background-color: @gray-10; - color: @ui-action-discreet-type-hover; - .umb-variant-switcher__expand { - color: @ui-action-discreet-type-hover; - } - } - - &.--error { - &::before { - content: '!'; - position: absolute; - top: -8px; - right: -10px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 10px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - } - } -} - -.umb-variant-switcher__expand { - color: @ui-action-discreet-type; - margin-top: 3px; - margin-left: 5px; - margin-right: -5px; - transition: color 0.2s ease-in-out; -} - -.umb-variant-switcher__item { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid @gray-9; - position: relative; -} - -.umb-variant-switcher__item:last-child { - border-bottom: none; -} - -.umb-variant-switcher__item.--current { - color: @ui-light-active-type; -} -.umb-variant-switcher__item.--current .umb-variant-switcher__name-wrapper { - border-left: 4px solid @ui-active; -} - -.umb-variant-switcher__item:hover { - outline: none; -} - -.umb-variant-switcher__item.--not-allowed:not(.--current) .umb-variant-switcher__name-wrapper:hover { - //background-color: @white !important; - cursor: default; -} - -.umb-variant-switcher__item:hover .umb-variant-switcher__split-view { - display: block; - cursor: pointer; -} - -.umb-variant-switcher__item.--error { - .umb-variant-switcher__name { - color: @red; - &::after { - content: '!'; - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 5px; - top: -3px; - width: 14px; - height: 14px; - border-radius: 7px; - font-size: 8px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - } - } -} - -.umb-variant-switcher__name-wrapper { - font-size: 14px; - flex: 1; - cursor: pointer; - padding-top: 6px !important; - padding-bottom: 6px !important; - background-color: transparent; - border: none; - border-left: 2px solid transparent; -} - -.umb-variant-switcher__name { - display: block; -} - -.umb-variant-switcher__state { - font-size: 13px; - color: @gray-4; -} - -.umb-variant-switcher__split-view { - font-size: 13px; - display: none; - padding: 16px 20px; - position: absolute; - right: 0; - top: 0; - bottom: 0; - background-color: @white; - - &:hover { - background-color: @ui-option-hover; - color: @ui-option-type-hover; - } -} // container From f1eab708c3a2e269780a94455ebeca4bf2fd16f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 29 Jan 2020 10:33:33 +0100 Subject: [PATCH 276/610] adding colors: pinkExtraLight + gray-12 + @gray-13 --- .../src/less/components/tree/umb-tree.less | 2 +- .../umb-editor-navigation-item.less | 2 +- .../src/less/sections.less | 8 ++++---- .../src/less/variables.less | 20 +++++++++++-------- 4 files changed, 18 insertions(+), 14 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 d06c15cd30..839e61c5f9 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 @@ -256,7 +256,7 @@ body.touch .umb-tree { } &.current > .umb-tree-item__inner > .umb-tree-item__annotation { - background-color: @pinkLight; + background-color: @ui-active; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index 6a6a8f9f5b..2cca776614 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -115,7 +115,7 @@ li { &.is-active a { - border-left-color: @ui-active; + border-left-color: @ui-active-border; } a { diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 40921c5b76..e1ccfeb092 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -31,7 +31,7 @@ ul.sections { height: 4px; bottom: 0; transform: translateY(4px); - background-color: @ui-active; + background-color: @pinkLight; position: absolute; border-radius: 3px 3px 0 0; opacity: 0; @@ -56,7 +56,7 @@ ul.sections { } &.current a { - color: @ui-active; + color: @pinkLight; &::after { opacity: 1; @@ -104,7 +104,7 @@ ul.sections-tray { li { &.current a { - color: @ui-active; + color: @ui-active-border; opacity: 1; &::after { @@ -124,7 +124,7 @@ ul.sections-tray { content: ""; width: 4px; height: 100%; - background-color: @ui-active; + background-color: @ui-active-border; position: absolute; border-radius: 0 3px 3px 0; opacity: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index a906bc0eed..674c8d08e9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -75,6 +75,8 @@ @gray-9: #E9E9EB; @gray-10: #F3F3F5; @gray-11: #F6F6F7; +@gray-12: #F9F9FA; +@gray-13: #FBFBFD; @sand-1: #DED4CF;// added 2019 @sand-2: #EBDED6;// added 2019 @@ -108,7 +110,7 @@ //@blueLight: #4f89de; @blue: #2E8AEA; @blueMid: #2152A3;// updated 2019 -@blueMidLight: rgb(99, 174, 236); +@blueMidLight: #6ab4f0; @blueDark: #3544b1;// updated 2019 @blueExtraDark: #1b264f;// added 2019 @blueLight: #ADD8E6; @@ -116,6 +118,7 @@ //@orange: #f79c37;// updated 2019 @pink: #D93F4C;// #C3325F;// update 2019 @pinkLight: #f5c1bc;// added 2019 +@pinkExtraLight: hsl(5, 84%, 94%);// added 2019 @pinkRedLight: #ff8a89;// added 2019 @brown: #9d8057;// added 2019 @brownLight: #e4e0dd;// added 2019 @@ -134,17 +137,18 @@ @ui-option-type: @blueExtraDark; @ui-option-type-hover: @blueMid; @ui-option: @white; -@ui-option-hover: @sand-7; +@ui-option-hover: @gray-12; @ui-option-disabled-type: @gray-6; @ui-option-disabled-type-hover: @gray-5; -@ui-option-disabled-hover: @sand-7; +@ui-option-disabled-hover: @gray-12; @ui-disabled-type: @gray-6; @ui-disabled-border: @gray-6; //@ui-active: #346ab3; -@ui-active: @pinkLight; +@ui-active: @pinkExtraLight; +@ui-active-border: @pinkLight; @ui-active-blur: @brownLight; @ui-active-type: @blueExtraDark; @ui-active-type-hover: @blueMid; @@ -161,19 +165,19 @@ @ui-light-type-hover: @blueMid; @ui-light-active-border: @pinkLight; -@ui-light-active-type: @blueExtraDark; -@ui-light-active-type-hover: @blueMid; +@ui-light-active-type: @blueMid; +@ui-light-active-type-hover: @blueMidLight; @ui-action: @white; -@ui-action-hover: @sand-7; +@ui-action-hover: @gray-12; @ui-action-type: @blueExtraDark; @ui-action-type-hover: @blueMid; @ui-action-border: @blueExtraDark; @ui-action-border-hover: @blueMid; @ui-action-discreet: @white; -@ui-action-discreet-hover: @sand-7; +@ui-action-discreet-hover: @gray-12; @ui-action-discreet-type: @blueExtraDark; @ui-action-discreet-type-hover: @blueMid; @ui-action-discreet-border: @gray-7; From 42fe20a11ebf7cd0fe93089ad3cf293ef7db9a25 Mon Sep 17 00:00:00 2001 From: jmcaveney Date: Wed, 29 Jan 2020 07:16:17 -0500 Subject: [PATCH 277/610] Uses AppSettings.Path constant for SystemDirectories.Umbraco (#7440) --- src/Umbraco.Core/Constants-AppSettings.cs | 15 +++++++++++++++ src/Umbraco.Core/IO/SystemDirectories.cs | 8 ++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 85d6b24ae0..704617d90c 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -43,6 +43,21 @@ namespace Umbraco.Core /// public const string Path = "Umbraco.Core.Path"; + /// + /// Gets the path to the css directory (/css by default). + /// + public const string CssPath = "umbracoCssPath"; + + /// + /// Gets the path to the scripts directory (/scripts by default). + /// + public const string ScriptsPath = "umbracoScriptsPath"; + + /// + /// Gets the path to media directory (/media by default). + /// + public const string MediaPath = "umbracoMediaPath"; + /// /// The reserved urls from web.config. /// diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index d6fb63b0a1..b4688d2e9f 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -29,13 +29,13 @@ namespace Umbraco.Core.IO public static string MacroPartials => MvcViews + "/MacroPartials/"; - public static string Media => IOHelper.ReturnPath("umbracoMediaPath", "~/media"); + public static string Media => IOHelper.ReturnPath(Constants.AppSettings.MediaPath, "~/media"); - public static string Scripts => IOHelper.ReturnPath("umbracoScriptsPath", "~/scripts"); + public static string Scripts => IOHelper.ReturnPath(Constants.AppSettings.ScriptsPath, "~/scripts"); - public static string Css => IOHelper.ReturnPath("umbracoCssPath", "~/css"); + public static string Css => IOHelper.ReturnPath(Constants.AppSettings.CssPath, "~/css"); - public static string Umbraco => IOHelper.ReturnPath("umbracoPath", "~/umbraco"); + public static string Umbraco => IOHelper.ReturnPath(Constants.AppSettings.Path, "~/umbraco"); public static string Packages => Data + "/packages"; From a945b0c511febd4617a6c9cbd327e9cca5d59069 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 30 Jan 2020 02:52:20 +1000 Subject: [PATCH 278/610] use default color when previous selection removed (#7448) Thanks Nathan! --- .../iconpicker/iconpicker.controller.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js index 471d23ae84..5bfee22c4e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.controller.js @@ -16,7 +16,7 @@ function IconPickerController($scope, iconHelper, localizationService) { vm.close = close; vm.colors = [ - { name: "Black", value: "color-black" }, + { name: "Black", value: "color-black", default: true }, { name: "Blue Grey", value: "color-blue-grey" }, { name: "Grey", value: "color-grey" }, { name: "Brown", value: "color-brown" }, @@ -49,7 +49,7 @@ function IconPickerController($scope, iconHelper, localizationService) { }); // set a default color if nothing is passed in - vm.color = $scope.model.color ? findColor($scope.model.color) : vm.colors[0]; + vm.color = $scope.model.color ? findColor($scope.model.color) : vm.colors.find(x => x.default); // if an icon is passed in - preselect it vm.icon = $scope.model.icon ? $scope.model.icon : undefined; @@ -71,12 +71,13 @@ function IconPickerController($scope, iconHelper, localizationService) { } function findColor(value) { - return _.findWhere(vm.colors, {value: value}); + return vm.colors.find(x => x.value === value); } - function selectColor(color, $index, $event) { - $scope.model.color = color.value; - vm.color = color; + function selectColor(color) { + let newColor = (color || vm.colors.find(x => x.default)); + $scope.model.color = newColor.value; + vm.color = newColor; } function close() { From b6a073a12e5484f3acf7bdef9301d95fb9ddd650 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 29 Jan 2020 17:54:56 +0100 Subject: [PATCH 279/610] Add missing documentation for web.routing in umbracoSettings.config (#7468) Cheers Kenn :) --- src/Umbraco.Web.UI/config/umbracoSettings.config | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 9e60a9499c..c3df6cfb41 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -231,6 +231,14 @@ By default you can call any content Id in the url and show the content with that id, for example: http://mysite.com/1092 or http://mysite.com/1092.aspx would render the content with id 1092. Setting this setting to true stops that behavior + @disableRedirectUrlTracking + When the URL changes for content, redirects are automatically created for redirect handling within the + request pipeline. Setting this setting to true stops the automatic creation of redirects. Note that this + does not stop the request pipeline from handling any previously created redirects. + @urlProviderMode + By default Umbraco automatically figures out if internal URLs should be rendered as relative or absolute, + depending on the current request and the configured domains. By setting this setting to "Relative" or + "Absolute" you can force Umbraco to always render URLs as either relative or absolute. @umbracoApplicationUrl The url of the Umbraco application. By default, Umbraco will figure it out from the first request. Configure it here if you need anything specific. Needs to be a complete url with scheme and umbraco From 423a21da10820bab3aea6a84c7f11876c5ca43f2 Mon Sep 17 00:00:00 2001 From: Matthew-Wise <6782865+Matthew-Wise@users.noreply.github.com> Date: Thu, 30 Jan 2020 10:20:16 +0000 Subject: [PATCH 280/610] Added dialog aria labelling (#7000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Matt 👍 --- src/Umbraco.Web.UI.Client/src/views/content/rights.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/rights.html b/src/Umbraco.Web.UI.Client/src/views/content/rights.html index 6ceec1db51..3395468cb0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/rights.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/rights.html @@ -6,8 +6,8 @@ -
- + -
Set permissions for {{ currentNode.name }}
-

+
Set permissions for {{ currentNode.name }}
+

Date: Thu, 30 Jan 2020 11:45:50 +0100 Subject: [PATCH 281/610] =?UTF-8?q?V8:=20Change=20"Include=20subfolders=20?= =?UTF-8?q?in=20search"=20to=20"Search=20only=20in=20th=E2=80=A6=20(#4720)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lots of lovely work here - this is one of those PRs that show how good communication can really make the experience of contributing one of learning for all parties. Thanks Kenn and all who contributed their time here --- .../mediapicker/mediapicker.controller.js | 10 ++++++++-- .../infiniteeditors/mediapicker/mediapicker.html | 9 ++++----- 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 +- 5 files changed, 15 insertions(+), 10 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 4419805194..c189131646 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 @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.MediaPickerController", - function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService) { + function ($scope, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage) { var vm = this; @@ -37,6 +37,11 @@ angular.module("umbraco") $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); $scope.lockedFolder = true; + $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; + + $scope.filterOptions = { + excludeSubFolders: umbSessionStorage.get("mediaPickerExcludeSubFolders") || false + }; var userStartNodes = []; @@ -392,6 +397,7 @@ angular.module("umbraco") } function toggle() { + umbSessionStorage.set("mediaPickerExcludeSubFolders", $scope.filterOptions.excludeSubFolders); // Make sure to activate the changeSearch function everytime the toggle is clicked changeSearch(); } @@ -404,7 +410,7 @@ angular.module("umbraco") function searchMedia() { vm.loading = true; - entityResource.getPagedDescendants($scope.currentFolder.id, "Media", vm.searchOptions) + entityResource.getPagedDescendants($scope.filterOptions.excludeSubFolders ? $scope.currentFolder.id : $scope.startNodeId, "Media", vm.searchOptions) .then(function (data) { // update image data to work with image grid angular.forEach(data.items, function (mediaItem) { 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 82612d5e19..e1a89061b6 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 @@ -32,10 +32,10 @@
+ label-key="general_excludeFromSubFolders">
@@ -112,9 +112,8 @@ disable-folder-select={{disableFolderSelect}} only-images={{onlyImages}} only-folders={{onlyFolders}} - include-sub-folders={{showChilds}} - current-folder-id="{{currentFolder.id}}" - allow-open-folder="!disableFolderSelect"> + include-sub-folders={{!filterOptions.excludeSubFolders}} + current-folder-id="{{currentFolder.id}}"> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 98ef2a892a..ae8925e911 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -661,7 +661,7 @@ Ikon Id Importer - Inkludér undermapper i søgning + Søg kun i denne mappe Info Indre margen Indsæt diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index baa04cef7c..a85df5714b 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -674,7 +674,7 @@ Icon Id Import - Include subfolders in search + Search only this folder Info Inner margin Insert 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 4b3c1fa870..d14fb03727 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -676,7 +676,7 @@ Icon Id Import - Include subfolders in search + Search only this folder Info Inner margin Insert From 48a3ec0581045824999b25f992e1dc6001a80058 Mon Sep 17 00:00:00 2001 From: Nik Date: Tue, 28 Jan 2020 18:52:29 +0000 Subject: [PATCH 282/610] [FIX] Corrected Create method so that String UDI is correctly identified - [FIX] Changed check of UDI type to check for GUID type and then String type as previously was checking for GUID type twice. This resulted in the method never creating a String UDI if needed. - [ISSUE] Github Issue #7507 https://github.com/umbraco/Umbraco-CMS/issues/7507 --- src/Umbraco.Core/Udi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index ea3ec0ed2d..b11ce250ad 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -316,7 +316,7 @@ namespace Umbraco.Core if (udiType == UdiType.GuidUdi) return new GuidUdi(uri); - if (udiType == UdiType.GuidUdi) + if (udiType == UdiType.StringUdi) return new StringUdi(uri); throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); From 0a89fcc248135752199ae1f394846551c1160f28 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 30 Jan 2020 17:06:15 +0100 Subject: [PATCH 283/610] =?UTF-8?q?V8:=20Ensure=20consistent=20template=20?= =?UTF-8?q?file=20casing=20no=20matter=20how=20templa=E2=80=A6=20(#4889)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Kenn! Wonderful, as ever --- src/Umbraco.Web/Editors/TemplateController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs index 8da5e80f2e..903446d23c 100644 --- a/src/Umbraco.Web/Editors/TemplateController.cs +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -194,7 +194,9 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Alias, display.Content, master); + // we need to pass the template name as alias to keep the template file casing consistent with templates created with content + // - see comment in FileService.CreateTemplateForContentType for additional details + var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Name, display.Content, master); Mapper.Map(template, display); } From 377f1925eaa7add6b8c94bb29174da6873541d0e Mon Sep 17 00:00:00 2001 From: Steve Temple Date: Fri, 31 Jan 2020 09:34:08 +0000 Subject: [PATCH 284/610] Obsolete CreatorName and WriterName on IPublishedContent --- .../PublishedContent/IPublishedContent.cs | 2 ++ .../PublishedContentHashtableConverter.cs | 2 ++ src/Umbraco.Web/Models/PublishedContentBase.cs | 4 ++++ .../PublishedCache/NuCache/PublishedContent.cs | 4 ++++ .../PublishedCache/PublishedMember.cs | 4 ++++ src/Umbraco.Web/PublishedContentExtensions.cs | 18 ++++++++++++++++-- 6 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index 1c0d39a8b8..e8bee90d7c 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -63,6 +63,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the name of the user who created the content item. /// + [Obsolete("Use CreatorName(IUserService) extension instead")] string CreatorName { get; } /// @@ -78,6 +79,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the name of the user who last updated the content item. /// + [Obsolete("Use WriterName(IUserService) extension instead")] string WriterName { get; } /// diff --git a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs index 41f0e2fb65..3bbc4a793b 100644 --- a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs +++ b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs @@ -248,8 +248,10 @@ namespace Umbraco.Web.Macros public string UrlSegment => throw new NotImplementedException(); + [Obsolete("Use WriterName(IUserService) extension instead")] public string WriterName { get; } + [Obsolete("Use CreatorName(IUserService) extension instead")] public string CreatorName { get; } public int WriterId => _inner.WriterId; diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 148bab11c0..032a1a6fda 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -54,6 +54,8 @@ namespace Umbraco.Web.Models public abstract int CreatorId { get; } /// + [Obsolete("Use CreatorName(IUserService) extension instead")] + public abstract string CreatorName { get; } /// @@ -63,6 +65,8 @@ namespace Umbraco.Web.Models public abstract int WriterId { get; } /// + [Obsolete("Use WriterName(IUserService) extension instead")] + public abstract string WriterName { get; } /// diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index 3c3c3ac54f..c84d49d685 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -163,6 +163,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public override int CreatorId => _contentNode.CreatorId; /// + [Obsolete("Use CreatorName(IUserService) extension instead")] + public override string CreatorName => GetProfileNameById(_contentNode.CreatorId); /// @@ -172,6 +174,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public override int WriterId => ContentData.WriterId; /// + [Obsolete("Use WriterName(IUserService) extension instead")] + public override string WriterName => GetProfileNameById(ContentData.WriterId); /// diff --git a/src/Umbraco.Web/PublishedCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs index 6e9ec61c62..30e5882ea8 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -140,9 +140,13 @@ namespace Umbraco.Web.PublishedCache public override string UrlSegment => throw new NotSupportedException(); // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current + [Obsolete("Use WriterName(IUserService) extension instead")] + public override string WriterName => _member.GetCreatorProfile().Name; // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current + [Obsolete("Use CreatorName(IUserService) extension instead")] + public override string CreatorName => _member.GetCreatorProfile().Name; public override int WriterId => _member.CreatorId; diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 061422859c..4e9735b0bd 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -28,6 +28,20 @@ namespace Umbraco.Web private static UmbracoContext UmbracoContext => Current.UmbracoContext; private static ISiteDomainHelper SiteDomainHelper => Current.Factory.GetInstance(); + #region Creator/Writer Names + + public static string CreatorName(this IPublishedContent content, IUserService userService) + { + return userService.GetProfileById(content.CreatorId)?.Name; + } + + public static string WriterName(this IPublishedContent content, IUserService userService) + { + return userService.GetProfileById(content.WriterId)?.Name; + } + + #endregion + #region IsComposedOf /// @@ -1004,8 +1018,8 @@ namespace Umbraco.Web { "NodeTypeAlias", n.ContentType.Alias }, { "CreateDate", n.CreateDate }, { "UpdateDate", n.UpdateDate }, - { "CreatorName", n.CreatorName }, - { "WriterName", n.WriterName }, + { "CreatorName", n.CreatorName(services.UserService) }, + { "WriterName", n.WriterName(services.UserService) }, { "Url", n.Url() } }; From b95d674092e8c71a8d135d8799746ffa6dfc29b8 Mon Sep 17 00:00:00 2001 From: Steve Temple Date: Fri, 31 Jan 2020 09:50:37 +0000 Subject: [PATCH 285/610] Tidy up formatting --- src/Umbraco.Web/Models/PublishedContentBase.cs | 2 -- src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs | 2 -- src/Umbraco.Web/PublishedCache/PublishedMember.cs | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 032a1a6fda..4bdea8b920 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -55,7 +55,6 @@ namespace Umbraco.Web.Models /// [Obsolete("Use CreatorName(IUserService) extension instead")] - public abstract string CreatorName { get; } /// @@ -66,7 +65,6 @@ namespace Umbraco.Web.Models /// [Obsolete("Use WriterName(IUserService) extension instead")] - public abstract string WriterName { get; } /// diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index c84d49d685..d80affbfa9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -164,7 +164,6 @@ namespace Umbraco.Web.PublishedCache.NuCache /// [Obsolete("Use CreatorName(IUserService) extension instead")] - public override string CreatorName => GetProfileNameById(_contentNode.CreatorId); /// @@ -175,7 +174,6 @@ namespace Umbraco.Web.PublishedCache.NuCache /// [Obsolete("Use WriterName(IUserService) extension instead")] - public override string WriterName => GetProfileNameById(ContentData.WriterId); /// diff --git a/src/Umbraco.Web/PublishedCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs index 30e5882ea8..e7cdb65f2e 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -141,12 +141,10 @@ namespace Umbraco.Web.PublishedCache // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current [Obsolete("Use WriterName(IUserService) extension instead")] - public override string WriterName => _member.GetCreatorProfile().Name; // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current [Obsolete("Use CreatorName(IUserService) extension instead")] - public override string CreatorName => _member.GetCreatorProfile().Name; public override int WriterId => _member.CreatorId; From 6b461671800c2224376d90ef201ad582ec05994c Mon Sep 17 00:00:00 2001 From: Steve Temple Date: Fri, 31 Jan 2020 14:40:37 +0000 Subject: [PATCH 286/610] In GetDataTable use Obsolete CreatorName and WriterName for now --- src/Umbraco.Web/PublishedContentExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 4e9735b0bd..8d85c4615a 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1018,8 +1018,8 @@ namespace Umbraco.Web { "NodeTypeAlias", n.ContentType.Alias }, { "CreateDate", n.CreateDate }, { "UpdateDate", n.UpdateDate }, - { "CreatorName", n.CreatorName(services.UserService) }, - { "WriterName", n.WriterName(services.UserService) }, + { "CreatorName", n.CreatorName }, + { "WriterName", n.WriterName }, { "Url", n.Url() } }; From a8672790d3d5b12717b909b2ef00569d47516253 Mon Sep 17 00:00:00 2001 From: Steve Temple Date: Fri, 31 Jan 2020 13:08:32 +0000 Subject: [PATCH 287/610] Obsolete Url on IPublishedContent --- src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs | 1 + .../Models/PublishedContent/PublishedContentWrapped.cs | 1 + src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs | 1 + src/Umbraco.Web/Controllers/UmbLoginController.cs | 2 +- src/Umbraco.Web/Models/PublishedContentBase.cs | 1 + src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs | 2 +- 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index 1c0d39a8b8..3f8b1c2ff3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -97,6 +97,7 @@ namespace Umbraco.Core.Models.PublishedContent /// The value of this property is contextual. It depends on the 'current' request uri, /// if any. /// + [Obsolete("Use the Url() extension instead")] string Url { get; } /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index fb41c95419..b83dc7a013 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -94,6 +94,7 @@ namespace Umbraco.Core.Models.PublishedContent public virtual DateTime UpdateDate => _content.UpdateDate; /// + [Obsolete("Use the Url() extension instead")] public virtual string Url => _content.Url; /// diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 860b9b9179..a7b6d3d18a 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -199,6 +199,7 @@ namespace Umbraco.Tests.PublishedContent public DateTime UpdateDate { get; set; } public Guid Version { get; set; } public int Level { get; set; } + [Obsolete("Use the Url() extension instead")] public string Url { get; set; } public PublishedItemType ItemType => PublishedItemType.Content; diff --git a/src/Umbraco.Web/Controllers/UmbLoginController.cs b/src/Umbraco.Web/Controllers/UmbLoginController.cs index 2ff80e2668..88bc17abff 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginController.cs @@ -46,7 +46,7 @@ namespace Umbraco.Web.Controllers // if it's not a local url we'll redirect to the root of the current site return Redirect(Url.IsLocalUrl(model.RedirectUrl) ? model.RedirectUrl - : CurrentPage.AncestorOrSelf(1).Url); + : CurrentPage.AncestorOrSelf(1).Url()); } //redirect to current page by default diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 148bab11c0..7f62c72004 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -69,6 +69,7 @@ namespace Umbraco.Web.Models public abstract DateTime UpdateDate { get; } /// + [Obsolete("Use the Url() extension instead")] public virtual string Url => this.Url(); /// diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs index 0b2a607f8b..2a891ff1c7 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -101,7 +101,7 @@ namespace Umbraco.Web.PropertyEditors if (mediaTyped == null) throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available."); - var location = mediaTyped.Url; + var location = mediaTyped.Url(); // Find the width & height attributes as we need to set the imageprocessor QueryString var width = img.GetAttributeValue("width", int.MinValue); From a3bf9d2a51a9315750d42f0adbbe6febce20e9ae Mon Sep 17 00:00:00 2001 From: Robert Foster Date: Sat, 1 Feb 2020 20:06:16 +1000 Subject: [PATCH 288/610] Renamed "DermineIsChildOfListView" to "DetermineIsChildOfListView" --- src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs | 4 ++-- src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index dc0df4ca96..c811c236d4 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.Models.Mapping target.Icon = source.ContentType.Icon; target.Id = source.Id; target.IsBlueprint = source.Blueprint; - target.IsChildOfListView = DermineIsChildOfListView(source); + target.IsChildOfListView = DetermineIsChildOfListView(source); target.IsContainer = source.ContentType.IsContainer; target.IsElement = source.ContentType.IsElement; target.Key = source.Key; @@ -211,7 +211,7 @@ namespace Umbraco.Web.Models.Mapping return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"({source.Name})"; } - private bool DermineIsChildOfListView(IContent source) + private bool DetermineIsChildOfListView(IContent source) { // map the IsChildOfListView (this is actually if it is a descendant of a list view!) var parent = _contentService.GetParent(source); diff --git a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs index 05c006ec41..fa89496392 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaMapDefinition.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web.Models.Mapping target.CreateDate = source.CreateDate; target.Icon = source.ContentType.Icon; target.Id = source.Id; - target.IsChildOfListView = DermineIsChildOfListView(source); + target.IsChildOfListView = DetermineIsChildOfListView(source); target.Key = source.Key; target.MediaLink = string.Join(",", source.GetUrls(Current.Configs.Settings().Content, _logger)); target.Name = source.Name; @@ -95,7 +95,7 @@ namespace Umbraco.Web.Models.Mapping target.VariesByCulture = source.ContentType.VariesByCulture(); } - private bool DermineIsChildOfListView(IMedia source) + private bool DetermineIsChildOfListView(IMedia source) { // map the IsChildOfListView (this is actually if it is a descendant of a list view!) var parent = _mediaService.GetParent(source); From c96c0cff320495d0c028cb65a1bc2c4905a5cff0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2020 03:27:33 +0000 Subject: [PATCH 289/610] Bump tinymce from 4.9.2 to 4.9.7 in /src/Umbraco.Web.UI.Client Bumps [tinymce](https://github.com/tinymce/tinymce-dist) from 4.9.2 to 4.9.7. - [Release notes](https://github.com/tinymce/tinymce-dist/releases) - [Changelog](https://github.com/tinymce/tinymce-dist/blob/4.9.7/changelog.txt) - [Commits](https://github.com/tinymce/tinymce-dist/compare/4.9.2...4.9.7) Signed-off-by: dependabot[bot] --- src/Umbraco.Web.UI.Client/package-lock.json | 6 +++--- src/Umbraco.Web.UI.Client/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index dd58b5ca18..95beae8316 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -15313,9 +15313,9 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "tinymce": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.2.tgz", - "integrity": "sha512-ZRoTGG4GAsOI73QPSNkabO7nkoYw9H6cglRB44W2mMkxSiqxYi8WJlgkUphk0fDqo6ZD6r3E+NSP4UHxF2lySg==" + "version": "4.9.7", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.7.tgz", + "integrity": "sha512-cj0HvUuniTuIjOAJdRt5BUfeQqM5yHjbA2NOub9HUHXlCrT9OwD9WBPU6tGlaPC2l2I4eGoOnT8llosZRdQU5Q==" }, "tmp": { "version": "0.0.33", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index beff2b45d1..508409b4ea 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -42,7 +42,7 @@ "npm": "6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", - "tinymce": "4.9.2", + "tinymce": "4.9.7", "typeahead.js": "0.11.1", "underscore": "1.9.1" }, From 889f0fc08553c5e6ef47c70b81181bfb3517bf88 Mon Sep 17 00:00:00 2001 From: elitsa Date: Mon, 3 Feb 2020 11:39:11 +0100 Subject: [PATCH 290/610] Html encoding document name when it's rendered in the relation types html. --- .../umbraco/developer/RelationTypes/EditRelationType.aspx.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs index 33366681f5..c718183988 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using umbraco.BasePages; @@ -105,9 +106,9 @@ namespace umbraco.cms.presentation.developer.RelationTypes readOnlyRelation.Id = reader.GetInt("id"); readOnlyRelation.ParentId = reader.GetInt("parentId"); - readOnlyRelation.ParentText = reader.GetString("parentText"); + readOnlyRelation.ParentText = HttpUtility.HtmlEncode(reader.GetString("parentText")); readOnlyRelation.ChildId = reader.GetInt("childId"); - readOnlyRelation.ChildText = reader.GetString("childText"); + readOnlyRelation.ChildText = HttpUtility.HtmlEncode(reader.GetString("childText")); readOnlyRelation.RelType = reader.GetInt("relType"); readOnlyRelation.DateTime = reader.GetDateTime("datetime"); readOnlyRelation.Comment = reader.GetString("comment"); From 3d0543922a1a94aa78e1f26b72ef34b3f1ae0a32 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 3 Feb 2020 22:48:00 +1100 Subject: [PATCH 291/610] un-breaks ctor --- .../ValueConverters/MarkdownEditorValueConverter.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index c62a79d283..5c027ed9b6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -15,6 +15,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters private readonly HtmlLocalLinkParser _localLinkParser; private readonly HtmlUrlParser _urlParser; + [Obsolete("Use ctor defining all dependencies instead")] + public MarkdownEditorValueConverter() + : this(Current.Factory.GetInstance(), Current.Factory.GetInstance()) + { + } + public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser) { _localLinkParser = localLinkParser; @@ -25,7 +31,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters => Constants.PropertyEditors.Aliases.MarkdownEditor == propertyType.EditorAlias; public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IHtmlString); + => typeof(IHtmlString); public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; From a5fa7465b10ace6926affd19e0a86849e4be8f53 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 14:27:19 +0000 Subject: [PATCH 292/610] In ConvertTinyMceAndGridMediaUrlsToLocalLink, avoid unnecessary updates --- ...nvertTinyMceAndGridMediaUrlsToLocalLink.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index b68aa23d78..c369af4adf 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -43,6 +43,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 var value = property.TextValue; if (string.IsNullOrWhiteSpace(value)) continue; + + bool propertyChanged = false; if (property.PropertyTypeDto.DataTypeDto.EditorAlias == Constants.PropertyEditors.Aliases.Grid) { try @@ -55,7 +57,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 var controlValue = control["value"]; if (controlValue?.Type == JTokenType.String) { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value(), out var controlChanged); + propertyChanged |= controlChanged; } } @@ -76,10 +79,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 } else { - property.TextValue = UpdateMediaUrls(mediaLinkPattern, value); + property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); } - Database.Update(property); + if (propertyChanged) + Database.Update(property); } @@ -91,10 +95,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 Context.AddPostMigration(); } - private string UpdateMediaUrls(Regex mediaLinkPattern, string value) + private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) { - return mediaLinkPattern.Replace(value, match => + bool matched = false; + + var result = mediaLinkPattern.Replace(value, match => { + matched = true; + // match groups: // - 1 = from the beginning of the a tag until href attribute value begins // - 2 = the href attribute value excluding the querystring (if present) @@ -106,6 +114,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 ? match.Value : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; }); + + changed = matched; + + return result; } } } From c154af84d84f5493ada27245be3a77c4fd73d1a3 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 14:36:07 +0000 Subject: [PATCH 293/610] In AddTypedLabels, clear VarcharValue as well as TextValue --- .../Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs index 309f8acbc3..033197a78e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs @@ -101,16 +101,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 foreach (var value in values) Database.Execute(Sql() .Update(u => u - .Set(x => x.IntegerValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (int?) null : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) - .Set(x => x.TextValue, null)) + .Set(x => x.IntegerValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (int?)null : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) .Where(x => x.Id == value.Id)); values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue).From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); foreach (var value in values) Database.Execute(Sql() .Update(u => u - .Set(x => x.DateValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (DateTime?) null : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) - .Set(x => x.TextValue, null)) + .Set(x => x.DateValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (DateTime?)null : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) .Where(x => x.Id == value.Id)); // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it From 1677b32274a1fd13eceeaa4395d033a8d99b9bb6 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 14:37:31 +0000 Subject: [PATCH 294/610] Create temporary index on umbracoPropertyData --- .../Migrations/Upgrade/V_8_0_0/VariantsMigration.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 8c60d30680..7bd1cca3b1 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { MigratePropertyData(); + CreatePropertyDataIndexes(); MigrateContentAndPropertyTypes(); MigrateContent(); MigrateVersions(); @@ -90,6 +91,15 @@ HAVING COUNT(v2.id) <> 1").Any()) Rename.Table(PreTables.PropertyData).To(Constants.DatabaseSchema.Tables.PropertyData).Do(); } + private void CreatePropertyDataIndexes() + { + // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. + // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created + var tableDefinition = Persistence.DatabaseModelDefinitions.DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); + Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); + Execute.Sql($"CREATE UNIQUE NONCLUSTERED INDEX IX_umbracoPropertyData_Temp ON {PreTables.PropertyData} (versionId,propertyTypeId,languageId,segment)").Do(); + } + private void MigrateContentAndPropertyTypes() { if (!ColumnExists(PreTables.ContentType, "variations")) From 681264d233e36fcfef16cfbb29c5e4ae3496d8d8 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 14:40:24 +0000 Subject: [PATCH 295/610] Remove temporary index --- .../Migrations/Upgrade/Common/CreateKeysAndIndexes.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs index 29006c8811..4872e0019d 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs @@ -12,6 +12,7 @@ namespace Umbraco.Core.Migrations.Upgrade.Common { // remove those that may already have keys Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.KeyValue).Do(); + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.PropertyData).Do(); // re-create *all* keys and indexes foreach (var x in DatabaseSchemaCreator.OrderedTables) From a6d89f5363e9c49a4d0ad237bc55f4638d6522be Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 14:53:31 +0000 Subject: [PATCH 296/610] SQLCE does not support UPDATE...FROM, but we can still use it for SQL Server --- .../V_8_0_0/RenameMediaVersionTable.cs | 23 +++++--- .../Upgrade/V_8_0_0/VariantsMigration.cs | 54 ++++++++++++++----- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs index b60923fcba..2b27bdafe8 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { @@ -17,14 +18,24 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 AddColumn("id", out var sqls); - // SQLCE does not support UPDATE...FROM - var temp2 = Database.Fetch($@"SELECT v.versionId, v.id + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var versions = Database.Fetch($@"SELECT v.versionId, v.id FROM cmsContentVersion v JOIN umbracoNode n on v.contentId=n.id WHERE n.nodeObjectType='{Constants.ObjectTypes.Media}'"); - foreach (var t in temp2) - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id={t.id} WHERE versionId='{t.versionId}'").Do(); - + foreach (var t in versions) + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id={t.id} WHERE versionId='{t.versionId}'").Do(); + } + else + { + Database.Execute($@"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id +FROM {Constants.DatabaseSchema.Tables.MediaVersion} m +JOIN cmsContentVersion v on m.versionId = v.versionId +JOIN umbracoNode n on v.contentId=n.id +WHERE n.nodeObjectType='{Constants.ObjectTypes.Media}'"); + } foreach (var sql in sqls) Execute.Sql(sql).Do(); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 8c60d30680..053a64a8e3 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -74,10 +74,18 @@ HAVING COUNT(v2.id) <> 1").Any()) { Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); - // SQLCE does not support UPDATE...FROM - var temp = Database.Fetch($"SELECT id, versionId FROM {PreTables.ContentVersion}"); - foreach (var t in temp) - Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2=@v2 WHERE versionId=@v1", new { v1 = t.versionId, v2 = t.id }); + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var versions = Database.Fetch($"SELECT id, versionId FROM {PreTables.ContentVersion}"); + foreach (var t in versions) + Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2=@v2 WHERE versionId=@v1", new { v1 = t.versionId, v2 = t.id }); + } + else + { + Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id FROM {PreTables.ContentVersion} INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); + } + Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); } @@ -153,22 +161,40 @@ HAVING COUNT(v2.id) <> 1").Any()) ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); // populate contentVersion text, current and userId columns for documents - // SQLCE does not support UPDATE...FROM - var temp1 = Database.Fetch($"SELECT versionId, text, newest, documentUser FROM {PreTables.Document}"); - foreach (var t in temp1) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", - new { text = t.text, current = t.newest, userId=t.documentUser, versionId=t.versionId }); + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var documents = Database.Fetch($"SELECT versionId, text, published, newest, documentUser FROM {PreTables.Document}"); + foreach (var t in documents) + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", + new { text = t.text, current = t.newest && !t.published, userId = t.documentUser, versionId = t.versionId }); + } + else + { + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser +FROM {PreTables.ContentVersion} v INNER JOIN {PreTables.Document} d ON d.versionId = v.versionId"); + } // populate contentVersion text and current columns for non-documents, userId is default - // SQLCE does not support UPDATE...FROM - var temp2 = Database.Fetch($@"SELECT cver.versionId, n.text + if (Database.DatabaseType.IsSqlCe()) + { + // SQLCE does not support UPDATE...FROM + var otherContent = Database.Fetch($@"SELECT cver.versionId, n.text FROM {PreTables.ContentVersion} cver JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); - foreach (var t in temp2) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 WHERE versionId=@versionId", - new { text = t.text, versionId=t.versionId }); + foreach (var t in otherContent) + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 WHERE versionId=@versionId", + new { text = t.text, versionId = t.versionId }); + } + else + { + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 +FROM {PreTables.ContentVersion} cver +JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id +WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); + } // create table Create.Table(withoutKeysAndIndexes: true).Do(); From 2b56a5aa2c6b3ed5932011679cf198eaca3edf4a Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 14:23:33 +0000 Subject: [PATCH 297/610] Use sp_rename in ReplaceColumn when available --- .../Migrations/MigrationBase_Extra.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs index bf07e4d08f..f4c6150073 100644 --- a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.SqlSyntax; @@ -84,10 +85,18 @@ namespace Umbraco.Core.Migrations protected void ReplaceColumn(string tableName, string currentName, string newName) { - AddColumn(tableName, newName, out var sqls); - Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(tableName)} SET {SqlSyntax.GetQuotedColumnName(newName)}={SqlSyntax.GetQuotedColumnName(currentName)}").Do(); - foreach (var sql in sqls) Execute.Sql(sql).Do(); - Delete.Column(currentName).FromTable(tableName).Do(); + if (DatabaseType.IsSqlCe()) + { + AddColumn(tableName, newName, out var sqls); + Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(tableName)} SET {SqlSyntax.GetQuotedColumnName(newName)}={SqlSyntax.GetQuotedColumnName(currentName)}").Do(); + foreach (var sql in sqls) Execute.Sql(sql).Do(); + Delete.Column(currentName).FromTable(tableName).Do(); + } + else + { + Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); + AlterColumn(tableName, newName); + } } protected bool TableExists(string tableName) From e830af26ce819a329a9c6b2545b7c44293756326 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 16:15:54 +0000 Subject: [PATCH 298/610] VariantsMigration - setting edited flag for versions --- .../Migrations/Upgrade/V_8_0_0/VariantsMigration.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 8c60d30680..fd2844638c 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Umbraco.Core.Migrations.Install; @@ -223,7 +224,7 @@ WHERE versionId NOT IN (SELECT (versionId) FROM {PreTables.ContentVersion} WHERE if (!ColumnExists(PreTables.Document, "edited")) { AddColumn(PreTables.Document, "edited", out var sqls); - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=0"); + Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); foreach (var sql in sqls) Database.Execute(sql); } @@ -240,11 +241,15 @@ JOIN {Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId JOIN {PreTables.ContentVersion} cv2 ON n.id=cv2.nodeId JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 JOIN {Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId -WHERE v1.propertyTypeId=v2.propertyTypeId AND v1.languageId=v2.languageId AND v1.segment=v2.segment"); +WHERE v1.propertyTypeId=v2.propertyTypeId +AND (v1.languageId=v2.languageId OR (v1.languageId IS NULL AND v2.languageId IS NULL)) +AND (v1.segment=v2.segment OR (v1.segment IS NULL AND v2.segment IS NULL))"); + var updatedIds = new HashSet(); foreach (var t in temp) if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) - Database.Execute("UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeIdd", new { t.id }); + if (updatedIds.Add((int)t.id)) + Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", new { nodeId = t.id }); // drop more columns Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); From cb42eebb24a78978e533cb121213999e301a0c63 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 16:27:37 +0000 Subject: [PATCH 299/610] Migrations - ensure that documents with a published version are marked as published --- .../Migrations/Upgrade/V_8_0_0/VariantsMigration.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 8c60d30680..ce66094026 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -209,6 +209,10 @@ VALUES (@id, @templateId, 0)", new { id=id, templateId=t.templateId }); Database.Execute($@"DELETE FROM {PreTables.Document} WHERE versionId NOT IN (SELECT (versionId) FROM {PreTables.ContentVersion} WHERE {SqlSyntax.GetQuotedColumnName("current")} = 1) AND (published<>1 OR newest<>1)"); + // ensure that documents with a published version are marked as published + Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( +SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); + // drop some document columns Delete.Column("text").FromTable(PreTables.Document).Do(); Delete.Column("templateId").FromTable(PreTables.Document).Do(); From 03631cfe9a594af69682080dfc067350cdfc893a Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 16:53:35 +0000 Subject: [PATCH 300/610] Add extra "published=newest" versions with INSERT...SELECT instead of foreach --- .../Upgrade/V_8_0_0/VariantsMigration.cs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 8c60d30680..3c22f8923c 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -179,31 +179,33 @@ SELECT cver.id, doc.templateId, doc.published FROM {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId"); + // need to add extra rows for where published=newest // 'cos INSERT above has inserted the 'published' document version // and v8 always has a 'edited' document version too - var temp3 = Database.Fetch($@"SELECT doc.nodeId, doc.updateDate, doc.documentUser, doc.text, doc.templateId, cver.id versionId + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) +SELECT doc.nodeId, NEWID(), doc.updateDate, doc.documentUser, 1, doc.text FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId WHERE doc.newest=1 AND doc.published=1"); - var getIdentity = "@@@@IDENTITY"; - foreach (var t in temp3) - { - Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) -VALUES (@nodeId, @versionId, @versionDate, @userId, 1, @text)", new { nodeId=t.nodeId, versionId=Guid.NewGuid(), versionDate=t.updateDate, userId=t.documentUser, text=t.text }); - var id = Database.ExecuteScalar("SELECT " + getIdentity); - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0 WHERE nodeId=@0 AND id<>@1", (int) t.nodeId, id); - Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) -VALUES (@id, @templateId, 0)", new { id=id, templateId=t.templateId }); - var versionId = (int) t.versionId; - var pdatas = Database.Fetch(Sql().Select().From().Where(x => x.VersionId == versionId)); - foreach (var pdata in pdatas) - { - pdata.VersionId = id; - Database.Insert(pdata); - } - } + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) +SELECT cverNew.id, doc.templateId, 0 +FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc +JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId +WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); + + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.PropertyData)} (propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,versionId) +SELECT propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,cverNew.id +FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc +JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId +JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId +JOIN {SqlSyntax.GetQuotedTableName(PreTables.PropertyData)} pd ON pd.versionId=cver.id +WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); + // reduce document to 1 row per content Database.Execute($@"DELETE FROM {PreTables.Document} From 0187967cd8ddd2343ad935530e327515d9a39026 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Mon, 3 Feb 2020 17:01:49 +0000 Subject: [PATCH 301/610] Don't mark a version as current if it is published --- .../Migrations/Upgrade/V_8_0_0/VariantsMigration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 3c22f8923c..fe566c8668 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -154,10 +154,10 @@ HAVING COUNT(v2.id) <> 1").Any()) // populate contentVersion text, current and userId columns for documents // SQLCE does not support UPDATE...FROM - var temp1 = Database.Fetch($"SELECT versionId, text, newest, documentUser FROM {PreTables.Document}"); + var temp1 = Database.Fetch($"SELECT versionId, text, published, newest, documentUser FROM {PreTables.Document}"); foreach (var t in temp1) Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", - new { text = t.text, current = t.newest, userId=t.documentUser, versionId=t.versionId }); + new { text = t.text, current = t.newest && !t.published, userId=t.documentUser, versionId=t.versionId }); // populate contentVersion text and current columns for non-documents, userId is default // SQLCE does not support UPDATE...FROM From a426a15b5a0cec83f5c630b06a87330eb7e85d8b Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 4 Feb 2020 15:49:05 +1100 Subject: [PATCH 302/610] fixes migration and uses migration syntax --- .../Migrations/Upgrade/V_8_0_0/VariantsMigration.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 7bd1cca3b1..ebeffd4898 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -97,7 +97,14 @@ HAVING COUNT(v2.id) <> 1").Any()) // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created var tableDefinition = Persistence.DatabaseModelDefinitions.DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); - Execute.Sql($"CREATE UNIQUE NONCLUSTERED INDEX IX_umbracoPropertyData_Temp ON {PreTables.PropertyData} (versionId,propertyTypeId,languageId,segment)").Do(); + Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) + .WithOptions().Unique() + .WithOptions().NonClustered() + .OnColumn("versionId").Ascending() + .OnColumn("propertyTypeId").Ascending() + .OnColumn("languageId").Ascending() + .OnColumn("segment").Ascending() + .Do(); } private void MigrateContentAndPropertyTypes() From 4cb307978ae4353184f1b3e940b52cb916743224 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 4 Feb 2020 16:41:33 +1100 Subject: [PATCH 303/610] Fixes issue with upgrading from v7 -> 8.6, will cherry pick this --- .../Upgrade/V_8_0_0/AddTypedLabels.cs | 3 +- .../V_8_0_0/ContentVariationMigration.cs | 58 +------ .../ConvertRelatedLinksToMultiUrlPicker.cs | 11 +- .../V_8_0_0/Models/ContentTypeDto80.cs | 63 ++++++++ .../V_8_0_0/Models/PropertyDataDto80.cs | 142 ++++++++++++++++++ .../V_8_0_0/Models/PropertyTypeDto80.cs | 80 ++++++++++ ...nvertTinyMceAndGridMediaUrlsToLocalLink.cs | 11 +- .../AddDatabaseIndexesMissingOnForeignKeys.cs | 2 +- .../V_8_6_0/MissingContentVersionsIndexes.cs | 27 ++-- src/Umbraco.Core/Umbraco.Core.csproj | 3 + 10 files changed, 326 insertions(+), 74 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs index 309f8acbc3..b50de05b95 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs @@ -75,8 +75,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var labelPropertyTypes = Database.Fetch(Sql() .Select(x => x.Id, x => x.Alias) .From() - .Where(x => x.DataTypeId == Constants.DataTypes.LabelString) - ); + .Where(x => x.DataTypeId == Constants.DataTypes.LabelString)); var intPropertyAliases = new[] { Constants.Conventions.Media.Width, Constants.Conventions.Media.Height, Constants.Conventions.Member.FailedPasswordAttempts }; var bigintPropertyAliases = new[] { Constants.Conventions.Media.Bytes }; diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 84dd393b0d..eabbd34b08 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using NPoco; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 @@ -47,14 +49,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } } - var propertyTypes = Database.Fetch(Sql().Select().From()); + var propertyTypes = Database.Fetch(Sql().Select().From()); foreach (var dto in propertyTypes) { dto.Variations = GetNewValue(dto.Variations); Database.Update(dto); } - var contentTypes = Database.Fetch(Sql().Select().From()); + var contentTypes = Database.Fetch(Sql().Select().From()); foreach (var dto in contentTypes) { dto.Variations = GetNewValue(dto.Variations); @@ -62,57 +64,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } } - // we *need* to use this private DTO here, which does *not* have extra properties, which would kill the migration + // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - private class ContentTypeDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] - public int PrimaryKey { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } - - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string Alias { get; set; } - - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string Icon { get; set; } - - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string Thumbnail { get; set; } - - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string Description { get; set; } - - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } - - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } - - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - public NodeDto NodeDto { get; set; } - } + } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index 1956876402..1e327346cf 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -2,8 +2,7 @@ using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -34,11 +33,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } var sqlPropertyTpes = Sql() - .Select() - .From() - .Where(x => dataTypeIds.Contains(x.DataTypeId)); + .Select() + .From() + .Where(x => dataTypeIds.Contains(x.DataTypeId)); - var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); + var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); if (propertyTypeIds.Count == 0) return; diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs new file mode 100644 index 0000000000..bc41e5e32a --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs @@ -0,0 +1,63 @@ +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors + /// + [TableName(TableName)] + [PrimaryKey("pk")] + [ExplicitColumns] + internal class ContentTypeDto80 + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 535)] + public int PrimaryKey { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Alias { get; set; } + + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string Icon { get; set; } + + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string Thumbnail { get; set; } + + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string Description { get; set; } + + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } + + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + public NodeDto NodeDto { get; set; } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs new file mode 100644 index 0000000000..f0a36b223d --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs @@ -0,0 +1,142 @@ +using NPoco; +using System; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors + /// + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class PropertyDataDto80 + { + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto80))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } + + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string VarcharValue { get; set; } + + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string TextValue { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto80 PropertyTypeDto { get; set; } + + [Ignore] + public object Value + { + get + { + if (IntegerValue.HasValue) + return IntegerValue.Value; + + if (DecimalValue.HasValue) + return DecimalValue.Value; + + if (DateValue.HasValue) + return DateValue.Value; + + if (!string.IsNullOrEmpty(VarcharValue)) + return VarcharValue; + + if (!string.IsNullOrEmpty(TextValue)) + return TextValue; + + return null; + } + } + + public PropertyDataDto80 Clone(int versionId) + { + return new PropertyDataDto80 + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + } + + protected bool Equals(PropertyDataDto other) + { + return Id == other.Id; + } + + public override bool Equals(object other) + { + return + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || other is PropertyDataDto pdata && pdata.Id == Id); + } + + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return Id; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs new file mode 100644 index 0000000000..6e2bd7371a --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs @@ -0,0 +1,80 @@ +using NPoco; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table would result in errors + /// + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class PropertyTypeDto80 + { + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 50)] + public int Id { get; set; } + + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } + + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } + + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } + + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string Alias { get; set; } + + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Name { get; set; } + + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } + + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } + + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string ValidationRegExp { get; set; } + + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string Description { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto DataTypeDto { get; set; } + + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index b68aa23d78..fdf692056a 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Migrations.PostMigrations; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; @@ -27,15 +28,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); var sqlPropertyData = Sql() - .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) + .From() + .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce || x.EditorAlias == Constants.PropertyEditors.Aliases.Grid); - var properties = Database.Fetch(sqlPropertyData); + var properties = Database.Fetch(sqlPropertyData); var exceptions = new List(); foreach (var property in properties) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs index e46f78dd72..070ff82a2c 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; - if (IndexExists(indexName) == false) + if (!IndexExists(indexName)) { Create .Index(indexName) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs index 75de01dd7f..c744409c2f 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs @@ -4,21 +4,30 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { public class MissingContentVersionsIndexes : MigrationBase { + private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + public MissingContentVersionsIndexes(IMigrationContext context) : base(context) { } public override void Migrate() { - Create - .Index("IX_" + ContentVersionDto.TableName + "_NodeId") - .OnTable(ContentVersionDto.TableName) - .OnColumn("nodeId") - .Ascending() - .OnColumn("current") - .Ascending() - .WithOptions().NonClustered() - .Do(); + // We must check before we create an index because if we are upgrading from v7 we force re-create all + // indexes in the whole DB and then this would throw + + if (!IndexExists(IndexName)) + { + Create + .Index(IndexName) + .OnTable(ContentVersionDto.TableName) + .OnColumn("nodeId") + .Ascending() + .OnColumn("current") + .Ascending() + .WithOptions().NonClustered() + .Do(); + } + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c36e628dec..a198074066 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,9 @@ --> + + + From 4698ce70d3011501a3b538ebda7443c1e889976e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 4 Feb 2020 16:43:36 +1100 Subject: [PATCH 304/610] Fixes issue with upgrading from v7 -> 8.6 # Conflicts: # src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs --- .../Upgrade/V_8_0_0/AddTypedLabels.cs | 3 +- .../V_8_0_0/ContentVariationMigration.cs | 58 +------ .../ConvertRelatedLinksToMultiUrlPicker.cs | 11 +- .../V_8_0_0/Models/ContentTypeDto80.cs | 63 ++++++++ .../V_8_0_0/Models/PropertyDataDto80.cs | 142 ++++++++++++++++++ .../V_8_0_0/Models/PropertyTypeDto80.cs | 80 ++++++++++ ...nvertTinyMceAndGridMediaUrlsToLocalLink.cs | 11 +- .../V_8_6_0/MissingContentVersionsIndexes.cs | 27 ++-- src/Umbraco.Core/Umbraco.Core.csproj | 3 + 9 files changed, 325 insertions(+), 73 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs index 309f8acbc3..b50de05b95 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs @@ -75,8 +75,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var labelPropertyTypes = Database.Fetch(Sql() .Select(x => x.Id, x => x.Alias) .From() - .Where(x => x.DataTypeId == Constants.DataTypes.LabelString) - ); + .Where(x => x.DataTypeId == Constants.DataTypes.LabelString)); var intPropertyAliases = new[] { Constants.Conventions.Media.Width, Constants.Conventions.Media.Height, Constants.Conventions.Member.FailedPasswordAttempts }; var bigintPropertyAliases = new[] { Constants.Conventions.Media.Bytes }; diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 84dd393b0d..eabbd34b08 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using NPoco; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 @@ -47,14 +49,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } } - var propertyTypes = Database.Fetch(Sql().Select().From()); + var propertyTypes = Database.Fetch(Sql().Select().From()); foreach (var dto in propertyTypes) { dto.Variations = GetNewValue(dto.Variations); Database.Update(dto); } - var contentTypes = Database.Fetch(Sql().Select().From()); + var contentTypes = Database.Fetch(Sql().Select().From()); foreach (var dto in contentTypes) { dto.Variations = GetNewValue(dto.Variations); @@ -62,57 +64,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } } - // we *need* to use this private DTO here, which does *not* have extra properties, which would kill the migration + // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - private class ContentTypeDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] - public int PrimaryKey { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } - - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string Alias { get; set; } - - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string Icon { get; set; } - - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string Thumbnail { get; set; } - - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string Description { get; set; } - - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } - - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } - - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - public NodeDto NodeDto { get; set; } - } + } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index 1956876402..1e327346cf 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -2,8 +2,7 @@ using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -34,11 +33,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 } var sqlPropertyTpes = Sql() - .Select() - .From() - .Where(x => dataTypeIds.Contains(x.DataTypeId)); + .Select() + .From() + .Where(x => dataTypeIds.Contains(x.DataTypeId)); - var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); + var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); if (propertyTypeIds.Count == 0) return; diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs new file mode 100644 index 0000000000..bc41e5e32a --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs @@ -0,0 +1,63 @@ +using NPoco; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors + /// + [TableName(TableName)] + [PrimaryKey("pk")] + [ExplicitColumns] + internal class ContentTypeDto80 + { + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 535)] + public int PrimaryKey { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Alias { get; set; } + + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string Icon { get; set; } + + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string Thumbnail { get; set; } + + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string Description { get; set; } + + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } + + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + public NodeDto NodeDto { get; set; } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs new file mode 100644 index 0000000000..f0a36b223d --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs @@ -0,0 +1,142 @@ +using NPoco; +using System; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors + /// + [TableName(TableName)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class PropertyDataDto80 + { + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto80))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } + + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string VarcharValue { get; set; } + + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string TextValue { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto80 PropertyTypeDto { get; set; } + + [Ignore] + public object Value + { + get + { + if (IntegerValue.HasValue) + return IntegerValue.Value; + + if (DecimalValue.HasValue) + return DecimalValue.Value; + + if (DateValue.HasValue) + return DateValue.Value; + + if (!string.IsNullOrEmpty(VarcharValue)) + return VarcharValue; + + if (!string.IsNullOrEmpty(TextValue)) + return TextValue; + + return null; + } + } + + public PropertyDataDto80 Clone(int versionId) + { + return new PropertyDataDto80 + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + } + + protected bool Equals(PropertyDataDto other) + { + return Id == other.Id; + } + + public override bool Equals(object other) + { + return + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || other is PropertyDataDto pdata && pdata.Id == Id); + } + + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return Id; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs new file mode 100644 index 0000000000..6e2bd7371a --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs @@ -0,0 +1,80 @@ +using NPoco; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models +{ + /// + /// Snapshot of the as it was at version 8.0 + /// + /// + /// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table would result in errors + /// + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + [PrimaryKey("id")] + [ExplicitColumns] + internal class PropertyTypeDto80 + { + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 50)] + public int Id { get; set; } + + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } + + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } + + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } + + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string Alias { get; set; } + + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Name { get; set; } + + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } + + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } + + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string ValidationRegExp { get; set; } + + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string Description { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto DataTypeDto { get; set; } + + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index b68aa23d78..fdf692056a 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Migrations.PostMigrations; +using Umbraco.Core.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Services; @@ -27,15 +28,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); var sqlPropertyData = Sql() - .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x.DataTypeDto))) + .From() + .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce || x.EditorAlias == Constants.PropertyEditors.Aliases.Grid); - var properties = Database.Fetch(sqlPropertyData); + var properties = Database.Fetch(sqlPropertyData); var exceptions = new List(); foreach (var property in properties) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs index 75de01dd7f..c744409c2f 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs @@ -4,21 +4,30 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { public class MissingContentVersionsIndexes : MigrationBase { + private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + public MissingContentVersionsIndexes(IMigrationContext context) : base(context) { } public override void Migrate() { - Create - .Index("IX_" + ContentVersionDto.TableName + "_NodeId") - .OnTable(ContentVersionDto.TableName) - .OnColumn("nodeId") - .Ascending() - .OnColumn("current") - .Ascending() - .WithOptions().NonClustered() - .Do(); + // We must check before we create an index because if we are upgrading from v7 we force re-create all + // indexes in the whole DB and then this would throw + + if (!IndexExists(IndexName)) + { + Create + .Index(IndexName) + .OnTable(ContentVersionDto.TableName) + .OnColumn("nodeId") + .Ascending() + .OnColumn("current") + .Ascending() + .WithOptions().NonClustered() + .Do(); + } + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a8e3fc2988..6b77ae83f6 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,9 @@ --> + + + From 6bd5ff9b33c4c7efeac5e42d870afb3a18d8005e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 4 Feb 2020 16:45:47 +1100 Subject: [PATCH 305/610] puts new migration in the right place with the right name --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 2 +- .../MissingDictionaryIndex.cs} | 4 ++-- src/Umbraco.Core/Umbraco.Core.csproj | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs => V_8_7_0/MissingDictionaryIndex.cs} (84%) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index c835a889d1..7143be2b90 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -191,7 +191,7 @@ namespace Umbraco.Core.Migrations.Upgrade To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); - To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); + To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs similarity index 84% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs index 070ff82a2c..fa5116c990 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddDatabaseIndexesMissingOnForeignKeys.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs @@ -2,9 +2,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 { - public class AddDatabaseIndexesMissingOnForeignKeys : MigrationBase + public class MissingDictionaryIndex : MigrationBase { - public AddDatabaseIndexesMissingOnForeignKeys(IMigrationContext context) + public MissingDictionaryIndex(IMigrationContext context) : base(context) { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a198074066..1fc3709e88 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -136,7 +136,7 @@ - + From 3bfc2514301b14f03a6a9e6ac3c1cf515b838b84 Mon Sep 17 00:00:00 2001 From: elitsa Date: Mon, 3 Feb 2020 11:39:11 +0100 Subject: [PATCH 306/610] Html encoding document name when it's rendered in the relation types html. (cherry picked from commit 889f0fc08553c5e6ef47c70b81181bfb3517bf88) --- .../umbraco/developer/RelationTypes/EditRelationType.aspx.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs index 33366681f5..c718183988 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/EditRelationType.aspx.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using umbraco.BasePages; @@ -105,9 +106,9 @@ namespace umbraco.cms.presentation.developer.RelationTypes readOnlyRelation.Id = reader.GetInt("id"); readOnlyRelation.ParentId = reader.GetInt("parentId"); - readOnlyRelation.ParentText = reader.GetString("parentText"); + readOnlyRelation.ParentText = HttpUtility.HtmlEncode(reader.GetString("parentText")); readOnlyRelation.ChildId = reader.GetInt("childId"); - readOnlyRelation.ChildText = reader.GetString("childText"); + readOnlyRelation.ChildText = HttpUtility.HtmlEncode(reader.GetString("childText")); readOnlyRelation.RelType = reader.GetInt("relType"); readOnlyRelation.DateTime = reader.GetDateTime("datetime"); readOnlyRelation.Comment = reader.GetString("comment"); From e3b0d8df61af76a0bf1c30f6b1a211b063bab45c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 5 Feb 2020 12:41:51 +1100 Subject: [PATCH 307/610] changes install background --- .../src/assets/img/installer.jpg | Bin 155700 -> 595227 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg b/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg index 6c0515906ffe07866f73d383bf6277d87415236d..75bf0d52af90724518357fc47a0780cab10c8129 100644 GIT binary patch literal 595227 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⪻25?6-TZ5r<4>%WNAAwKB8Eudhg${SmmIjv_RAP>tk6uhqKUezwvE_h$K8aPq@2{!m^^x((D zz;^0k^OXtPE>>^}b|u5=K`uF_zrM%IMww()zaCYsB$q$!fe-ae$Om#QOAMWHV&>XM zUR665{8zVcg$G)6_jptrVe`0|IGn6`=YzoKePO5aPe0jT4)Lnyw7K~>?l|`0R25({ zlCM^6m(qH_%e?%_{oBO(6nVa|7?VM`O3^cH82oVg#-U_vLTTp%MVC|oZI8cC81YF? z{v6XAQXP*$So@gXcB{W>b@BE>{u!DNhhD@(2Kr7_q&~19BmcQ7EqysaN3%us3k&>pwTOKYz@sM4Hr zflzCtZBYtnHn-d+79Td0C@H&+fD3$hY+`dLV@mV)&b=|Pr>qeAQ&~9=qSFN)y7t!M z-)-`z0My%x{K?bVBAPu%^QYVgj!`RfRrj;cJS^}N-*cyE-nQ1j{6uYrMn*By0S-oL z)S5SO7wCG41d3C*2w8x%z< zrq+HDs*u5q5Ne{eRT}NnBI`DUs!zp$v+Z*=z?sun^%A!TS<%6U7`Vw!t@mXp%_J>PzeRgjrU$bnGC~9{-&7IG;cMhI2zuOeq!k zD)N2j(nmtf;$Jp2hPe^v;rhebr;gZ|p)g-d1@~4G1_=_NT9ja2qGYC7vAuBqt@BMM z`TsrnUWQjVd1=n6!1*4Oc=oNQp3NyL7?E2_;alJ5hQ=2gq%n0VBW7Wf)v{IUM$&kP zJ+r*m<~Gb1Qjs6S4*Y~VW1VSAVDB%#@NXLDl++Y&3Re67$a9!Z*m{038$Cb0G210s z2~Xb+lbR&KPqfR7R~5s4{T>=Cr!DJ!V4p0V)K6xpI|%=1)B&2Rpop5NDw z$cY!Y8f#YBjz~Y1$m!PQjJ%Ya3t%PbGwj577L;QMFG(_r0@O6~Kbd^*&~9!0`0p?3aOrH|D(%AK$tW#Rj%Cnp4*0~KT{9lj)u2y?rMDcj+6R_-d z&EH&&9yi#I^1W>BnYde@$zGAetux`R%dCZ#ynGjxVdM~7;mLofA^anB_@gS>PuvJ& zGu$h@2N}$>cl_T!d3KZe8xM!V0-twBWdi;|yU>(1zMm`VP z-#maiAb7b8jpj9Se9%(nqeQ3B-=YK?+;Qbrd5g~0Y)*|-#+{C7?E|3-riN9EPLl)9 z1aXkniK6 zQAL@jI0*^wm@9Z#s>@Qv-bh>D4`c>3$KA!MR0NB2(1eIf33<~gT`10BD~QLlu{fb3cOVwQItpF93d zdj7-CF;I;>+gl6UKFi)^&Rc25yn>=uRUZ2_w372i?tDdXS>WXE-xg@B$ zT_lYtjM1Qg$k646ctm4ZeZE=qE!mOC(~~!bI@Mim%agi4LK92aUnoj>=G_9uQLpdh zf$boM8rI#7ePSk|DbD70w~?Hb2zcO8MWpxSTCW@C7!eZ|8(`sT+rw~Doe9kRvE{~Ml%oc$V0ZEEx%-lR_W~Oo2{q|? zj^{DQS)uUvt=Tr5xetZhA)umq@U9E)>qJ+Y6YGyTE5o&=eVs^;cX>a2i8&S;`(W69 z#9B~xTq&&jfUhSsw9hdsfWvTp68CPHDHQi%BE(k0IkF^%lEOLY;*d?dshC zp)Wn#`!d0oFLw03q-;$!Ad}`^DU79OWXbg2iJj_ZT;A&bOURf+ek5NG#7xo5DoD>> z#9YWsg`xFbzs9d0dc39wAihwPYr6mK=-X12z$lPa zrE~y~8xeO08GrEa%8ShbXl(=3 z9@h06cRl9eqfFnra*rLyw9?0&tO)%>nM(FFH2CC9G=`o-35HKJtK!7-ixQN%sQ;ZO zy?;Rf2F)Y3WpITYJ@tt#zw)^2>2MGU9>{xv$0yE{{YLyVi>7dpkQkM3s=l7yeiSq{5NfeHj3 z@>2vHlEIGlE~AmhK(QD6J&-pqS)(RHZ}#%$E-{)aSs|~flqM|c_##S%Fx@hMfzfUU zEK^&;r(fFi&bwFORNL>SuTSRhLjlv2uoreyow{}1B{n~HJeo4QSjg|z9N$z{?cMlm zlK-77GjyHZ&;T^p=0r`zE)E{+2MaGPlObG6TEDd#ftvAI%nJ0u;}*%P zC6PGT6SIQtF!h{-Cicf;MDDEDXugx8wv{x|mt2?5=G#8J7PxE8H4KpH5G1WlyztQ3 z%ikmE$q2ua>@m3{9K_+dj0u^@ExRMI9IVb0iLDo;e0sdwJ<( zx!PfgGT{O3?ul`xH5TD)cZ#zHvQb%2A*-C#9&uiiMCm#8Z0HgpR=wE9_2oE6k0R#7 zksE+y6&5Ie5C2EoW`B~{tzRsZ6ApWvs8`q}FM24P;F#ft{C+3)+~alRBXdl0?%;T@ z?UOeY*M1dYR2}xD6b8D`sTjGD;Gb6Fm`kN@TvgD(IJf{x3b5W*$?e4|KgmCZj-frg zxddz~s4ETDh|-&qY6@ddHf*mlf%P2fksmd6nU$vTC%LoWl<`Kr9hfkMz{#2;2kG9Y zi4&9SX9|c6^^H@<`Ljync1>+C`@DMUckrFE%=zDl0LXrW%IHq2MW8c4Y_{ast{;lt zHV)8QV}W6c@cSX%J*t6oF-4l?3aW)dMEphePPMtEpbjt)BRlz`PwqQ6J%5hRP~1-< z0j})=|41IP>=L!PA0RSY;LvylWKs*evs3;kx&!ZJtHx@*UYQ*d$bdyGAt};u_*3)a z*>|~NzgGQR!-V;_&se9k?ddAb;rBQReslUNFrm@iF=@Id*JxE8!E3aD66dt!uMko| zVv0^{zNp#HiBz~5=CYM%khghDeU?VOB)e;Db&U}6?oB>uyf_T#W=v#;q9}kVQkk2rn!#YxxuTgb1QrK)@}$cQWo|BbaK-i_7OA;j^ru0wR+ns5gFkPF zIxw2=YC5hg-Li8Q{%ZA$M*X;N7U?Jnf?2%{q-mQ#3%jBVL1i+^l!YOt?@$Bvd@d5~ zCr9D;U17$7sc??<6rl+6{B;4z(3(=h71d-kuM6#T>C0Z;A7N(~`Pk8uIZ>Lu!vFpm z4vJPoYxFG!hP+Z)I#4(9*D#u#9V&3li(42k(Ln7`1nbKk6n>rk za3%bKuDvAnb-eTMK5Dx{dvPUm@Zs;+O_Q^;tGjT3@{oIOD@mFC0F4N^nLJUG<#^&> zIJ*D31~E!x#lxJrcEt3)76ZQYY(_yQsi~c;OhIE}t3i(S#mqQVH9ef=Ah9bCPp339 zbB#fq&JD%&KP#b!6wX90XqIAUVK>jLG~#>!Q;>%4P53 zEOXS2fR~C|&n~AL1eaac9e=-R!h)-|r=jp8gR2**3l7hG74)^fv)|(~SiiUmN~OkM zI~1Q1q;MFef#)dm4j#gN`d+N203zb-vW(`<^1_Zi%@4<_u;xs2+!@ zi}0RF#=3w0&&W?8_2$GA1L+HEE{cWt=h3DmdraGJuz**Dogmc`HQLNe-uBrCR;1n_ zCljGqeRHn+@oLsEbBgh=gcfp)(Aa1e7R6h-ug_DTMX2Y3KMq)$CV~UVz6D!zyGzkkkt@@=fwQpOvS;>HWAR$tW%S&cqn#($+@d-mZ`6%~IarBBEA z#&_fIJnq-h=RX#UTI19%!-=e?K+4h_u3nK34NKR+7r7aNzW!nh;9izb{}TYX&iVu8@u&00m9V{hA204S+>-FlTCh6S#I=W>*Ws zRlDG0=Sgj&^L*uze>v(sfe(QX%Wm7O={StPXQFix{~RH9cpXY#CZ#$)CKGe@Ka1a{2c_!pbruCK~xmyp6J zh2vFJ?aLqni~anrxjh&(AIrEE8X$Pt7FkhD8`PYSzy*2w+{1?2RhhWBC+xk!u(z$b(WLbhBs+%+gJYa3v9y2^g{sQAGeP|7&QyL5?4epU&t ziYF>4gnTKICtd8`gQr5@>0T9|<6aM$?uTqQ37^xKCs(UtO@J|%IT%{M^I%hls2+$D|w7N@v}u@NL2W{bOfgC9@*(fha3xmvhHTgj*GBmo2Y| z(^5wp?0|Y7Y%TqDjk)D3NUTD>Dt5za^+oB$*nHU4(s+6VaNQxYwrEV)Hkzb?x@8E= zK_)RC<+l|s@J;QDd_z)hoR%coUlOI%J>r1vX(70}{>oPsGI(4^!{d#{{j-`^pXZs{ zO}{wF$@6;@$T%9$fjt0yX%OUbh&&1GA~%F+z4cut57D))GeKvI3We{JZ~U7ad7yab z1Gh|BdvyoFYy5JS-qRBFvuyv+Ls2ViB>IbM?qd(0ocuNpDWve(Exsl)Wcjx3b3eMX zv}l{OQ{$Z^%DJ=s;OFB#U8xdxpq zz)Xo=3ph65VZU!~{eYE|FQaFx@981}7#?+!veM!U>Hqs@_Sc+1D9KsSQr+ &`z< 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΋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%0AO8q|tunVSVfJ;*TB=HQk0qmg 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 n0CH8xhyy~-xIqz7l`}~djR=7X#MQr{5LT2+` Date: Wed, 5 Feb 2020 15:41:29 +1100 Subject: [PATCH 308/610] Fixes migration syntax --- .../Migrations/Upgrade/V_8_0_0/VariantsMigration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 5ff544eeab..87509a4ba7 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -215,12 +215,12 @@ JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nod WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); Database.Execute($@" -INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.PropertyData)} (propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,versionId) +INSERT INTO {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} (propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,versionId) SELECT propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,cverNew.id FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId -JOIN {SqlSyntax.GetQuotedTableName(PreTables.PropertyData)} pd ON pd.versionId=cver.id +JOIN {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} pd ON pd.versionId=cver.id WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); From d6b1e0d4c75ebd6ae33d28bd968b69b254ee5856 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 5 Feb 2020 16:02:40 +1100 Subject: [PATCH 309/610] formats sql onto new lines --- .../Migrations/Upgrade/V_8_0_0/VariantsMigration.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 57568d0a7d..3e813fe1cb 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -84,7 +84,9 @@ HAVING COUNT(v2.id) <> 1").Any()) } else { - Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id FROM {PreTables.ContentVersion} INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); + Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id +FROM {PreTables.ContentVersion} +INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); } Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); From 44e261b36af0b142a6cae18ac823f4c8f24ddbe3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 5 Feb 2020 22:04:28 +0100 Subject: [PATCH 310/610] V8: Improve treepicker keyboard navigation for trees with listviews (#7032) * Remember the current tab focus when opening a mini list view in the tree picker * Automatically assign focus to the search box in mini list views --- .../infiniteeditors/treepicker/treepicker.controller.js | 9 +++++++++ .../src/views/components/umb-mini-list-view.html | 1 + 2 files changed, 10 insertions(+) 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 0faee7bf90..3ec6a3f3cb 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 @@ -64,6 +64,8 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", var currentNode = $scope.model.currentNode; + var previouslyFocusedElement = null; + function initDialogTree() { vm.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); // TODO: Also deal with unexpanding!! @@ -487,6 +489,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", } function openMiniListView(node) { + previouslyFocusedElement = document.activeElement; vm.miniListView = node; } @@ -650,6 +653,12 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", function closeMiniListView() { vm.miniListView = undefined; + if (previouslyFocusedElement) { + $timeout(function () { + previouslyFocusedElement.focus(); + previouslyFocusedElement = null; + }); + } } function listViewItemsLoaded(items) { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html index da1e5c3aa7..d66e7f7f90 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -43,6 +43,7 @@ ng-model="search" ng-change="searchMiniListView(search, miniListView)" prevent-enter-submit + umb-auto-focus no-dirty-check>

JviYUnrecn)Zl`G15Hb45BYCTrQ|5qXLH_%Y$0 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乕-TJYa)O6XNfG;eW70fRmbMa|FL+?k>qXXauu{?e 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 T1 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�S-7)_2TJ4U21+fzk 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

From 97aa19c7a609a05848f076e4fa7d7c8b229aba1b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 6 Feb 2020 08:48:25 +0000 Subject: [PATCH 311/610] Merge pull request #7556 from umbraco/dependabot/npm_and_yarn/src/Umbraco.Web.UI.Client/tinymce-4.9.7 8.6RC: Bump tinymce from 4.9.2 to 4.9.7 in /src/Umbraco.Web.UI.Client (cherry picked from commit 45e4e39412b2047937ec60c7351568f6c77ce9ee) --- src/Umbraco.Web.UI.Client/package-lock.json | 6 +++--- src/Umbraco.Web.UI.Client/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index dd58b5ca18..95beae8316 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -15313,9 +15313,9 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "tinymce": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.2.tgz", - "integrity": "sha512-ZRoTGG4GAsOI73QPSNkabO7nkoYw9H6cglRB44W2mMkxSiqxYi8WJlgkUphk0fDqo6ZD6r3E+NSP4UHxF2lySg==" + "version": "4.9.7", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.7.tgz", + "integrity": "sha512-cj0HvUuniTuIjOAJdRt5BUfeQqM5yHjbA2NOub9HUHXlCrT9OwD9WBPU6tGlaPC2l2I4eGoOnT8llosZRdQU5Q==" }, "tmp": { "version": "0.0.33", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index beff2b45d1..508409b4ea 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -42,7 +42,7 @@ "npm": "6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", - "tinymce": "4.9.2", + "tinymce": "4.9.7", "typeahead.js": "0.11.1", "underscore": "1.9.1" }, From 555be8dd0adaa534961831d9986d0dc2483616f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 6 Feb 2020 12:47:50 +0100 Subject: [PATCH 312/610] first part of enabling allow segments --- .../components/umbgroupsbuilder.directive.js | 3 ++ .../propertysettings.controller.js | 5 +++ .../propertysettings/propertysettings.html | 44 ++++++++++++------- .../views/components/umb-groups-builder.html | 9 +++- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 10 ++++- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 12 ++++- .../Umbraco/config/lang/en_us.xml | 12 ++++- .../ContentEditing/DocumentTypeDisplay.cs | 5 ++- .../ContentEditing/PropertyTypeBasic.cs | 3 ++ .../Mapping/ContentTypeMapDefinition.cs | 5 ++- 10 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index a9b9cc52b1..6f76828f5a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -522,6 +522,7 @@ oldPropertyModel.allowCultureVariant = scope.model.allowCultureVariant; oldPropertyModel.alias = ""; } + // SEGMENTS_TODO: test if we need to do the same for the segments ^^ var propertyModel = angular.copy(property); var propertySettings = { @@ -530,6 +531,7 @@ contentType: scope.contentType, contentTypeName: scope.model.name, contentTypeAllowCultureVariant: scope.model.allowCultureVariant, + contentTypeAllowSegmentVariant: scope.model.allowSegmentVariant, view: "views/common/infiniteeditors/propertysettings/propertysettings.html", size: "small", submit: function(model) { @@ -557,6 +559,7 @@ property.isSensitiveData = propertyModel.isSensitiveData; property.isSensitiveValue = propertyModel.isSensitiveValue; property.allowCultureVariant = propertyModel.allowCultureVariant; + property.allowSegmentVariant = propertyModel.allowSegmentVariant; // update existing data types if(model.updateSameDataTypes) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js index b8581d28d0..fbf7e7de3d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js @@ -30,6 +30,7 @@ vm.close = close; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.toggleValidation = toggleValidation; vm.toggleShowOnMemberProfile = toggleShowOnMemberProfile; vm.toggleMemberCanEdit = toggleMemberCanEdit; @@ -246,6 +247,10 @@ $scope.model.property.allowCultureVariant = toggleValue($scope.model.property.allowCultureVariant); } + function toggleAllowSegmentVariants() { + $scope.model.property.allowSegmentVariant = toggleValue($scope.model.property.allowSegmentVariant); + } + function toggleValidation() { $scope.model.property.validation.mandatory = toggleValue($scope.model.property.validation.mandatory); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 4474390199..029bd2b235 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -40,7 +40,7 @@ - +
- +
- +
@@ -132,9 +132,9 @@
- -
- + +
+
- + +
+ +
+ + + + +
+
@@ -176,7 +188,7 @@ checked="model.property.isSensitiveData" on-click="vm.toggleIsSensitiveData()"> - +
@@ -206,7 +218,7 @@ label-key="general_submit" action="vm.submit(model)"> - + 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 a5ec479dfd..7716d81b41 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 @@ -37,7 +37,7 @@ - +
    @@ -215,7 +215,12 @@
    - + +
    + +
    + +
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 78075d9b7a..4f3a0082dd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -780,6 +780,7 @@ Generelt Editor Skift tillad sprogvarianter + Skift tillad segmentering Baggrundsfarve @@ -1405,9 +1406,16 @@ Mange hilsner fra Umbraco robotten fane har ingen sorteringsrækkefølge Hvor er denne komposition brugt? Denne komposition brugt i kompositionen af de følgende indholdstyper: - Tillad sprogvariation + Tillad variationer + Tillad sprogvariation + Tillad segmentering + Tillader sprogvariationer + Tillader segmentering Tillad at redaktører kan oprette indhold af denne type på flere sprog. + Tillad at redaktører kan oprette dette indhold på flere sprog. + Tillad at redaktører kan oprette flere udgaver af denne type indhold. Tillad sprogvariation + Tillad segmentering Element-type Er en Element-type En Element-type er tiltænkt brug i f.eks. Nested Content, ikke i indholdstræet. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 85b23f53ed..2b5d537ca7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -795,6 +795,7 @@ General Editor Toggle allow culture variants + Toggle allow segmentation Background colour @@ -1644,9 +1645,16 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: - Allow varying by culture + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. Allow varying by culture + Allow segmentation Element type Is an Element type An Element type is meant to be used for instance in Nested Content, and not in the tree. @@ -2385,7 +2393,7 @@ To manage your website, simply open the Umbraco back office and start adding con Welcome to The Friendly CMS Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - + Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! 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 fb1ed6fa63..3ed499e8d9 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -796,6 +796,7 @@ General Editor Toggle allow culture variants + Toggle allow segmentation Background color @@ -1654,9 +1655,16 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: - Allow varying by culture + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. Allow varying by culture + Allow segmentation Element type Is an element type An element type is meant to be used for instance in Nested Content, and not in the tree. @@ -2397,7 +2405,7 @@ To manage your website, simply open the Umbraco back office and start adding con Welcome to The Friendly CMS Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - + Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! diff --git a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs index 6b82f74ca7..9467033aec 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs @@ -20,9 +20,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "defaultTemplate")] public EntityBasic DefaultTemplate { get; set; } - + [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } + } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs index d180a68a2c..5ff1744ea2 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs @@ -54,5 +54,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index cdd9ad74eb..1f78c7372d 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -121,6 +121,7 @@ namespace Umbraco.Web.Models.Mapping MapTypeToDisplayBase(source, target); target.AllowCultureVariant = source.VariesByCulture(); + target.AllowSegmentVariant = source.VariesBySegment(); //sync templates target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates); @@ -227,7 +228,7 @@ namespace Umbraco.Web.Models.Mapping target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); - + if (source.Id > 0) target.Id = source.Id; @@ -338,6 +339,7 @@ namespace Umbraco.Web.Models.Mapping { target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; target.DataTypeId = source.DataTypeId; target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; @@ -354,6 +356,7 @@ namespace Umbraco.Web.Models.Mapping { target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; target.DataTypeId = source.DataTypeId; target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; From fbd95f1c8edfc3afc45f3e758b7894a393a40b86 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 6 Feb 2020 15:10:05 +0100 Subject: [PATCH 313/610] Fixes problem with #7530 where UmbracoContext is empty when HtmlImageSourceParser gets initialized --- .../Templates/HtmlImageSourceParser.cs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs index dcc5318b89..9a3d86c9da 100644 --- a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -13,14 +13,10 @@ namespace Umbraco.Web.Templates this._getMediaUrl = getMediaUrl; } + private readonly IUmbracoContextAccessor _umbracoContextAccessor; public HtmlImageSourceParser(IUmbracoContextAccessor umbracoContextAccessor) { - if (umbracoContextAccessor?.UmbracoContext?.UrlProvider == null) - { - return; - } - - _getMediaUrl = (guid) => umbracoContextAccessor.UmbracoContext.UrlProvider.GetMediaUrl(guid); + _umbracoContextAccessor = umbracoContextAccessor; } private static readonly Regex ResolveImgPattern = new Regex(@"(]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)", @@ -29,7 +25,7 @@ namespace Umbraco.Web.Templates private static readonly Regex DataUdiAttributeRegex = new Regex(@"data-udi=\\?(?:""|')(?umb://[A-z0-9\-]+/[A-z0-9]+)\\?(?:""|')", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - private readonly Func _getMediaUrl; + private Func _getMediaUrl; /// /// Parses out media UDIs from an html string based on 'data-udi' html attributes @@ -57,12 +53,11 @@ namespace Umbraco.Web.Templates /// Umbraco image tags are identified by their data-udi attributes public string EnsureImageSources(string text) { - // no point in doing any processing if we don't have - // a function to retrieve Urls - if (_getMediaUrl == null) - { - return text; - } + if (_umbracoContextAccessor.UmbracoContext == null) + throw new InvalidOperationException("Could not parse image sources, there is no current UmbracoContext"); + + if(_getMediaUrl == null) + _getMediaUrl = (guid) => _umbracoContextAccessor.UmbracoContext.UrlProvider.GetMediaUrl(guid); return ResolveImgPattern.Replace(text, match => { From b182141d6c36782d54702d49c2660900f74140fc Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 6 Feb 2020 17:33:22 +0100 Subject: [PATCH 314/610] Fix jumping section name on focus --- src/Umbraco.Web.UI.Client/src/less/sections.less | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 40921c5b76..27b11b1c3b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -41,13 +41,13 @@ ul.sections { &:focus .section__name { .tabbing-active & { - border: 1px solid; - border-color: @gray-9; + border: 1px solid @gray-9; } } } .section__name { + border: 1px solid transparent; border-radius: 3px; margin-top: 1px; padding: 3px 10px 4px 10px; @@ -75,9 +75,9 @@ ul.sections { opacity: 0.6; transition: opacity .1s linear; } - + &:hover i { - opacity:1; + opacity: 1; } } From d285d40578522249f2e902f8ef0820db154df1f3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 6 Feb 2020 17:52:36 +0100 Subject: [PATCH 315/610] That safeguard unfortunately broke all the tests --- src/Umbraco.Web/Templates/HtmlImageSourceParser.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs index 9a3d86c9da..539ce303fd 100644 --- a/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Web/Templates/HtmlImageSourceParser.cs @@ -53,9 +53,6 @@ namespace Umbraco.Web.Templates /// Umbraco image tags are identified by their data-udi attributes public string EnsureImageSources(string text) { - if (_umbracoContextAccessor.UmbracoContext == null) - throw new InvalidOperationException("Could not parse image sources, there is no current UmbracoContext"); - if(_getMediaUrl == null) _getMediaUrl = (guid) => _umbracoContextAccessor.UmbracoContext.UrlProvider.GetMediaUrl(guid); From 02a9645ed17cc6b31cafc3cd1edfd745dabb7809 Mon Sep 17 00:00:00 2001 From: Enkel Media Date: Fri, 7 Feb 2020 00:03:53 +0100 Subject: [PATCH 316/610] #6812 AuditService some methods not returning any values (#6822) * #6812 Worked on AuditService some methods not returning any values * Changed userId to include also admin-user * Use Constant for "SuperUserId" over magic int Co-authored-by: markusobviuse <36044239+markusobviuse@users.noreply.github.com> --- .../Persistence/Repositories/Implement/AuditRepository.cs | 3 ++- src/Umbraco.Core/Services/Implement/AuditService.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index 0788594e3a..4cb533e86f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -77,7 +77,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public IEnumerable Get(AuditType type, IQuery query) { var sqlClause = GetBaseQuery(false) - .Where(x => x.Header == type.ToString()); + .Where("(logHeader=@0)", type.ToString()); + var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); diff --git a/src/Umbraco.Core/Services/Implement/AuditService.cs b/src/Umbraco.Core/Services/Implement/AuditService.cs index 5eb08f2dea..7d3be1d52b 100644 --- a/src/Umbraco.Core/Services/Implement/AuditService.cs +++ b/src/Umbraco.Core/Services/Implement/AuditService.cs @@ -142,7 +142,7 @@ namespace Umbraco.Core.Services.Implement if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); - if (userId < 0) + if (userId < Constants.Security.SuperUserId) { totalRecords = 0; return Enumerable.Empty(); From 636d0c9ccd46f55c629b5a828264915c8bd389d8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 7 Feb 2020 12:18:27 +1100 Subject: [PATCH 317/610] Ensures that we track relations for values on all variant property values --- .../DataValueReferenceFactoryCollection.cs | 51 ++-- ...ataValueReferenceFactoryCollectionTests.cs | 223 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 3 files changed, 255 insertions(+), 20 deletions(-) create mode 100644 src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 386ab6a8f3..e7b051e17f 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -13,35 +13,46 @@ namespace Umbraco.Core.PropertyEditors public IEnumerable GetAllReferences(PropertyCollection properties, PropertyEditorCollection propertyEditors) { - var trackedRelations = new List(); + var trackedRelations = new HashSet(); foreach (var p in properties) { if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here - if (!p.PropertyType.VariesByNothing()) continue; - var val = p.GetValue(); // get the invariant value + //TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants - var valueEditor = editor.GetValueEditor(); - if (valueEditor is IDataValueReference reference) + foreach(var propertyVal in p.Values) { - var refs = reference.GetReferences(val); - trackedRelations.AddRange(refs); - } + var val = propertyVal.EditedValue; - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (var item in this) - { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media/content items - if (item.IsForEditor(editor)) - trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); + var valueEditor = editor.GetValueEditor(); + if (valueEditor is IDataValueReference reference) + { + var refs = reference.GetReferences(val); + foreach(var r in refs) + trackedRelations.Add(r); + } + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) + { + foreach(var r in item.GetDataValueReference().GetReferences(val)) + trackedRelations.Add(r); + } + + } } + + } return trackedRelations; diff --git a/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs new file mode 100644 index 0000000000..ae6f80b32d --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -0,0 +1,223 @@ +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PropertyEditors; +using static Umbraco.Core.Models.Property; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class DataValueReferenceFactoryCollectionTests + { + [Test] + public void GetAllReferences_All_Variants_With_IDataValueReferenceFactory() + { + var collection = new DataValueReferenceFactoryCollection(new TestDataValueReferenceFactory().Yield()); + + // label does not implement IDataValueReference + var labelEditor = new LabelPropertyEditor(Mock.Of()); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(labelEditor.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(new DataType(labelEditor)) + { + Variations = ContentVariation.CultureAndSegment + }) + { + Values = new List + { + // Ignored (no culture) + new PropertyValue + { + EditedValue = trackedUdi1 + }, + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + // Ignored (no culture) + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Culture = "en-US", + Segment = "B", + EditedValue = trackedUdi3 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi2, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi3, result.ElementAt(1).Udi.ToString()); + } + + [Test] + public void GetAllReferences_All_Variants_With_IDataValueReference_Editor() + { + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty()); + + // mediaPicker does implement IDataValueReference + var mediaPicker = new MediaPickerPropertyEditor(Mock.Of()); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(mediaPicker.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(new DataType(mediaPicker)) + { + Variations = ContentVariation.CultureAndSegment + }) + { + Values = new List + { + // Ignored (no culture) + new PropertyValue + { + EditedValue = trackedUdi1 + }, + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + // Ignored (no culture) + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Culture = "en-US", + Segment = "B", + EditedValue = trackedUdi3 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi2, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi3, result.ElementAt(1).Udi.ToString()); + } + + [Test] + public void GetAllReferences_Invariant_With_IDataValueReference_Editor() + { + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty()); + + // mediaPicker does implement IDataValueReference + var mediaPicker = new MediaPickerPropertyEditor(Mock.Of()); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(mediaPicker.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(new DataType(mediaPicker)) + { + Variations = ContentVariation.Nothing | ContentVariation.Segment + }) + { + Values = new List + { + new PropertyValue + { + EditedValue = trackedUdi1 + }, + // Ignored (has culture) + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + // Ignored (has culture) + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Segment = "B", + EditedValue = trackedUdi4 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi1, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi4, result.ElementAt(1).Udi.ToString()); + } + + private class TestDataValueReferenceFactory : IDataValueReferenceFactory + { + public IDataValueReference GetDataValueReference() => new TestMediaDataValueReference(); + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias == Constants.PropertyEditors.Aliases.Label; + + private class TestMediaDataValueReference : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + // This is the same as the media picker, it will just try to parse the value directly as a UDI + + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index ff923bb04b..e6f273301f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -145,6 +145,7 @@ + From a49af7ab9b8556699fccbede1aa83d2547d767f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 7 Feb 2020 12:49:43 +0100 Subject: [PATCH 318/610] convert to hex color code --- src/Umbraco.Web.UI.Client/src/less/variables.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 674c8d08e9..51b854cf78 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -118,7 +118,7 @@ //@orange: #f79c37;// updated 2019 @pink: #D93F4C;// #C3325F;// update 2019 @pinkLight: #f5c1bc;// added 2019 -@pinkExtraLight: hsl(5, 84%, 94%);// added 2019 +@pinkExtraLight: #fde5e3;// added 2020 @pinkRedLight: #ff8a89;// added 2019 @brown: #9d8057;// added 2019 @brownLight: #e4e0dd;// added 2019 From aed0a09ea37238272e5fdf160747c2e5c61a0b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 7 Feb 2020 13:38:01 +0100 Subject: [PATCH 319/610] indentation of border for variant that are in not-created state --- .../src/less/components/editor/umb-variant-switcher.less | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index c021391775..6814f9cf44 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -298,6 +298,11 @@ button.umb-variant-switcher__toggle { border-bottom-color: @gray-10; } + .umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::after { + left: 55px;// overwrite left to achieve same indention on the dashed border as language. + } + .umb-variant-switcher__name-wrapper { margin-left: 48px; @@ -317,5 +322,7 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__state { //flex: 0 0 200px; } + + } } From c9621ca8be29ada6b818cfb1b2f2f92a6021de85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 7 Feb 2020 13:39:27 +0100 Subject: [PATCH 320/610] comment change --- .../src/less/components/editor/umb-variant-switcher.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index 6814f9cf44..eb44a3a96a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -300,7 +300,7 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__item.--state-notCreated:not(.--active) { .umb-variant-switcher__name-wrapper::after { - left: 55px;// overwrite left to achieve same indention on the dashed border as language. + left: 55px;// overwrite left to achieve same indentation on the dashed border as language. } .umb-variant-switcher__name-wrapper { From a7ff20de6e62ae47bae3dde63f9ce3708034a3fc Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Fri, 7 Feb 2020 15:01:03 -0800 Subject: [PATCH 321/610] Introduce Image URL Generator abstraction --- src/Umbraco.Core/Composing/Current.cs | 3 + src/Umbraco.Core/Models/IImageUrlGenerator.cs | 7 + .../Models/ImageUrlGenerationOptions.cs | 35 ++ src/Umbraco.Core/Models/UserExtensions.cs | 11 +- .../ValueConverters/ImageCropperValue.cs | 63 ++- src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../PropertyEditors/ImageCropperTest.cs | 98 +++-- .../PublishedContentTestBase.cs | 3 +- .../PublishedContent/PublishedContentTests.cs | 3 +- src/Umbraco.Tests/Runtimes/StandaloneTests.cs | 1 + src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 + .../Views/Partials/Grid/Editors/Media.cshtml | 22 +- src/Umbraco.Web/Editors/ImagesController.cs | 18 +- .../ImageCropperTemplateCoreExtensions.cs | 389 ++++++++++++++++++ .../ImageCropperTemplateExtensions.cs | 186 +-------- .../Models/ImageProcessorImageUrlGenerator.cs | 108 +++++ .../PropertyEditors/GridPropertyEditor.cs | 33 +- .../RichTextEditorPastedImages.cs | 4 +- .../PropertyEditors/RichTextPropertyEditor.cs | 20 +- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 4 + .../Templates/TemplateUtilities.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 + 22 files changed, 747 insertions(+), 269 deletions(-) create mode 100644 src/Umbraco.Core/Models/IImageUrlGenerator.cs create mode 100644 src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs create mode 100644 src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs create mode 100644 src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index 846331a16e..a06f09baf6 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Dictionary; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PackageActions; using Umbraco.Core.Packaging; @@ -208,6 +209,8 @@ namespace Umbraco.Core.Composing public static IVariationContextAccessor VariationContextAccessor => Factory.GetInstance(); + public static IImageUrlGenerator ImageUrlGenerator + => Factory.GetInstance(); #endregion } } diff --git a/src/Umbraco.Core/Models/IImageUrlGenerator.cs b/src/Umbraco.Core/Models/IImageUrlGenerator.cs new file mode 100644 index 0000000000..0dc2933fd9 --- /dev/null +++ b/src/Umbraco.Core/Models/IImageUrlGenerator.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Models +{ + public interface IImageUrlGenerator + { + string GetImageUrl(ImageUrlGenerationOptions options); + } +} diff --git a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs new file mode 100644 index 0000000000..9e8d3f3a00 --- /dev/null +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -0,0 +1,35 @@ +namespace Umbraco.Core.Models +{ + public class ImageUrlGenerationOptions + { + public string ImageUrl { get; set; } + public int? Width { get; set; } + public int? Height { get; set; } + public decimal? WidthRatio { get; set; } + public decimal? HeightRatio { get; set; } + public int? Quality { get; set; } + public string ImageCropMode { get; set; } + public string ImageCropAnchor { get; set; } + public bool DefaultCrop { get; set; } + public FocalPointPosition FocalPoint { get; set; } + public CropCoordinates Crop { get; set; } + public string CacheBusterValue { get; set; } + public string FurtherOptions { get; set; } + public bool UpScale { get; set; } = true; + public string AnimationProcessMode { get; set; } + + public class FocalPointPosition + { + public decimal Left { get; set; } + public decimal Top { get; set; } + } + + public class CropCoordinates + { + public decimal X1 { get; set; } + public decimal Y1 { get; set; } + public decimal X2 { get; set; } + public decimal Y2 { get; set; } + } + } +} diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index e00ac4ba15..f11a8efe46 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -106,13 +106,14 @@ namespace Umbraco.Core.Models //use the custom avatar var avatarUrl = Current.MediaFileSystem.GetUrl(user.Avatar); + var urlGenerator = Current.ImageUrlGenerator; return new[] { - avatarUrl + "?width=30&height=30&mode=crop", - avatarUrl + "?width=60&height=60&mode=crop", - avatarUrl + "?width=90&height=90&mode=crop", - avatarUrl + "?width=150&height=150&mode=crop", - avatarUrl + "?width=300&height=300&mode=crop" + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 30, Height = 30 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 60, Height = 60 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 90, Height = 90 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 150, Height = 150 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 300, Height = 300 }) }; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs index b211272b51..70eb7e0477 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -7,6 +7,8 @@ using System.Runtime.Serialization; using System.Text; using System.Web; using Newtonsoft.Json; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; using Umbraco.Core.Serialization; namespace Umbraco.Core.PropertyEditors.ValueConverters @@ -59,38 +61,34 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters : Crops.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } - internal void AppendCropBaseUrl(StringBuilder url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint) + internal ImageUrlGenerationOptions GetCropBaseOptions(string url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint) { if (preferFocalPoint && HasFocalPoint() || crop != null && crop.Coordinates == null && HasFocalPoint() || defaultCrop && HasFocalPoint()) { - url.Append("?center="); - url.Append(FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); - url.Append(","); - url.Append(FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); - url.Append("&mode=crop"); + return new ImageUrlGenerationOptions { ImageUrl = url, FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition { Left = FocalPoint.Left, Top = FocalPoint.Top } }; } else if (crop != null && crop.Coordinates != null && preferFocalPoint == false) { - url.Append("?crop="); - url.Append(crop.Coordinates.X1.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.X2.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.Y2.ToString(CultureInfo.InvariantCulture)); - url.Append("&cropmode=percentage"); + return new ImageUrlGenerationOptions { ImageUrl = url, Crop = new ImageUrlGenerationOptions.CropCoordinates { X1 = crop.Coordinates.X1, X2 = crop.Coordinates.X2, Y1 = crop.Coordinates.Y1, Y2 = crop.Coordinates.Y2 } }; } else { - url.Append("?anchor=center"); - url.Append("&mode=crop"); + return new ImageUrlGenerationOptions { ImageUrl = url, DefaultCrop = true }; } } /// /// Gets the value image url for a specified crop. /// - public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) + [Obsolete("Use the overload that takes an IImageUrlGenerator")] + public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(alias, Current.ImageUrlGenerator, useCropDimensions, useFocalPoint, cacheBusterValue); + + /// + /// Gets the value image url for a specified crop. + /// + public string GetCropUrl(string alias, IImageUrlGenerator imageUrlGenerator, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) { var crop = GetCrop(alias); @@ -98,38 +96,37 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (crop == null && !string.IsNullOrWhiteSpace(alias)) return null; - var url = new StringBuilder(); - - AppendCropBaseUrl(url, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint); + var options = GetCropBaseOptions(string.Empty, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint); if (crop != null && useCropDimensions) { - url.Append("&width=").Append(crop.Width); - url.Append("&height=").Append(crop.Height); + options.Width = crop.Width; + options.Height = crop.Height; } - if (cacheBusterValue != null) - url.Append("&rnd=").Append(cacheBusterValue); + options.CacheBusterValue = cacheBusterValue; - return url.ToString(); + return imageUrlGenerator.GetImageUrl(options); } /// /// Gets the value image url for a specific width and height. /// - public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null) + [Obsolete("Use the overload that takes an IImageUrlGenerator")] + public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(width, height, Current.ImageUrlGenerator, useFocalPoint, cacheBusterValue); + + /// + /// Gets the value image url for a specific width and height. + /// + public string GetCropUrl(int width, int height, IImageUrlGenerator imageUrlGenerator, bool useFocalPoint = false, string cacheBusterValue = null) { - var url = new StringBuilder(); + var options = GetCropBaseOptions(string.Empty, null, true, useFocalPoint); - AppendCropBaseUrl(url, null, true, useFocalPoint); + options.Width = width; + options.Height = height; + options.CacheBusterValue = cacheBusterValue; - url.Append("&width=").Append(width); - url.Append("&height=").Append(height); - - if (cacheBusterValue != null) - url.Append("&rnd=").Append(cacheBusterValue); - - return url.ToString(); + return imageUrlGenerator.GetImageUrl(options); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1fc3709e88..3cf7ba7839 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,6 +132,8 @@ + + diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 433ba64b38..62c0792f7b 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -21,6 +21,7 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Web.Models; using Umbraco.Web; using Umbraco.Web.PropertyEditors; +using System.Text; namespace Umbraco.Tests.PropertyEditors { @@ -110,7 +111,7 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_CropAliasTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true); Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); } @@ -120,28 +121,28 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_CropAliasIgnoreWidthHeightTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, width: 50, height: 50); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, width: 50, height: 50); Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); } [Test] public void GetCropUrl_WidthHeightTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 200, height: 300); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 200, height: 300); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300", urlString); } [Test] public void GetCropUrl_FocalPointTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "thumb", preferFocalPoint: true, useCropDimensions: true); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "thumb", preferFocalPoint: true, useCropDimensions: true); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=100&height=100", urlString); } [Test] public void GetCropUrlFurtherOptionsTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 200, height: 300, furtherOptions: "&filter=comic&roundedcorners=radius-26|bgcolor-fff"); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 200, height: 300, furtherOptions: "&filter=comic&roundedcorners=radius-26|bgcolor-fff"); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); } @@ -151,7 +152,7 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrlNullTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Banner", useCropDimensions: true); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Banner", useCropDimensions: true); Assert.AreEqual(null, urlString); } @@ -162,7 +163,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetBaseCropUrlFromModelTest() { var cropDataSet = CropperJson1.DeserializeImageCropperValue(); - var urlString = cropDataSet.GetCropUrl("thumb"); + var urlString = cropDataSet.GetCropUrl("thumb", new TestImageUrlGenerator()); Assert.AreEqual("?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); } @@ -172,7 +173,7 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_CropAliasHeightRatioModeTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, ratioMode:ImageCropRatioMode.Height); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, ratioMode:ImageCropRatioMode.Height); Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&heightratio=1", urlString); } @@ -182,7 +183,7 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_WidthHeightRatioModeTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode:ImageCropRatioMode.Height); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode:ImageCropRatioMode.Height); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=300&heightratio=0.5", urlString); } @@ -192,7 +193,7 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_HeightWidthRatioModeTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode: ImageCropRatioMode.Width); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode: ImageCropRatioMode.Width); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&height=150&widthratio=2", urlString); } @@ -202,11 +203,11 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_SpecifiedCropModeTest() { - var urlStringMin = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Min); - var urlStringBoxPad = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.BoxPad); - var urlStringPad = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Pad); - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode:ImageCropMode.Max); - var urlStringStretch = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); + var urlStringMin = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Min); + var urlStringBoxPad = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.BoxPad); + var urlStringPad = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Pad); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode:ImageCropMode.Max); + var urlStringStretch = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); Assert.AreEqual(MediaPath + "?mode=min&width=300&height=150", urlStringMin); Assert.AreEqual(MediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); @@ -221,7 +222,7 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_UploadTypeTest() { - var urlString = MediaPath.GetCropUrl(width: 100, height: 270, imageCropMode: ImageCropMode.Crop, imageCropAnchor: ImageCropAnchor.Center); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), width: 100, height: 270, imageCropMode: ImageCropMode.Crop, imageCropAnchor: ImageCropAnchor.Center); Assert.AreEqual(MediaPath + "?mode=crop&anchor=center&width=100&height=270", urlString); } @@ -233,7 +234,7 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, width: 300, height: 150, preferFocalPoint:true); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, width: 300, height: 150, preferFocalPoint:true); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString); } @@ -245,7 +246,7 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); } @@ -257,7 +258,7 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200); Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); } @@ -269,7 +270,7 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200, useCropDimensions: true); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200, useCropDimensions: true); Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&width=270&height=161", urlString); } @@ -281,7 +282,7 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", height: 200); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", height: 200); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&widthratio=1.6770186335403726708074534161&height=200", urlString); } @@ -293,7 +294,7 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, width: 200); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, width: 200); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=200", urlString); } @@ -305,7 +306,7 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, height: 200); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, height: 200); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&height=200", urlString); } @@ -317,8 +318,57 @@ namespace Umbraco.Tests.PropertyEditors { var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"" + MediaPath + "\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(400, 400, cropperJson, imageCropMode: ImageCropMode.Pad, furtherOptions: "&bgcolor=fff"); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), 400, 400, cropperJson, imageCropMode: ImageCropMode.Pad, furtherOptions: "&bgcolor=fff"); Assert.AreEqual(MediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); } + + private class TestImageUrlGenerator : IImageUrlGenerator + { + public string GetImageUrl(ImageUrlGenerationOptions options) + { + var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty); + + if (options.FocalPoint != null) + { + imageProcessorUrl.Append("?center="); + imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append(","); + imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&mode=crop"); + } + else if (options.Crop != null) + { + imageProcessorUrl.Append("?crop="); + imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&cropmode=percentage"); + } + else if (options.DefaultCrop) + { + imageProcessorUrl.Append("?anchor=center&mode=crop"); + } + else + { + imageProcessorUrl.Append("?mode=" + options.ImageCropMode.ToString().ToLower()); + if (options.ImageCropAnchor != null)imageProcessorUrl.Append("&anchor=" + options.ImageCropAnchor.ToString().ToLower()); + } + + var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format="); + if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&quality=" + options.Quality); + if (options.HeightRatio != null) imageProcessorUrl.Append("&heightratio=" + options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.WidthRatio != null) imageProcessorUrl.Append("&widthratio=" + options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.Width != null) imageProcessorUrl.Append("&width=" + options.Width); + if (options.Height != null) imageProcessorUrl.Append("&height=" + options.Height); + if (options.UpScale == false) imageProcessorUrl.Append("&upscale=false"); + if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&animationprocessmode=" + options.AnimationProcessMode); + if (options.FurtherOptions != null) imageProcessorUrl.Append(options.FurtherOptions); + if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&quality=" + options.Quality); + if (options.CacheBusterValue != null) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); + + return imageProcessorUrl.ToString(); + } + } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index a96cad4076..59791fc645 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -12,6 +12,7 @@ using Umbraco.Web.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web; using Umbraco.Web.Templates; +using Umbraco.Web.Models; namespace Umbraco.Tests.PublishedContent { @@ -46,7 +47,7 @@ namespace Umbraco.Tests.PublishedContent var pastedImages = new RichTextEditorPastedImages(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); var localLinkParser = new HtmlLocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages)) { Id = 1 }); + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, Mock.Of())) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index be63e3b5da..998fc92380 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -22,6 +22,7 @@ using Umbraco.Tests.Testing; using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.PropertyEditors; using Umbraco.Web.Templates; +using Umbraco.Web.Models; namespace Umbraco.Tests.PublishedContent { @@ -53,7 +54,7 @@ namespace Umbraco.Tests.PublishedContent var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser, pastedImages)) { Id = 1002 }, + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser, pastedImages, Mock.Of())) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 }, new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 }); diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 7ab329b9a0..9157a76773 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -97,6 +97,7 @@ namespace Umbraco.Tests.Runtimes composition.Register(Lifetime.Singleton); composition.Register(Lifetime.Singleton); composition.Register(_ => Mock.Of(), Lifetime.Singleton); + composition.Register(_ => Mock.Of(), Lifetime.Singleton); composition.RegisterUnique(f => new DistributedCache()); composition.WithCollectionBuilder().Append(); composition.RegisterUnique(); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 4f7de2d230..468d4e8b03 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -43,6 +43,7 @@ using Current = Umbraco.Core.Composing.Current; using FileSystems = Umbraco.Core.IO.FileSystems; using Umbraco.Web.Templates; using Umbraco.Web.PropertyEditors; +using Umbraco.Core.Models; namespace Umbraco.Tests.Testing { @@ -248,6 +249,7 @@ namespace Umbraco.Tests.Testing var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Run); Composition.RegisterUnique(f => runtimeStateMock.Object); + Composition.Register(_ => Mock.Of()); // ah... Composition.WithCollectionBuilder(); diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml index ea79ce41ad..e20b717ed5 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml @@ -2,20 +2,24 @@ @using Umbraco.Web.Templates @if (Model.value != null) -{ +{ var url = Model.value.image; if(Model.editor.config != null && Model.editor.config.size != null){ - url += "?width=" + Model.editor.config.size.width; - url += "&height=" + Model.editor.config.size.height; - - if(Model.value.focalPoint != null){ - url += "¢er=" + Model.value.focalPoint.top +"," + Model.value.focalPoint.left; - url += "&mode=crop"; - } + url = ImageCropperTemplateExtensions.GetCropUrl(url, + width: Model.editor.config.size.width, + height: Model.editor.config.size.height, + cropDataSet: Model.value.focalPoint == null ? null : new Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue + { + FocalPoint = new Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue.ImageCropperFocalPoint + { + Top = Model.value.focalPoint.top, + Left = Model.value.focalPoint.left + } + }); } var altText = Model.value.altText ?? Model.value.caption ?? string.Empty; - + @altText if (Model.value.caption != null) diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index db6706d1bb..c8f0e2d9bf 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -2,8 +2,10 @@ using System.IO; using System.Net; using System.Net.Http; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Core.Models; using Umbraco.Web.Media; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -19,11 +21,17 @@ namespace Umbraco.Web.Editors { private readonly IMediaFileSystem _mediaFileSystem; private readonly IContentSection _contentSection; + private readonly IImageUrlGenerator _imageUrlGenerator; - public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection) + [Obsolete("This constructor will be removed in a future release. Please use the constructor with the IImageUrlGenerator overload")] + public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection) : this (mediaFileSystem, contentSection, Current.ImageUrlGenerator) + { + } + public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection, IImageUrlGenerator imageUrlGenerator) { _mediaFileSystem = mediaFileSystem; _contentSection = contentSection; + _imageUrlGenerator = imageUrlGenerator; } /// @@ -75,12 +83,10 @@ namespace Umbraco.Web.Editors // so ignore and we won't set a last modified date. } - // TODO: When we abstract imaging for netcore, we are actually just going to be abstracting a URI builder for images, this - // is one of those places where this can be used. + var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null; + var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = imagePath, UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd }); - var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : string.Empty; - - response.Headers.Location = new Uri($"{imagePath}?upscale=false&width={width}&animationprocessmode=first&mode=max{rnd}", UriKind.RelativeOrAbsolute); + response.Headers.Location = new Uri(imageUrl, UriKind.RelativeOrAbsolute); return response; } diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs new file mode 100644 index 0000000000..b8e832beb9 --- /dev/null +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -0,0 +1,389 @@ +using System; +using Newtonsoft.Json.Linq; +using System.Globalization; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.Models; + +namespace Umbraco.Web +{ + public static class ImageCropperTemplateCoreExtensions + { + /// + /// Gets the ImageProcessor Url by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item + /// + /// + /// The IPublishedContent item. + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// The ImageProcessor.Web Url. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); + } + + /// + /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The property alias of the property containing the Json data e.g. umbracoFile + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// The ImageProcessor.Web Url. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaItem.GetCropUrl(imageUrlGenerator, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + } + + /// + /// Gets the ImageProcessor Url from the IPublishedContent item. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// Property alias of the property containing the Json data. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point, to generate an output image using the focal point instead of the predefined crop + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this IPublishedContent mediaItem, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (mediaItem == null) throw new ArgumentNullException("mediaItem"); + + var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; + + if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + return string.Empty; + + var mediaItemUrl = mediaItem.MediaUrl(propertyAlias: propertyAlias); + + //get the default obj from the value converter + var cropperValue = mediaItem.Value(propertyAlias); + + //is it strongly typed? + var stronglyTyped = cropperValue as ImageCropperValue; + if (stronglyTyped != null) + { + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //this shouldn't be the case but we'll check + var jobj = cropperValue as JObject; + if (jobj != null) + { + stronglyTyped = jobj.ToObject(); + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //it's a single string + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The Json data from the Umbraco Core Image Cropper property editor + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string imageCropperValue = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageCropperValue cropDataSet = null; + if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + cropDataSet = imageCropperValue.DeserializeImageCropperValue(); + } + return GetCropUrl( + imageUrl, imageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The generator that will process all the options and the image URL to return a full image urls with all processing options appended + /// + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + ImageCropperValue cropDataSet, + int? width = null, + int? height = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (string.IsNullOrEmpty(imageUrl) == false) + { + ImageUrlGenerationOptions options; + + if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + var crop = cropDataSet.GetCrop(cropAlias); + + // if a crop was specified, but not found, return null + if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) + return null; + + options = cropDataSet.GetCropBaseOptions(imageUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); + + if (crop != null & useCropDimensions) + { + width = crop.Width; + height = crop.Height; + } + + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) + { + options.HeightRatio = (decimal)crop.Height / crop.Width; + } + + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) + { + options.WidthRatio = (decimal)crop.Width / crop.Height; + } + } + else + { + options = new ImageUrlGenerationOptions + { + ImageUrl = imageUrl, + ImageCropMode = (imageCropMode ?? ImageCropMode.Pad).ToString().ToLowerInvariant(), + ImageCropAnchor = imageCropAnchor?.ToString().ToLowerInvariant() + }; + } + + options.Quality = quality; + options.Width = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Width ? null : width; + options.Height = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Height ? null : height; + + if (ratioMode == ImageCropRatioMode.Width && height != null) + { + // if only height specified then assume a square + if (width == null) + { + width = height; + } + + options.WidthRatio = (decimal)width / (decimal)height; + } + + if (ratioMode == ImageCropRatioMode.Height && width != null) + { + // if only width specified then assume a square + if (height == null) + { + height = width; + } + + options.HeightRatio = (decimal)height / (decimal)width; + } + + options.UpScale = upScale; + options.FurtherOptions = furtherOptions; + options.CacheBusterValue = cacheBusterValue; + + return imageUrlGenerator.GetImageUrl(options); + } + + return string.Empty; + } + } +} diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 656f1e05a2..f3833ba42a 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -29,10 +29,7 @@ namespace Umbraco.Web /// /// The ImageProcessor.Web Url. /// - public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) - { - return mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); - } + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); /// /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. @@ -49,10 +46,7 @@ namespace Umbraco.Web /// /// The ImageProcessor.Web Url. /// - public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) - { - return mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); - } + public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, propertyAlias, cropAlias, Current.ImageUrlGenerator); /// /// Gets the ImageProcessor Url from the IPublishedContent item. @@ -121,44 +115,7 @@ namespace Umbraco.Web bool cacheBuster = true, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (mediaItem == null) throw new ArgumentNullException("mediaItem"); - - var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; - - if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) - return string.Empty; - - var mediaItemUrl = mediaItem.MediaUrl(propertyAlias: propertyAlias); - - //get the default obj from the value converter - var cropperValue = mediaItem.Value(propertyAlias); - - //is it strongly typed? - var stronglyTyped = cropperValue as ImageCropperValue; - if (stronglyTyped != null) - { - return GetCropUrl( - mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //this shouldn't be the case but we'll check - var jobj = cropperValue as JObject; - if (jobj != null) - { - stronglyTyped = jobj.ToObject(); - return GetCropUrl( - mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //it's a single string - return GetCropUrl( - mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); /// /// Gets the ImageProcessor Url from the image path. @@ -227,19 +184,7 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (string.IsNullOrEmpty(imageUrl)) return string.Empty; - - ImageCropperValue cropDataSet = null; - if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - cropDataSet = imageCropperValue.DeserializeImageCropperValue(); - } - return GetCropUrl( - imageUrl, cropDataSet, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); - } + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); /// /// Gets the ImageProcessor Url from the image path. @@ -306,129 +251,8 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (string.IsNullOrEmpty(imageUrl) == false) - { - var imageProcessorUrl = new StringBuilder(); + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); - if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - var crop = cropDataSet.GetCrop(cropAlias); - - // if a crop was specified, but not found, return null - if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) - return null; - - imageProcessorUrl.Append(imageUrl); - cropDataSet.AppendCropBaseUrl(imageProcessorUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); - - if (crop != null & useCropDimensions) - { - width = crop.Width; - height = crop.Height; - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) - { - var heightRatio = (decimal)crop.Height / (decimal)crop.Width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) - { - var widthRatio = (decimal)crop.Width / (decimal)crop.Height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } - } - else - { - imageProcessorUrl.Append(imageUrl); - - if (imageCropMode == null) - { - imageCropMode = ImageCropMode.Pad; - } - - imageProcessorUrl.Append("?mode=" + imageCropMode.ToString().ToLower()); - - if (imageCropAnchor != null) - { - imageProcessorUrl.Append("&anchor=" + imageCropAnchor.ToString().ToLower()); - } - } - - var hasFormat = furtherOptions != null && furtherOptions.InvariantContains("&format="); - - //Only put quality here, if we don't have a format specified. - //Otherwise we need to put quality at the end to avoid it being overridden by the format. - if (quality != null && hasFormat == false) - { - imageProcessorUrl.Append("&quality=" + quality); - } - - if (width != null && ratioMode != ImageCropRatioMode.Width) - { - imageProcessorUrl.Append("&width=" + width); - } - - if (height != null && ratioMode != ImageCropRatioMode.Height) - { - imageProcessorUrl.Append("&height=" + height); - } - - if (ratioMode == ImageCropRatioMode.Width && height != null) - { - // if only height specified then assume a square - if (width == null) - { - width = height; - } - - var widthRatio = (decimal)width / (decimal)height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } - - if (ratioMode == ImageCropRatioMode.Height && width != null) - { - // if only width specified then assume a square - if (height == null) - { - height = width; - } - - var heightRatio = (decimal)height / (decimal)width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - if (upScale == false) - { - imageProcessorUrl.Append("&upscale=false"); - } - - if (furtherOptions != null) - { - imageProcessorUrl.Append(furtherOptions); - } - - //If furtherOptions contains a format, we need to put the quality after the format. - if (quality != null && hasFormat) - { - imageProcessorUrl.Append("&quality=" + quality); - } - - if (cacheBusterValue != null) - { - imageProcessorUrl.Append("&rnd=").Append(cacheBusterValue); - } - - return imageProcessorUrl.ToString(); - } - - return string.Empty; - } internal static ImageCropperValue DeserializeImageCropperValue(this string json) { diff --git a/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs new file mode 100644 index 0000000000..816cfda0e9 --- /dev/null +++ b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models +{ + internal class ImageProcessorImageUrlGenerator : IImageUrlGenerator + { + public string GetImageUrl(ImageUrlGenerationOptions options) + { + var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty); + + if (options.FocalPoint != null) + { + imageProcessorUrl.Append("?center="); + imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append(","); + imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&mode=crop"); + } + else if (options.Crop != null) + { + imageProcessorUrl.Append("?crop="); + imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&cropmode=percentage"); + } + else if (options.DefaultCrop) + { + imageProcessorUrl.Append("?anchor=center"); + imageProcessorUrl.Append("&mode=crop"); + } + else + { + imageProcessorUrl.Append("?mode=" + options.ImageCropMode.ToString().ToLower()); + + if (options.ImageCropAnchor != null) + { + imageProcessorUrl.Append("&anchor=" + options.ImageCropAnchor.ToString().ToLower()); + } + } + + var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format="); + + //Only put quality here, if we don't have a format specified. + //Otherwise we need to put quality at the end to avoid it being overridden by the format. + if (options.Quality != null && hasFormat == false) + { + imageProcessorUrl.Append("&quality=" + options.Quality); + } + + if (options.HeightRatio != null) + { + imageProcessorUrl.Append("&heightratio=" + options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (options.WidthRatio != null) + { + imageProcessorUrl.Append("&widthratio=" + options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (options.Width != null) + { + imageProcessorUrl.Append("&width=" + options.Width); + } + + if (options.Height != null) + { + imageProcessorUrl.Append("&height=" + options.Height); + } + + if (options.UpScale == false) + { + imageProcessorUrl.Append("&upscale=false"); + } + + if (options.AnimationProcessMode != null) + { + imageProcessorUrl.Append("&animationprocessmode=" + options.AnimationProcessMode); + } + + if (options.FurtherOptions != null) + { + imageProcessorUrl.Append(options.FurtherOptions); + } + + //If furtherOptions contains a format, we need to put the quality after the format. + if (options.Quality != null && hasFormat) + { + imageProcessorUrl.Append("&quality=" + options.Quality); + } + + if (options.CacheBusterValue != null) + { + imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); + } + + return imageProcessorUrl.ToString(); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 7134fe8703..28cc70f776 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -30,18 +31,31 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlImageSourceParser _imageSourceParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly HtmlLocalLinkParser _localLinkParser; + private readonly IImageUrlGenerator _imageUrlGenerator; + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages, HtmlLocalLinkParser localLinkParser) + : this(logger, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, Current.ImageUrlGenerator) + { + } + + public GridPropertyEditor(ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; _localLinkParser = localLinkParser; + _imageUrlGenerator = imageUrlGenerator; } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -50,7 +64,7 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated /// /// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser, _imageUrlGenerator); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); @@ -61,19 +75,32 @@ namespace Umbraco.Web.PropertyEditors private readonly RichTextEditorPastedImages _pastedImages; private readonly RichTextPropertyEditor.RichTextPropertyValueEditor _richTextPropertyValueEditor; private readonly MediaPickerPropertyEditor.MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; + private readonly IImageUrlGenerator _imageUrlGenerator; + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages, HtmlLocalLinkParser localLinkParser) + : this(attribute, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, Current.ImageUrlGenerator) + { + } + + public GridPropertyValueEditor(DataEditorAttribute attribute, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; - _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages); + _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, _imageUrlGenerator); _mediaPickerPropertyValueEditor = new MediaPickerPropertyEditor.MediaPickerPropertyValueEditor(attribute); + _imageUrlGenerator = imageUrlGenerator; } /// @@ -108,7 +135,7 @@ namespace Umbraco.Web.PropertyEditors // Parse the HTML var html = rte.Value?.ToString(); - var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId); + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId, _imageUrlGenerator); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); rte.Value = editorValueWithMediaUrlsRemoved; diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs index 0b2a607f8b..e5afc44e43 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId) + internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator) { // Find all img's that has data-tmpimg attribute // Use HTML Agility Pack - https://html-agility-pack.net @@ -109,7 +109,7 @@ namespace Umbraco.Web.PropertyEditors if (width != int.MinValue && height != int.MinValue) { - location = $"{location}?width={width}&height={height}&mode=max"; + location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = location, ImageCropMode = "max", Width = width, Height = height }); } img.SetAttributeValue("src", location); diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 427e36b37a..42777f11ad 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -30,25 +31,36 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; + private readonly IImageUrlGenerator _imageUrlGenerator; /// /// The constructor will setup the property editor based on the attribute if one is found /// + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) + : this(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, Current.ImageUrlGenerator) + { + } + + /// + /// The constructor will setup the property editor based on the attribute if one is found + /// + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; _pastedImages = pastedImages; + _imageUrlGenerator = imageUrlGenerator; } /// /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages, _imageUrlGenerator); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); @@ -63,14 +75,16 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; + private readonly IImageUrlGenerator _imageUrlGenerator; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; _pastedImages = pastedImages; + _imageUrlGenerator = imageUrlGenerator; } /// @@ -124,7 +138,7 @@ namespace Umbraco.Web.PropertyEditors var mediaParent = config?.MediaParentId; var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId); + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 1c4121da0c..203bae4854 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -38,6 +38,8 @@ using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; +using Umbraco.Core.Models; +using Umbraco.Web.Models; namespace Umbraco.Web.Runtime { @@ -190,6 +192,8 @@ namespace Umbraco.Web.Runtime composition.MediaUrlProviders() .Append(); + composition.RegisterUnique(); + composition.RegisterUnique(); composition.ContentFinders() diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index f796985d39..f08abfaf12 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -52,6 +52,6 @@ namespace Umbraco.Web.Templates [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(RichTextEditorPastedImages.FindAndPersistPastedTempImages) + " instead")] internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) - => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); + => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId, Current.Factory.GetInstance()); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0111a36993..6fbf7ea881 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -155,6 +155,7 @@ + @@ -223,6 +224,7 @@ + From e7248ea48f2b6a0611a61bd0b0b2debf79ebb89b Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Fri, 7 Feb 2020 16:37:27 -0800 Subject: [PATCH 322/610] Fix ImageCropperText parameter ordering --- src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 62c0792f7b..8f2014ccdb 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -174,7 +174,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_CropAliasHeightRatioModeTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, ratioMode:ImageCropRatioMode.Height); - Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&heightratio=1", urlString); + Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&heightratio=1&width=100", urlString); } /// @@ -184,7 +184,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_WidthHeightRatioModeTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode:ImageCropRatioMode.Height); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=300&heightratio=0.5", urlString); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&heightratio=0.5&width=300", urlString); } /// @@ -194,7 +194,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_HeightWidthRatioModeTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode: ImageCropRatioMode.Width); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&height=150&widthratio=2", urlString); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&widthratio=2&height=150", urlString); } /// From 408ee452e1b52c0e06ee6ccac27ddf135131d1bd Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Sat, 8 Feb 2020 11:05:14 -0800 Subject: [PATCH 323/610] Code cleanup, added unit tests --- .../ImageProcessorImageUrlGeneratorTest.cs | 232 ++++++++++++++++++ .../PropertyEditors/ImageCropperTest.cs | 84 +++---- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../ImageCropperTemplateCoreExtensions.cs | 117 ++++----- .../Models/ImageProcessorImageUrlGenerator.cs | 79 ++---- 5 files changed, 343 insertions(+), 170 deletions(-) create mode 100644 src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs diff --git a/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs new file mode 100644 index 0000000000..ffbb0db946 --- /dev/null +++ b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs @@ -0,0 +1,232 @@ +using System; +using System.Globalization; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; +using Umbraco.Tests.Components; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.Models; +using Umbraco.Web; +using Umbraco.Web.PropertyEditors; +using System.Text; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class ImageProcessorImageUrlGeneratorTest + { + private const string MediaPath = "/media/1005/img_0671.jpg"; + private static readonly ImageUrlGenerationOptions.CropCoordinates Crop = new ImageUrlGenerationOptions.CropCoordinates { X1 = 0.58729977382575338m, Y1 = 0.055768992440203169m, X2 = 0m, Y2 = 0.32457553600198386m }; + private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus1 = new ImageUrlGenerationOptions.FocalPointPosition { Top = 0.80827067669172936m, Left = 0.96m }; + private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus2 = new ImageUrlGenerationOptions.FocalPointPosition { Top = 0.41m, Left = 0.4275m }; + private static readonly ImageProcessorImageUrlGenerator Generator = new ImageProcessorImageUrlGenerator(); + + [Test] + public void GetCropUrl_CropAliasTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, Crop = Crop, Width = 100, Height = 100 }); + Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + } + + [Test] + public void GetCropUrl_WidthHeightTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 200, Height = 300 }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300", urlString); + } + + [Test] + public void GetCropUrl_FocalPointTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 100, Height = 100 }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=100&height=100", urlString); + } + + [Test] + public void GetCropUrlFurtherOptionsTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 200, Height = 300, FurtherOptions = "&filter=comic&roundedcorners=radius-26|bgcolor-fff" }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); + } + + /// + /// Test that if a crop alias has been specified that doesn't exist the method returns null + /// + [Test] + public void GetCropUrlNullTest() + { + var urlString = Generator.GetImageUrl(null); + Assert.AreEqual(null, urlString); + } + + /// + /// Test that if a crop alias has been specified that doesn't exist the method returns null + /// + [Test] + public void GetCropUrlEmptyTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions()); + Assert.AreEqual("?mode=crop", urlString); + } + + /// + /// Test the GetCropUrl method on the ImageCropDataSet Model + /// + [Test] + public void GetBaseCropUrlFromModelTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { Crop = Crop, Width = 100, Height = 100 }); + Assert.AreEqual("?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + } + + /// + /// Test the height ratio mode with predefined crop dimensions + /// + [Test] + public void GetCropUrl_CropAliasHeightRatioModeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, Crop = Crop, Width = 100, HeightRatio = 1 }); + Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&heightratio=1&width=100", urlString); + } + + /// + /// Test the height ratio mode with manual width/height dimensions + /// + [Test] + public void GetCropUrl_WidthHeightRatioModeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 300, HeightRatio = 0.5m }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&heightratio=0.5&width=300", urlString); + } + + /// + /// Test the height ratio mode with width/height dimensions + /// + [Test] + public void GetCropUrl_HeightWidthRatioModeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Height = 150, WidthRatio = 2 }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&widthratio=2&height=150", urlString); + } + + /// + /// Test that if Crop mode is specified as anything other than Crop the image doesn't use the crop + /// + [Test] + public void GetCropUrl_SpecifiedCropModeTest() + { + var urlStringMin = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Min", Width = 300, Height = 150 }); + var urlStringBoxPad = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "BoxPad", Width = 300, Height = 150 }); + var urlStringPad = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Pad", Width = 300, Height = 150 }); + var urlStringMax = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Max", Width = 300, Height = 150 }); + var urlStringStretch = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Stretch", Width = 300, Height = 150 }); + + Assert.AreEqual(MediaPath + "?mode=min&width=300&height=150", urlStringMin); + Assert.AreEqual(MediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); + Assert.AreEqual(MediaPath + "?mode=pad&width=300&height=150", urlStringPad); + Assert.AreEqual(MediaPath + "?mode=max&width=300&height=150", urlStringMax); + Assert.AreEqual(MediaPath + "?mode=stretch&width=300&height=150", urlStringStretch); + } + + /// + /// Test for upload property type + /// + [Test] + public void GetCropUrl_UploadTypeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Crop", ImageCropAnchor = "Center", Width = 100, Height = 270 }); + Assert.AreEqual(MediaPath + "?mode=crop&anchor=center&width=100&height=270", urlString); + } + + /// + /// Test for preferFocalPoint when focal point is centered + /// + [Test] + public void GetCropUrl_PreferFocalPointCenter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Width = 300, Height = 150 }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString); + } + + /// + /// Test to check if height ratio is returned for a predefined crop without coordinates and focal point in centre when a width parameter is passed + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidth() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + } + + /// + /// Test to check if height ratio is returned for a predefined crop without coordinates and focal point is custom when a width parameter is passed + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPoint() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus2, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); + Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + } + + /// + /// Test to check if crop ratio is ignored if useCropDimensions is true + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus2, Width = 270, Height = 161 }); + Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&width=270&height=161", urlString); + } + + /// + /// Test to check if width ratio is returned for a predefined crop without coordinates and focal point in centre when a height parameter is passed + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithHeight() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Height = 200, WidthRatio = 1.6770186335403726708074534161m }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&widthratio=1.6770186335403726708074534161&height=200", urlString); + } + + /// + /// Test to check result when only a width parameter is passed, effectivly a resize only + /// + [Test] + public void GetCropUrl_WidthOnlyParameter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Width = 200 }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=200", urlString); + } + + /// + /// Test to check result when only a height parameter is passed, effectivly a resize only + /// + [Test] + public void GetCropUrl_HeightOnlyParameter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Height = 200 }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&height=200", urlString); + } + + /// + /// Test to check result when using a background color with padding + /// + [Test] + public void GetCropUrl_BackgroundColorParameter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Pad", Width = 400, Height = 400, FurtherOptions = "&bgcolor=fff" }); + Assert.AreEqual(MediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); + } + } +} diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 8f2014ccdb..c5c2b4e61f 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -5,9 +5,7 @@ using Newtonsoft.Json; using NUnit.Framework; using Newtonsoft.Json.Linq; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -112,7 +110,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_CropAliasTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true); - Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + Assert.AreEqual(MediaPath + "?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&w=100&h=100", urlString); } /// @@ -122,28 +120,28 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_CropAliasIgnoreWidthHeightTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, width: 50, height: 50); - Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + Assert.AreEqual(MediaPath + "?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&w=100&h=100", urlString); } [Test] public void GetCropUrl_WidthHeightTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 200, height: 300); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300", urlString); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&w=200&h=300", urlString); } [Test] public void GetCropUrl_FocalPointTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "thumb", preferFocalPoint: true, useCropDimensions: true); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=100&height=100", urlString); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&w=100&h=100", urlString); } [Test] public void GetCropUrlFurtherOptionsTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 200, height: 300, furtherOptions: "&filter=comic&roundedcorners=radius-26|bgcolor-fff"); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&w=200&h=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); } /// @@ -164,7 +162,7 @@ namespace Umbraco.Tests.PropertyEditors { var cropDataSet = CropperJson1.DeserializeImageCropperValue(); var urlString = cropDataSet.GetCropUrl("thumb", new TestImageUrlGenerator()); - Assert.AreEqual("?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + Assert.AreEqual("?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&w=100&h=100", urlString); } /// @@ -174,7 +172,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_CropAliasHeightRatioModeTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, ratioMode:ImageCropRatioMode.Height); - Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&heightratio=1&width=100", urlString); + Assert.AreEqual(MediaPath + "?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&hr=1&w=100", urlString); } /// @@ -184,7 +182,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_WidthHeightRatioModeTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode:ImageCropRatioMode.Height); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&heightratio=0.5&width=300", urlString); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&hr=0.5&w=300", urlString); } /// @@ -194,7 +192,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_HeightWidthRatioModeTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode: ImageCropRatioMode.Width); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&widthratio=2&height=150", urlString); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&wr=2&h=150", urlString); } /// @@ -209,11 +207,11 @@ namespace Umbraco.Tests.PropertyEditors var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode:ImageCropMode.Max); var urlStringStretch = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); - Assert.AreEqual(MediaPath + "?mode=min&width=300&height=150", urlStringMin); - Assert.AreEqual(MediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); - Assert.AreEqual(MediaPath + "?mode=pad&width=300&height=150", urlStringPad); - Assert.AreEqual(MediaPath + "?mode=max&width=300&height=150", urlString); - Assert.AreEqual(MediaPath + "?mode=stretch&width=300&height=150", urlStringStretch); + Assert.AreEqual(MediaPath + "?m=min&w=300&h=150", urlStringMin); + Assert.AreEqual(MediaPath + "?m=boxpad&w=300&h=150", urlStringBoxPad); + Assert.AreEqual(MediaPath + "?m=pad&w=300&h=150", urlStringPad); + Assert.AreEqual(MediaPath + "?m=max&w=300&h=150", urlString); + Assert.AreEqual(MediaPath + "?m=stretch&w=300&h=150", urlStringStretch); } /// @@ -223,7 +221,7 @@ namespace Umbraco.Tests.PropertyEditors public void GetCropUrl_UploadTypeTest() { var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), width: 100, height: 270, imageCropMode: ImageCropMode.Crop, imageCropAnchor: ImageCropAnchor.Center); - Assert.AreEqual(MediaPath + "?mode=crop&anchor=center&width=100&height=270", urlString); + Assert.AreEqual(MediaPath + "?m=crop&a=center&w=100&h=270", urlString); } /// @@ -235,7 +233,7 @@ namespace Umbraco.Tests.PropertyEditors const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, width: 300, height: 150, preferFocalPoint:true); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString); + Assert.AreEqual(MediaPath + "?m=defaultcrop&w=300&h=150", urlString); } /// @@ -247,7 +245,7 @@ namespace Umbraco.Tests.PropertyEditors const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + Assert.AreEqual(MediaPath + "?m=defaultcrop&hr=0.5962962962962962962962962963&w=200", urlString); } /// @@ -259,7 +257,7 @@ namespace Umbraco.Tests.PropertyEditors const string cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200); - Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + Assert.AreEqual(MediaPath + "?f=0.41x0.4275&hr=0.5962962962962962962962962963&w=200", urlString); } /// @@ -271,7 +269,7 @@ namespace Umbraco.Tests.PropertyEditors const string cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200, useCropDimensions: true); - Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&width=270&height=161", urlString); + Assert.AreEqual(MediaPath + "?f=0.41x0.4275&w=270&h=161", urlString); } /// @@ -283,7 +281,7 @@ namespace Umbraco.Tests.PropertyEditors const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", height: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&widthratio=1.6770186335403726708074534161&height=200", urlString); + Assert.AreEqual(MediaPath + "?m=defaultcrop&wr=1.6770186335403726708074534161&h=200", urlString); } /// @@ -295,7 +293,7 @@ namespace Umbraco.Tests.PropertyEditors const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, width: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=200", urlString); + Assert.AreEqual(MediaPath + "?m=defaultcrop&w=200", urlString); } /// @@ -307,7 +305,7 @@ namespace Umbraco.Tests.PropertyEditors const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, height: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&height=200", urlString); + Assert.AreEqual(MediaPath + "?m=defaultcrop&h=200", urlString); } /// @@ -319,10 +317,10 @@ namespace Umbraco.Tests.PropertyEditors var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"" + MediaPath + "\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), 400, 400, cropperJson, imageCropMode: ImageCropMode.Pad, furtherOptions: "&bgcolor=fff"); - Assert.AreEqual(MediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); + Assert.AreEqual(MediaPath + "?m=pad&w=400&h=400&bgcolor=fff", urlString); } - private class TestImageUrlGenerator : IImageUrlGenerator + internal class TestImageUrlGenerator : IImageUrlGenerator { public string GetImageUrl(ImageUrlGenerationOptions options) { @@ -330,42 +328,40 @@ namespace Umbraco.Tests.PropertyEditors if (options.FocalPoint != null) { - imageProcessorUrl.Append("?center="); + imageProcessorUrl.Append("?f="); imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); - imageProcessorUrl.Append(","); + imageProcessorUrl.Append("x"); imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); - imageProcessorUrl.Append("&mode=crop"); } else if (options.Crop != null) { - imageProcessorUrl.Append("?crop="); + imageProcessorUrl.Append("?c="); imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); - imageProcessorUrl.Append("&cropmode=percentage"); } else if (options.DefaultCrop) { - imageProcessorUrl.Append("?anchor=center&mode=crop"); + imageProcessorUrl.Append("?m=defaultcrop"); } else { - imageProcessorUrl.Append("?mode=" + options.ImageCropMode.ToString().ToLower()); - if (options.ImageCropAnchor != null)imageProcessorUrl.Append("&anchor=" + options.ImageCropAnchor.ToString().ToLower()); + imageProcessorUrl.Append("?m=" + options.ImageCropMode.ToString().ToLower()); + if (options.ImageCropAnchor != null)imageProcessorUrl.Append("&a=" + options.ImageCropAnchor.ToString().ToLower()); } - var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format="); - if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&quality=" + options.Quality); - if (options.HeightRatio != null) imageProcessorUrl.Append("&heightratio=" + options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); - if (options.WidthRatio != null) imageProcessorUrl.Append("&widthratio=" + options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); - if (options.Width != null) imageProcessorUrl.Append("&width=" + options.Width); - if (options.Height != null) imageProcessorUrl.Append("&height=" + options.Height); - if (options.UpScale == false) imageProcessorUrl.Append("&upscale=false"); - if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&animationprocessmode=" + options.AnimationProcessMode); + var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&f="); + if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&q=" + options.Quality); + if (options.HeightRatio != null) imageProcessorUrl.Append("&hr=" + options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.WidthRatio != null) imageProcessorUrl.Append("&wr=" + options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.Width != null) imageProcessorUrl.Append("&w=" + options.Width); + if (options.Height != null) imageProcessorUrl.Append("&h=" + options.Height); + if (options.UpScale == false) imageProcessorUrl.Append("&u=no"); + if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&apm=" + options.AnimationProcessMode); if (options.FurtherOptions != null) imageProcessorUrl.Append(options.FurtherOptions); - if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&quality=" + options.Quality); - if (options.CacheBusterValue != null) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); + if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&q=" + options.Quality); + if (options.CacheBusterValue != null) imageProcessorUrl.Append("&r=").Append(options.CacheBusterValue); return imageProcessorUrl.ToString(); } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index ff923bb04b..f9afdaa040 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -140,6 +140,7 @@ + diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index b8e832beb9..54b4ce2856 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -308,82 +308,71 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) { - if (string.IsNullOrEmpty(imageUrl) == false) + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageUrlGenerationOptions options; + + if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) { - ImageUrlGenerationOptions options; + var crop = cropDataSet.GetCrop(cropAlias); - if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + // if a crop was specified, but not found, return null + if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) + return null; + + options = cropDataSet.GetCropBaseOptions(imageUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); + + if (crop != null & useCropDimensions) { - var crop = cropDataSet.GetCrop(cropAlias); - - // if a crop was specified, but not found, return null - if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) - return null; - - options = cropDataSet.GetCropBaseOptions(imageUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); - - if (crop != null & useCropDimensions) - { - width = crop.Width; - height = crop.Height; - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) - { - options.HeightRatio = (decimal)crop.Height / crop.Width; - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) - { - options.WidthRatio = (decimal)crop.Width / crop.Height; - } - } - else - { - options = new ImageUrlGenerationOptions - { - ImageUrl = imageUrl, - ImageCropMode = (imageCropMode ?? ImageCropMode.Pad).ToString().ToLowerInvariant(), - ImageCropAnchor = imageCropAnchor?.ToString().ToLowerInvariant() - }; + width = crop.Width; + height = crop.Height; } - options.Quality = quality; - options.Width = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Width ? null : width; - options.Height = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Height ? null : height; - - if (ratioMode == ImageCropRatioMode.Width && height != null) + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) { - // if only height specified then assume a square - if (width == null) - { - width = height; - } - - options.WidthRatio = (decimal)width / (decimal)height; + options.HeightRatio = (decimal)crop.Height / crop.Width; } - if (ratioMode == ImageCropRatioMode.Height && width != null) + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) { - // if only width specified then assume a square - if (height == null) - { - height = width; - } - - options.HeightRatio = (decimal)height / (decimal)width; + options.WidthRatio = (decimal)crop.Width / crop.Height; } - - options.UpScale = upScale; - options.FurtherOptions = furtherOptions; - options.CacheBusterValue = cacheBusterValue; - - return imageUrlGenerator.GetImageUrl(options); + } + else + { + options = new ImageUrlGenerationOptions + { + ImageUrl = imageUrl, + ImageCropMode = (imageCropMode ?? ImageCropMode.Pad).ToString().ToLowerInvariant(), + ImageCropAnchor = imageCropAnchor?.ToString().ToLowerInvariant() + }; } - return string.Empty; + options.Quality = quality; + options.Width = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Width ? null : width; + options.Height = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Height ? null : height; + + if (ratioMode == ImageCropRatioMode.Width && height != null) + { + // if only height specified then assume a square + if (width == null) width = height; + options.WidthRatio = (decimal)width / (decimal)height; + } + + if (ratioMode == ImageCropRatioMode.Height && width != null) + { + // if only width specified then assume a square + if (height == null) height = width; + options.HeightRatio = (decimal)height / (decimal)width; + } + + options.UpScale = upScale; + options.FurtherOptions = furtherOptions; + options.CacheBusterValue = cacheBusterValue; + + return imageUrlGenerator.GetImageUrl(options); } } } diff --git a/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs index 816cfda0e9..47d7488c10 100644 --- a/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs +++ b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Text; -using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Models; @@ -13,13 +9,14 @@ namespace Umbraco.Web.Models { public string GetImageUrl(ImageUrlGenerationOptions options) { + if (options == null) return null; + var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty); if (options.FocalPoint != null) { imageProcessorUrl.Append("?center="); - imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); - imageProcessorUrl.Append(","); + imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)).Append(","); imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); imageProcessorUrl.Append("&mode=crop"); } @@ -34,73 +31,31 @@ namespace Umbraco.Web.Models } else if (options.DefaultCrop) { - imageProcessorUrl.Append("?anchor=center"); - imageProcessorUrl.Append("&mode=crop"); + imageProcessorUrl.Append("?anchor=center&mode=crop"); } else { - imageProcessorUrl.Append("?mode=" + options.ImageCropMode.ToString().ToLower()); + imageProcessorUrl.Append("?mode=").Append((options.ImageCropMode ?? "crop").ToLower()); - if (options.ImageCropAnchor != null) - { - imageProcessorUrl.Append("&anchor=" + options.ImageCropAnchor.ToString().ToLower()); - } + if (options.ImageCropAnchor != null) imageProcessorUrl.Append("&anchor=").Append(options.ImageCropAnchor.ToLower()); } var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format="); //Only put quality here, if we don't have a format specified. //Otherwise we need to put quality at the end to avoid it being overridden by the format. - if (options.Quality != null && hasFormat == false) - { - imageProcessorUrl.Append("&quality=" + options.Quality); - } - - if (options.HeightRatio != null) - { - imageProcessorUrl.Append("&heightratio=" + options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (options.WidthRatio != null) - { - imageProcessorUrl.Append("&widthratio=" + options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (options.Width != null) - { - imageProcessorUrl.Append("&width=" + options.Width); - } - - if (options.Height != null) - { - imageProcessorUrl.Append("&height=" + options.Height); - } - - if (options.UpScale == false) - { - imageProcessorUrl.Append("&upscale=false"); - } - - if (options.AnimationProcessMode != null) - { - imageProcessorUrl.Append("&animationprocessmode=" + options.AnimationProcessMode); - } - - if (options.FurtherOptions != null) - { - imageProcessorUrl.Append(options.FurtherOptions); - } + if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (options.HeightRatio != null) imageProcessorUrl.Append("&heightratio=").Append(options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.WidthRatio != null) imageProcessorUrl.Append("&widthratio=").Append(options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.Width != null) imageProcessorUrl.Append("&width=").Append(options.Width); + if (options.Height != null) imageProcessorUrl.Append("&height=").Append(options.Height); + if (options.UpScale == false) imageProcessorUrl.Append("&upscale=false"); + if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&animationprocessmode=").Append(options.AnimationProcessMode); + if (options.FurtherOptions != null) imageProcessorUrl.Append(options.FurtherOptions); //If furtherOptions contains a format, we need to put the quality after the format. - if (options.Quality != null && hasFormat) - { - imageProcessorUrl.Append("&quality=" + options.Quality); - } - - if (options.CacheBusterValue != null) - { - imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); - } + if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (options.CacheBusterValue != null) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); return imageProcessorUrl.ToString(); } From 7e7d38e74b68e28aed8aeae9fd9d44417d90a075 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Sat, 8 Feb 2020 16:49:12 -0800 Subject: [PATCH 324/610] Add ImageUrlGeneratorController --- .../resources/imageurlgenerator.resource.js | 36 ++++++++++++++++++ .../Editors/BackOfficeServerVariables.cs | 4 ++ .../Editors/ImageUrlGeneratorController.cs | 38 +++++++++++++++++++ .../ImageCropperTemplateCoreExtensions.cs | 4 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/imageurlgenerator.resource.js create mode 100644 src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/imageurlgenerator.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/imageurlgenerator.resource.js new file mode 100644 index 0000000000..a937cd2675 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/imageurlgenerator.resource.js @@ -0,0 +1,36 @@ +/** + * @ngdoc service + * @name umbraco.resources.imageUrlGeneratorResource + * @function + * + * @description + * Used by the various controllers to get an image URL formatted correctly for the current image URL generator + */ +(function () { + 'use strict'; + + function imageUrlGeneratorResource($http, umbRequestHelper) { + + function getCropUrl(mediaPath, width, height, imageCropMode, animationProcessMode) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "imageUrlGeneratorApiBaseUrl", + "GetCropUrl", + { mediaPath, width, height, imageCropMode, animationProcessMode })), + 'Failed to get crop URL'); + } + + + var resource = { + getCropUrl: getCropUrl + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('imageUrlGeneratorResource', imageUrlGeneratorResource); + +})(); diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index dc46a97df4..89413db84d 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -314,6 +314,10 @@ namespace Umbraco.Web.Editors "tinyMceApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.UploadImage()) }, + { + "imageUrlGeneratorApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetCropUrl(null, null, null, null, null)) + }, } }, { diff --git a/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs new file mode 100644 index 0000000000..7a02b2bd65 --- /dev/null +++ b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for getting URLs for images with parameters + /// + /// + /// + /// This controller allows for retrieving URLs for processed images, such as resized, cropped, + /// or otherwise altered. These can be different based on the IImageUrlGenerator + /// implementation in use, and so back-office could should not rely on hard-coded string + /// building to generate correct URLs + /// + /// + [PluginController("UmbracoApi")] + public class ImageUrlGeneratorController + { + private readonly IImageUrlGenerator _imageUrlGenerator; + + public ImageUrlGeneratorController(IImageUrlGenerator imageUrlGenerator) + { + _imageUrlGenerator = imageUrlGenerator; + } + + public string GetCropUrl(string mediaPath, int? width = null, int? height = null, ImageCropMode? imageCropMode = null, string animationProcessMode = null) + { + return mediaPath.GetCropUrl(_imageUrlGenerator, null, width: width, height: height, imageCropMode: imageCropMode, animationProcessMode: animationProcessMode); + } + } +} diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index 54b4ce2856..74b32f2941 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -306,7 +306,8 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) + bool upScale = true, + string animationProcessMode = null) { if (string.IsNullOrEmpty(imageUrl)) return string.Empty; @@ -353,6 +354,7 @@ namespace Umbraco.Web options.Quality = quality; options.Width = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Width ? null : width; options.Height = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Height ? null : height; + options.AnimationProcessMode = animationProcessMode; if (ratioMode == ImageCropRatioMode.Width && height != null) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6fbf7ea881..e5005f0fc0 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -150,6 +150,7 @@ + From 43094891742f9d4ff0110f30e32f7973f068f59a Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Sun, 9 Feb 2020 11:12:29 -0800 Subject: [PATCH 325/610] Make changes for PR suggestions --- .../Models/ImageUrlGenerationOptions.cs | 45 +++++++++++++--- src/Umbraco.Core/Models/UserExtensions.cs | 10 ++-- .../ValueConverters/ImageCropperValue.cs | 6 +-- .../ImageProcessorImageUrlGeneratorTest.cs | 52 +++++++++---------- .../Editors/ImageUrlGeneratorController.cs | 5 +- src/Umbraco.Web/Editors/ImagesController.cs | 2 +- .../ImageCropperTemplateCoreExtensions.cs | 3 +- .../Models/ImageProcessorImageUrlGenerator.cs | 41 ++++++++------- .../RichTextEditorPastedImages.cs | 2 +- 9 files changed, 98 insertions(+), 68 deletions(-) diff --git a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs index 9e8d3f3a00..f87657c33d 100644 --- a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -1,8 +1,17 @@ namespace Umbraco.Core.Models { + /// + /// These are options that are passed to the IImageUrlGenerator implementation to determine + /// the propery URL that is needed + /// public class ImageUrlGenerationOptions { - public string ImageUrl { get; set; } + public ImageUrlGenerationOptions (string imageUrl) + { + ImageUrl = imageUrl; + } + + public string ImageUrl { get; } public int? Width { get; set; } public int? Height { get; set; } public decimal? WidthRatio { get; set; } @@ -18,18 +27,40 @@ public bool UpScale { get; set; } = true; public string AnimationProcessMode { get; set; } + /// + /// The focal point position, in whatever units the registered IImageUrlGenerator uses, + /// typically a percentage of the total image from 0.0 to 1.0. + /// public class FocalPointPosition { - public decimal Left { get; set; } - public decimal Top { get; set; } + public FocalPointPosition (decimal top, decimal left) + { + Left = left; + Top = top; + } + + public decimal Left { get; } + public decimal Top { get; } } + /// + /// The bounds of the crop within the original image, in whatever units the registered + /// IImageUrlGenerator uses, typically a percentage between 0 and 100. + /// public class CropCoordinates { - public decimal X1 { get; set; } - public decimal Y1 { get; set; } - public decimal X2 { get; set; } - public decimal Y2 { get; set; } + public CropCoordinates (decimal x1, decimal y1, decimal x2, decimal y2) + { + X1 = x1; + Y1 = y1; + X2 = x2; + Y2 = y2; + } + + public decimal X1 { get; } + public decimal Y1 { get; } + public decimal X2 { get; } + public decimal Y2 { get; } } } } diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index f11a8efe46..5be66bac47 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -109,11 +109,11 @@ namespace Umbraco.Core.Models var urlGenerator = Current.ImageUrlGenerator; return new[] { - urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 30, Height = 30 }), - urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 60, Height = 60 }), - urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 90, Height = 90 }), - urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 150, Height = 150 }), - urlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = avatarUrl, ImageCropMode = "crop", Width = 300, Height = 300 }) + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 30, Height = 30 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 60, Height = 60 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 90, Height = 90 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 150, Height = 150 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 300, Height = 300 }) }; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs index 70eb7e0477..ab217d3870 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -67,15 +67,15 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters || crop != null && crop.Coordinates == null && HasFocalPoint() || defaultCrop && HasFocalPoint()) { - return new ImageUrlGenerationOptions { ImageUrl = url, FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition { Left = FocalPoint.Left, Top = FocalPoint.Top } }; + return new ImageUrlGenerationOptions(url) { FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(FocalPoint.Top, FocalPoint.Left) }; } else if (crop != null && crop.Coordinates != null && preferFocalPoint == false) { - return new ImageUrlGenerationOptions { ImageUrl = url, Crop = new ImageUrlGenerationOptions.CropCoordinates { X1 = crop.Coordinates.X1, X2 = crop.Coordinates.X2, Y1 = crop.Coordinates.Y1, Y2 = crop.Coordinates.Y2 } }; + return new ImageUrlGenerationOptions(url) { Crop = new ImageUrlGenerationOptions.CropCoordinates(crop.Coordinates.X1, crop.Coordinates.Y1, crop.Coordinates.X2, crop.Coordinates.Y2) }; } else { - return new ImageUrlGenerationOptions { ImageUrl = url, DefaultCrop = true }; + return new ImageUrlGenerationOptions(url) { DefaultCrop = true }; } } diff --git a/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs index ffbb0db946..30ead90de9 100644 --- a/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs +++ b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs @@ -27,36 +27,36 @@ namespace Umbraco.Tests.Models public class ImageProcessorImageUrlGeneratorTest { private const string MediaPath = "/media/1005/img_0671.jpg"; - private static readonly ImageUrlGenerationOptions.CropCoordinates Crop = new ImageUrlGenerationOptions.CropCoordinates { X1 = 0.58729977382575338m, Y1 = 0.055768992440203169m, X2 = 0m, Y2 = 0.32457553600198386m }; - private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus1 = new ImageUrlGenerationOptions.FocalPointPosition { Top = 0.80827067669172936m, Left = 0.96m }; - private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus2 = new ImageUrlGenerationOptions.FocalPointPosition { Top = 0.41m, Left = 0.4275m }; + private static readonly ImageUrlGenerationOptions.CropCoordinates Crop = new ImageUrlGenerationOptions.CropCoordinates(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m); + private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus1 = new ImageUrlGenerationOptions.FocalPointPosition(0.80827067669172936m, 0.96m); + private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus2 = new ImageUrlGenerationOptions.FocalPointPosition(0.41m, 0.4275m); private static readonly ImageProcessorImageUrlGenerator Generator = new ImageProcessorImageUrlGenerator(); [Test] public void GetCropUrl_CropAliasTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, Crop = Crop, Width = 100, Height = 100 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Crop = Crop, Width = 100, Height = 100 }); Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); } [Test] public void GetCropUrl_WidthHeightTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 200, Height = 300 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 200, Height = 300 }); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300", urlString); } [Test] public void GetCropUrl_FocalPointTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 100, Height = 100 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 100, Height = 100 }); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=100&height=100", urlString); } [Test] public void GetCropUrlFurtherOptionsTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 200, Height = 300, FurtherOptions = "&filter=comic&roundedcorners=radius-26|bgcolor-fff" }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 200, Height = 300, FurtherOptions = "&filter=comic&roundedcorners=radius-26|bgcolor-fff" }); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); } @@ -76,7 +76,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrlEmptyTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions()); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(null)); Assert.AreEqual("?mode=crop", urlString); } @@ -86,7 +86,7 @@ namespace Umbraco.Tests.Models [Test] public void GetBaseCropUrlFromModelTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { Crop = Crop, Width = 100, Height = 100 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(null) { Crop = Crop, Width = 100, Height = 100 }); Assert.AreEqual("?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); } @@ -96,7 +96,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_CropAliasHeightRatioModeTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, Crop = Crop, Width = 100, HeightRatio = 1 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Crop = Crop, Width = 100, HeightRatio = 1 }); Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&heightratio=1&width=100", urlString); } @@ -106,7 +106,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_WidthHeightRatioModeTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Width = 300, HeightRatio = 0.5m }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 300, HeightRatio = 0.5m }); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&heightratio=0.5&width=300", urlString); } @@ -116,7 +116,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_HeightWidthRatioModeTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus1, Height = 150, WidthRatio = 2 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Height = 150, WidthRatio = 2 }); Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&widthratio=2&height=150", urlString); } @@ -126,11 +126,11 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_SpecifiedCropModeTest() { - var urlStringMin = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Min", Width = 300, Height = 150 }); - var urlStringBoxPad = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "BoxPad", Width = 300, Height = 150 }); - var urlStringPad = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Pad", Width = 300, Height = 150 }); - var urlStringMax = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Max", Width = 300, Height = 150 }); - var urlStringStretch = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Stretch", Width = 300, Height = 150 }); + var urlStringMin = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Min", Width = 300, Height = 150 }); + var urlStringBoxPad = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "BoxPad", Width = 300, Height = 150 }); + var urlStringPad = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Pad", Width = 300, Height = 150 }); + var urlStringMax = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Max", Width = 300, Height = 150 }); + var urlStringStretch = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Stretch", Width = 300, Height = 150 }); Assert.AreEqual(MediaPath + "?mode=min&width=300&height=150", urlStringMin); Assert.AreEqual(MediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); @@ -145,7 +145,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_UploadTypeTest() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Crop", ImageCropAnchor = "Center", Width = 100, Height = 270 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Crop", ImageCropAnchor = "Center", Width = 100, Height = 270 }); Assert.AreEqual(MediaPath + "?mode=crop&anchor=center&width=100&height=270", urlString); } @@ -155,7 +155,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_PreferFocalPointCenter() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Width = 300, Height = 150 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Width = 300, Height = 150 }); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString); } @@ -165,7 +165,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidth() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); } @@ -175,7 +175,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPoint() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus2, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus2, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); } @@ -185,7 +185,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, FocalPoint = Focus2, Width = 270, Height = 161 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus2, Width = 270, Height = 161 }); Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&width=270&height=161", urlString); } @@ -195,7 +195,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_PreDefinedCropNoCoordinatesWithHeight() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Height = 200, WidthRatio = 1.6770186335403726708074534161m }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Height = 200, WidthRatio = 1.6770186335403726708074534161m }); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&widthratio=1.6770186335403726708074534161&height=200", urlString); } @@ -205,7 +205,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_WidthOnlyParameter() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Width = 200 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Width = 200 }); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=200", urlString); } @@ -215,7 +215,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_HeightOnlyParameter() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, DefaultCrop = true, Height = 200 }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Height = 200 }); Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&height=200", urlString); } @@ -225,7 +225,7 @@ namespace Umbraco.Tests.Models [Test] public void GetCropUrl_BackgroundColorParameter() { - var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = MediaPath, ImageCropMode = "Pad", Width = 400, Height = 400, FurtherOptions = "&bgcolor=fff" }); + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Pad", Width = 400, Height = 400, FurtherOptions = "&bgcolor=fff" }); Assert.AreEqual(MediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); } } diff --git a/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs index 7a02b2bd65..87d7e29619 100644 --- a/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs +++ b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs @@ -16,12 +16,11 @@ namespace Umbraco.Web.Editors /// /// This controller allows for retrieving URLs for processed images, such as resized, cropped, /// or otherwise altered. These can be different based on the IImageUrlGenerator - /// implementation in use, and so back-office could should not rely on hard-coded string + /// implementation in use, and so the BackOffice could should not rely on hard-coded string /// building to generate correct URLs /// /// - [PluginController("UmbracoApi")] - public class ImageUrlGeneratorController + public class ImageUrlGeneratorController : UmbracoAuthorizedJsonController { private readonly IImageUrlGenerator _imageUrlGenerator; diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index c8f0e2d9bf..f3682a5b43 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -84,7 +84,7 @@ namespace Umbraco.Web.Editors } var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null; - var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = imagePath, UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd }); + var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd }); response.Headers.Location = new Uri(imageUrl, UriKind.RelativeOrAbsolute); return response; diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index 74b32f2941..7ac5578d75 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -343,9 +343,8 @@ namespace Umbraco.Web } else { - options = new ImageUrlGenerationOptions + options = new ImageUrlGenerationOptions (imageUrl) { - ImageUrl = imageUrl, ImageCropMode = (imageCropMode ?? ImageCropMode.Pad).ToString().ToLowerInvariant(), ImageCropAnchor = imageCropAnchor?.ToString().ToLowerInvariant() }; diff --git a/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs index 47d7488c10..e471e9aa4a 100644 --- a/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs +++ b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs @@ -13,26 +13,9 @@ namespace Umbraco.Web.Models var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty); - if (options.FocalPoint != null) - { - imageProcessorUrl.Append("?center="); - imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)).Append(","); - imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); - imageProcessorUrl.Append("&mode=crop"); - } - else if (options.Crop != null) - { - imageProcessorUrl.Append("?crop="); - imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); - imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); - imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); - imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); - imageProcessorUrl.Append("&cropmode=percentage"); - } - else if (options.DefaultCrop) - { - imageProcessorUrl.Append("?anchor=center&mode=crop"); - } + if (options.FocalPoint != null) AppendFocalPoint(imageProcessorUrl, options); + else if (options.Crop != null) AppendCrop(imageProcessorUrl, options); + else if (options.DefaultCrop) imageProcessorUrl.Append("?anchor=center&mode=crop"); else { imageProcessorUrl.Append("?mode=").Append((options.ImageCropMode ?? "crop").ToLower()); @@ -59,5 +42,23 @@ namespace Umbraco.Web.Models return imageProcessorUrl.ToString(); } + + private void AppendFocalPoint(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options) + { + imageProcessorUrl.Append("?center="); + imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&mode=crop"); + } + + private void AppendCrop(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options) + { + imageProcessorUrl.Append("?crop="); + imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&cropmode=percentage"); + } } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs index e5afc44e43..e7ce0763e4 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -109,7 +109,7 @@ namespace Umbraco.Web.PropertyEditors if (width != int.MinValue && height != int.MinValue) { - location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions { ImageUrl = location, ImageCropMode = "max", Width = width, Height = height }); + location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location) { ImageCropMode = "max", Width = width, Height = height }); } img.SetAttributeValue("src", location); From 61ed8800cc773544c642f6cb69f425b6a9052a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 10 Feb 2020 07:58:31 +0100 Subject: [PATCH 326/610] ups --- .../src/less/components/editor/umb-variant-switcher.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index eb44a3a96a..79f2521f3e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -302,6 +302,7 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__name-wrapper::after { left: 55px;// overwrite left to achieve same indentation on the dashed border as language. } + } .umb-variant-switcher__name-wrapper { From 21ea8e3b8be7a3bd2e4d6f2757509a9d8e42313e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 10 Feb 2020 08:57:54 +0100 Subject: [PATCH 327/610] show split-view on focus --- .../src/less/components/editor/umb-variant-switcher.less | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index 79f2521f3e..87385caa87 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -199,7 +199,10 @@ button.umb-variant-switcher__toggle { cursor: default; } -.umb-variant-switcher__item:hover .umb-variant-switcher__split-view { +.umb-variant-switcher__item:focus, +.umb-variant-switcher__item:focus-within, +.umb-variant-switcher__item:hover .umb-variant-switcher__split-view, +.umb-variant-switcher__split-view:focus { display: block; cursor: pointer; } @@ -261,7 +264,7 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__split-view { font-size: 12px; display: none; - padding: 18px 20px; + padding: 20px 20px; position: absolute; right: 0; top: 0; From 124bc27921332fb42ab83852704a7d54dbc354f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Mon, 10 Feb 2020 09:16:03 +0100 Subject: [PATCH 328/610] V8/feature/ab4550 segments ui variant picker (#7614) * Fixes incorrect property inheritance logic * Fixes crash in canVariantPublish when variant.language is null * Adds variant display name in dropdown * Logic for invariant properties updated to also support segment invariance * Properties varied by segment only now properly saved when multiple variants are saved/published * Logic for disabling property editors moved to function and corrected for all cases of culture/segment properties * Fixes syntax error in less file --- .../content/umbtabbedcontent.directive.js | 18 +++++ .../services/umbdataformatter.service.js | 71 ++++++++++++------- .../content/umb-tabbed-content.html | 6 +- .../editor/umb-editor-content-header.html | 2 +- .../content/overlays/publish.controller.js | 2 +- src/Umbraco.Web/Editors/ContentController.cs | 3 +- 6 files changed, 70 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 3131fbc6d3..c703311c8e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -137,6 +137,24 @@ } } ); + + $scope.propertyEditorDisabled = function (property) { + if (property.unlockInvariantValue) { + return false; + } + + var contentLanguage = $scope.content.language; + + var canEditCulture = !contentLanguage || + // If the property culture equals the content culture it can be edited + property.culture === contentLanguage.culture || + // A culture-invariant property can only be edited by the default language variant + (property.culture == null && contentLanguage.isDefault); + + var canEditSegment = property.segment === $scope.content.segment; + + return !canEditCulture || !canEditSegment; + } } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index f306f1edd5..1815d4f749 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -394,40 +394,59 @@ */ formatContentGetData: function(displayModel) { - //We need to check for invariant properties among the variant variants. - //When we detect this, we want to make sure that the property object instance is the - //same reference object between all variants instead of a copy (which it will be when - //return from the JSON structure). + // We need to check for invariant properties among the variant variants, + // as the value of an invariant property is shared between different variants. + // A property can be culture invariant, segment invariant, or both. + // When we detect this, we want to make sure that the property object instance is the + // same reference object between all variants instead of a copy (which it will be when + // return from the JSON structure). if (displayModel.variants && displayModel.variants.length > 1) { + // Collect all invariant properties from the variants that are either the + // default language variant or the default segment variant. + var defaultVariants = _.filter(displayModel.variants, function (variant) { + var isDefaultLanguage = variant.language && variant.language.isDefault; + var isDefaultSegment = variant.segment == null; - var invariantProperties = []; - - //collect all invariant properties on the first first variant - var firstVariant = displayModel.variants[0]; - _.each(firstVariant.tabs, function(tab, tabIndex) { - _.each(tab.properties, function (property, propIndex) { - //in theory if there's more than 1 variant, that means they would all have a language - //but we'll do our safety checks anyways here - if (firstVariant.language && !property.culture && !property.segment) { - invariantProperties.push({ - tabIndex: tabIndex, - propIndex: propIndex, - property: property - }); - } - }); + return isDefaultLanguage || isDefaultSegment; }); + if (defaultVariants.length > 0) { + _.each(defaultVariants, function (defaultVariant) { + var invariantProps = []; - //now assign this same invariant property instance to the same index of the other variants property array - for (var j = 1; j < displayModel.variants.length; j++) { - var variant = displayModel.variants[j]; + _.each(defaultVariant.tabs, function (tab, tabIndex) { + _.each(tab.properties, function (property, propIndex) { + // culture == null -> property is culture invariant + // segment == null -> property is *possibly* segment invariant + if (!property.culture || !property.segment) { + invariantProps.push({ + tabIndex: tabIndex, + propIndex: propIndex, + property: property + }); + } + }); + }); - _.each(invariantProperties, function (invProp) { - var tab = variant.tabs[invProp.tabIndex]; + var otherVariants = _.filter(displayModel.variants, function (variant) { + return variant !== defaultVariant; + }); - tab.properties[invProp.propIndex] = invProp.property; + // now assign this same invariant property instance to the same index of the other variants property array + _.each(otherVariants, function (variant) { + _.each(invariantProps, function (invProp) { + var tab = variant.tabs[invProp.tabIndex]; + var prop = tab.properties[invProp.propIndex]; + + var inheritsCulture = prop.culture === invProp.property.culture && prop.segment == null && invProp.property.segment == null; + var inheritsSegment = prop.segment === invProp.property.segment && !prop.culture; + + if (inheritsCulture || inheritsSegment) { + tab.properties[invProp.propIndex] = invProp.property; + } + }); + }); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index d8a98e8ee1..800e2dbfcd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -11,13 +11,13 @@ data-element="property-{{property.alias}}" ng-repeat="property in group.properties track by property.alias" property="property" - show-inherit="variantNodeModel.variants.length > 1 && ((!content.language.isDefault && !property.culture) || (content.segment && !property.segment)) && !property.unlockInvariantValue" + show-inherit="propertyEditorDisabled(property)" inherits-from="defaultVariant.language.name"> -
    +
    + preview="propertyEditorDisabled(property)">
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index e6030517b7..b65d4db7ba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -60,7 +60,7 @@
    Open in split view
    diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index 8a61025295..a1d62dc1dd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -39,7 +39,7 @@ var published = !(variant.state === "NotCreated" || variant.state === "Draft"); // is this variant mandatory: - if (variant.language.isMandatory && !published && !variant.publish) { + if (variant.language && variant.language.isMandatory && !published && !variant.publish) { //if a mandatory variant isn't published or set to be published //then we cannot continue diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index f5d72894ca..0e8787a99c 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1874,7 +1874,8 @@ namespace Umbraco.Web.Editors ? variant.PropertyCollectionDto : new ContentPropertyCollectionDto { - Properties = variant.PropertyCollectionDto.Properties.Where(x => !x.Culture.IsNullOrWhiteSpace()) + Properties = variant.PropertyCollectionDto.Properties.Where( + x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()) }; //for each variant, map the property values From 1f2b293c594775cc6afe3844bb93565a7aaf1f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 10 Feb 2020 09:19:25 +0100 Subject: [PATCH 329/610] focus styling --- .../src/less/components/editor/umb-variant-switcher.less | 6 +++--- .../views/components/editor/umb-editor-content-header.html | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index 87385caa87..8dbc070856 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -199,8 +199,8 @@ button.umb-variant-switcher__toggle { cursor: default; } -.umb-variant-switcher__item:focus, -.umb-variant-switcher__item:focus-within, +.umb-variant-switcher__item:focus .umb-variant-switcher__split-view, +.umb-variant-switcher__item:focus-within .umb-variant-switcher__split-view, .umb-variant-switcher__item:hover .umb-variant-switcher__split-view, .umb-variant-switcher__split-view:focus { display: block; @@ -233,7 +233,7 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__name-wrapper { font-size: 14px; text-align: left; - flex: 1; + flex: 1; cursor: pointer; background-color: transparent; border: none; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index b65d4db7ba..58f9340121 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -63,7 +63,7 @@ -
    Open in split view
    +
    Open in split view
    -
    Open in split view
    +
    Open in split view
    From 3fac40b4b5dbadd0e099badc7f6745478b0393e1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 10 Feb 2020 20:15:12 +1100 Subject: [PATCH 330/610] loads preview via sql paging instead of one single query --- .../Repositories/ContentRepository.cs | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index a0b211b6b2..24c9e84873 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -837,6 +837,8 @@ 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()); @@ -851,42 +853,61 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", 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 -where umbracoNode.trashed = 0 -order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", +where (umbracoNode.trashed = 0) +order by (umbracoNode.{2}), (umbracoNode.parentID), (umbracoNode.sortOrder)", SqlSyntax.GetQuotedColumnName("xml"), SqlSyntax.GetQuotedColumnName("level"), SqlSyntax.GetQuotedColumnName("level")); + var args = new object[] { new { type = NodeObjectTypeId } }; XmlElement last = null; - //NOTE: Query creates a reader - does not load all into memory - foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + long pageSize = 500; + int? itemCount = null; + long currPage = 0; + do { - 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) + // Get the paged queries + Database.BuildPageQueries(currPage, pageSize, sql, ref args, out var sqlCount, out var sqlPage); + + // get the item count once + if (itemCount == null) { - parent = xmlDoc.GetElementById(parentId); - if (parent == null) + itemCount = Database.ExecuteScalar(sqlCount, args); + } + currPage++; + + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (var row in Database.Query(sqlPage, args)) + { + 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) { - //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; + 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(); } - var xmlDocFragment = xmlDoc.CreateDocumentFragment(); - xmlDocFragment.InnerXml = xml; - - last = (XmlElement)parent.AppendChild(xmlDocFragment); - - // fix sortOrder - see notes in UpdateSortOrder - last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); - } - + } while (itemCount == pageSize); + return xmlDoc; } From 58b2c3f791e02fa7b605a00b61e59cac4742bf70 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 10 Feb 2020 11:22:44 +0100 Subject: [PATCH 331/610] Align icon in mini search (#7589) * Align icon vertically and horizontally inside icon placeholder * Add aria-hidden to search icon --- .../src/less/components/umb-mini-search.less | 11 ++++++++--- .../components/umb-mini-search/umb-mini-search.html | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less index ac15b3dcf8..68cb9b2e58 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less @@ -4,16 +4,21 @@ .icon { position: absolute; - padding: 5px 8px; + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 1px; + padding: 0; pointer-events: none; - top: 2px; color: @ui-action-discreet-type; transition: color .1s linear; } input { width: 0px; - padding-left:24px; + padding-left: 24px; margin-bottom: 0px; background-color: transparent; border-color: @ui-action-discreet-border; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html index 20ce87f0eb..93801f14b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html @@ -1,5 +1,5 @@ - + Date: Mon, 10 Feb 2020 20:46:21 +1000 Subject: [PATCH 332/610] 7168 hotkeys in rte (#7604) --- .../src/views/components/grid/grid-rte.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html b/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html index b5531f477a..cbd1ac3f30 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html @@ -1,4 +1,4 @@ 
    -
    +
    From d1503eba03ec1cfd97209a28a91b75938a618e1a Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Mon, 10 Feb 2020 20:47:07 +1000 Subject: [PATCH 333/610] update angular-aria to 1.7.9 (#7606) --- src/Umbraco.Web.UI.Client/package-lock.json | 6 +++--- src/Umbraco.Web.UI.Client/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 95beae8316..68d0bda427 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1109,9 +1109,9 @@ "integrity": "sha512-kU/fHIGf2a4a3bH7E1tzALTHk+QfoUSCK9fEcMFisd6ZWvNDwPzXWAilItqOC3EDiAXPmGHaNc9/aXiD9xrAxQ==" }, "angular-aria": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.7.5.tgz", - "integrity": "sha512-X2dGRw+PK7hrV7/X1Ns4e5P3KC/OBFi1l7z//D/v7zbZObsAx48qBoX7unsck+s4+mnO+ikNNkHG5N49VfAyRw==" + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.7.9.tgz", + "integrity": "sha512-luI3Jemd1AbOQW0krdzfEG3fM0IFtLY0bSSqIDEx3POE0XjKIC1MkrO8Csyq9PPgueLphyAPofzUwZ8YeZ88SA==" }, "angular-chart.js": { "version": "1.1.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 508409b4ea..c150af79de 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -13,7 +13,7 @@ "ace-builds": "1.4.2", "angular": "1.7.9", "angular-animate": "1.7.5", - "angular-aria": "1.7.5", + "angular-aria": "1.7.9", "angular-chart.js": "^1.1.1", "angular-cookies": "1.7.5", "angular-dynamic-locale": "0.1.37", From d96bcae5c8835734fecdbd3da36893892ed0dd4e Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Mon, 10 Feb 2020 20:46:21 +1000 Subject: [PATCH 334/610] 7168 hotkeys in rte (#7604) (cherry picked from commit 608555126ef122fdcf5c4deace06f204965f213f) --- .../src/views/components/grid/grid-rte.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html b/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html index b5531f477a..cbd1ac3f30 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html @@ -1,4 +1,4 @@ 
    -
    +
    From b32e8c5f16c54cb38940726ebd4c3f1b62d0e407 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Mon, 10 Feb 2020 20:47:07 +1000 Subject: [PATCH 335/610] update angular-aria to 1.7.9 (#7606) (cherry picked from commit d1503eba03ec1cfd97209a28a91b75938a618e1a) --- src/Umbraco.Web.UI.Client/package-lock.json | 6 +++--- src/Umbraco.Web.UI.Client/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 95beae8316..68d0bda427 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1109,9 +1109,9 @@ "integrity": "sha512-kU/fHIGf2a4a3bH7E1tzALTHk+QfoUSCK9fEcMFisd6ZWvNDwPzXWAilItqOC3EDiAXPmGHaNc9/aXiD9xrAxQ==" }, "angular-aria": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.7.5.tgz", - "integrity": "sha512-X2dGRw+PK7hrV7/X1Ns4e5P3KC/OBFi1l7z//D/v7zbZObsAx48qBoX7unsck+s4+mnO+ikNNkHG5N49VfAyRw==" + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.7.9.tgz", + "integrity": "sha512-luI3Jemd1AbOQW0krdzfEG3fM0IFtLY0bSSqIDEx3POE0XjKIC1MkrO8Csyq9PPgueLphyAPofzUwZ8YeZ88SA==" }, "angular-chart.js": { "version": "1.1.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 508409b4ea..c150af79de 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -13,7 +13,7 @@ "ace-builds": "1.4.2", "angular": "1.7.9", "angular-animate": "1.7.5", - "angular-aria": "1.7.5", + "angular-aria": "1.7.9", "angular-chart.js": "^1.1.1", "angular-cookies": "1.7.5", "angular-dynamic-locale": "0.1.37", From 0b730968edab5e76e22a4fa9ffc15ed1e20ded46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 10 Feb 2020 14:39:05 +0100 Subject: [PATCH 336/610] get and set AllowSegmentVariant --- .../src/common/services/umbdataformatter.service.js | 2 +- src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs | 1 + src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 1815d4f749..02305b77c5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -83,7 +83,7 @@ }); var saveProperties = _.map(realProperties, function (p) { - var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant'); + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant'); return saveProperty; }); diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 1f78c7372d..fefef5e2d7 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -228,6 +228,7 @@ namespace Umbraco.Web.Models.Mapping target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); if (source.Id > 0) target.Id = source.Id; diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs index 2f5822d1e3..370a459279 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs @@ -241,7 +241,8 @@ namespace Umbraco.Web.Models.Mapping SortOrder = p.SortOrder, ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, - AllowCultureVariant = p.VariesByCulture() + AllowCultureVariant = p.VariesByCulture(), + AllowSegmentVariant = p.VariesBySegment() }); } From de11eba116ea951e81d9442a2d87c103ae0921f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 10 Feb 2020 14:43:57 +0100 Subject: [PATCH 337/610] adjusted color slightly --- src/Umbraco.Web.UI.Client/src/less/variables.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 51b854cf78..f8338137f1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -118,7 +118,7 @@ //@orange: #f79c37;// updated 2019 @pink: #D93F4C;// #C3325F;// update 2019 @pinkLight: #f5c1bc;// added 2019 -@pinkExtraLight: #fde5e3;// added 2020 +@pinkExtraLight: #fee4e1;// added 2020 @pinkRedLight: #ff8a89;// added 2019 @brown: #9d8057;// added 2019 @brownLight: #e4e0dd;// added 2019 From c5e0ef90c3f8aa88fb28bd289dd2358866ba7e07 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 10 Feb 2020 14:27:54 +0000 Subject: [PATCH 338/610] Verify & check SecurityStamp --- src/Umbraco.Web/Editors/AuthenticationController.cs | 7 ++++--- src/Umbraco.Web/Editors/BackOfficeController.cs | 12 +++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index c2c481e8e4..c7c08d1b42 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -301,7 +301,7 @@ namespace Umbraco.Web.Editors if (user != null) { var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); - var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); + var callbackUrl = ConstructCallbackUrl(identityUser.Id, code, identityUser.SecurityStamp.GenerateHash()); var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", // Ensure the culture of the found user is used for the email! @@ -506,7 +506,7 @@ namespace Umbraco.Web.Editors return response; } - private string ConstructCallbackUrl(int userId, string code) + private string ConstructCallbackUrl(int userId, string code, string userSecurityStamp) { // Get an mvc helper to get the url var http = EnsureHttpContext(); @@ -516,7 +516,8 @@ namespace Umbraco.Web.Editors { area = GlobalSettings.GetUmbracoMvcArea(), u = userId, - r = code + r = code, + s = userSecurityStamp }); // Construct full URL using configured application URL (which will fall back to request) diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index e77a1b70f2..04c4627d85 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -296,11 +296,21 @@ namespace Umbraco.Web.Editors } [HttpGet] - public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode) + public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode, [Bind(Prefix = "s")]string stampHash) { var user = UserManager.FindById(userId); if (user != null) { + // Check security stamp that has been generated in forgotten password email link is the same we have stored for user + // ie the user has not been marked inactive or password changed by an admin etc + if(user.SecurityStamp.GenerateHash() != stampHash) + { + // Password, email or something changed to the user since the password reset email requested + // Add error and redirect for it to be displayed + TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { Services.TextService.Localize("login/resetCodeExpired") }; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", resetCode, UserManager, user); if (result) { From 4a0ce8572c1b1e2fb1181bd0565a637fde19368c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 10 Feb 2020 17:29:31 +0100 Subject: [PATCH 339/610] Nested Content can't add multiple items when configured with a single element type --- .../propertyeditors/nestedcontent/nestedcontent.controller.js | 1 + 1 file changed, 1 insertion(+) 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 7de3a5b567..1c6d305c6b 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 @@ -221,6 +221,7 @@ if (vm.overlayMenu.availableItems.length === 1 && vm.overlayMenu.pasteItems.length === 0) { // only one scaffold type - no need to display the picker addNode(vm.scaffolds[0].contentTypeAlias); + vm.overlayMenu = null; return; } From c5ddba47a87ed5151dc94fc404b5c16c1d347917 Mon Sep 17 00:00:00 2001 From: emma burstow <37150989+emmaburstow@users.noreply.github.com> Date: Mon, 10 Feb 2020 16:21:03 +0000 Subject: [PATCH 340/610] Update SetUmbracoVersionStep.cs Punctuation fix --- src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index b5fdea32b7..17191d2521 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -13,7 +13,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.Install.InstallSteps { [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "UmbracoVersion", 50, "Installation is complete!, get ready to be redirected to your new CMS.", + "UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.", PerformsAppRestart = true)] internal class SetUmbracoVersionStep : InstallSetupStep { From 2c9c5baae51ef6b7ec86435892564fdb3aaa843e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 10 Feb 2020 13:59:11 +0100 Subject: [PATCH 341/610] Prevent multi URL picker from crashing with JS errors when absolute (non-local) links are added. --- .../propertyeditors/multiurlpicker/multiurlpicker.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2e4313ec76..172f9b2249 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 @@ -147,7 +147,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en _.each($scope.model.value, function (item){ // we must reload the "document" link URLs to match the current editor culture - if (item.udi.indexOf("/document/") > 0) { + if (item.udi && item.udi.indexOf("/document/") > 0) { item.url = null; entityResource.getUrlByUdi(item.udi).then(function (data) { item.url = data; From 94d9acdeee33a0ac9a1ab8707def50d42978f3e5 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 10 Feb 2020 13:59:11 +0100 Subject: [PATCH 342/610] Prevent multi URL picker from crashing with JS errors when absolute (non-local) links are added. (cherry picked from commit 2c9c5baae51ef6b7ec86435892564fdb3aaa843e) --- .../propertyeditors/multiurlpicker/multiurlpicker.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2e4313ec76..172f9b2249 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 @@ -147,7 +147,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en _.each($scope.model.value, function (item){ // we must reload the "document" link URLs to match the current editor culture - if (item.udi.indexOf("/document/") > 0) { + if (item.udi && item.udi.indexOf("/document/") > 0) { item.url = null; entityResource.getUrlByUdi(item.udi).then(function (data) { item.url = data; From 8e2ed68b887f78c20a740cf3c9684f2dd15ab988 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel Date: Tue, 11 Feb 2020 09:20:30 +0000 Subject: [PATCH 343/610] Add more descriptive exception (#7591) * Add more descriptive exepection * Finish with . * Update src/Umbraco.Core/Collections/TopoGraph.cs Co-Authored-By: Sebastiaan Janssen * Update src/Umbraco.Core/Collections/TopoGraph.cs Co-Authored-By: Sebastiaan Janssen Co-authored-by: Sebastiaan Janssen --- src/Umbraco.Core/Collections/TopoGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Collections/TopoGraph.cs b/src/Umbraco.Core/Collections/TopoGraph.cs index b8ded4a458..955a210465 100644 --- a/src/Umbraco.Core/Collections/TopoGraph.cs +++ b/src/Umbraco.Core/Collections/TopoGraph.cs @@ -126,7 +126,7 @@ namespace Umbraco.Core.Collections if (_items.TryGetValue(key, out value)) yield return value; else if (throwOnMissing) - throw new Exception(MissingDependencyError); + throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}"); } } } From 6ae88f6c76373ca79939e28993572eb1c378d4f7 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 11 Feb 2020 09:56:30 +0000 Subject: [PATCH 344/610] Revert "Verify & check SecurityStamp" This reverts commit c5e0ef90c3f8aa88fb28bd289dd2358866ba7e07. --- src/Umbraco.Web/Editors/AuthenticationController.cs | 7 +++---- src/Umbraco.Web/Editors/BackOfficeController.cs | 12 +----------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index c7c08d1b42..c2c481e8e4 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -301,7 +301,7 @@ namespace Umbraco.Web.Editors if (user != null) { var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); - var callbackUrl = ConstructCallbackUrl(identityUser.Id, code, identityUser.SecurityStamp.GenerateHash()); + var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", // Ensure the culture of the found user is used for the email! @@ -506,7 +506,7 @@ namespace Umbraco.Web.Editors return response; } - private string ConstructCallbackUrl(int userId, string code, string userSecurityStamp) + private string ConstructCallbackUrl(int userId, string code) { // Get an mvc helper to get the url var http = EnsureHttpContext(); @@ -516,8 +516,7 @@ namespace Umbraco.Web.Editors { area = GlobalSettings.GetUmbracoMvcArea(), u = userId, - r = code, - s = userSecurityStamp + r = code }); // Construct full URL using configured application URL (which will fall back to request) diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 04c4627d85..e77a1b70f2 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -296,21 +296,11 @@ namespace Umbraco.Web.Editors } [HttpGet] - public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode, [Bind(Prefix = "s")]string stampHash) + public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode) { var user = UserManager.FindById(userId); if (user != null) { - // Check security stamp that has been generated in forgotten password email link is the same we have stored for user - // ie the user has not been marked inactive or password changed by an admin etc - if(user.SecurityStamp.GenerateHash() != stampHash) - { - // Password, email or something changed to the user since the password reset email requested - // Add error and redirect for it to be displayed - TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { Services.TextService.Localize("login/resetCodeExpired") }; - return RedirectToLocal(Url.Action("Default", "BackOffice")); - } - var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", resetCode, UserManager, user); if (result) { From 72ff7ab8f1b6c974b6a862ce7295b3f70d7e6187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 11 Feb 2020 12:11:32 +0100 Subject: [PATCH 345/610] on blur event for umb-mini-search --- .../src/views/components/umb-mini-search/umb-mini-search.html | 1 + .../components/umb-mini-search/umbminisearch.component.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html index 20ce87f0eb..cf92ca26fe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html @@ -9,6 +9,7 @@ ng-model="vm.model" ng-change="vm.onChange()" ng-keydown="vm.onKeyDown($event)" + ng-blur="vm.onBlur($event)" prevent-enter-submit no-dirty-check> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js index 994129708f..d7aee744e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js @@ -10,7 +10,8 @@ bindings: { model: "=", onStartTyping: "&?", - onSearch: "&?" + onSearch: "&?", + onBlur: "&?" } }); From 355fe6be21ade315b2b8a007f3c588985bf0bd03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 11 Feb 2020 12:12:01 +0100 Subject: [PATCH 346/610] Remember user filtering --- .../views/users/views/users/users.controller.js | 16 +++++++++++++--- .../src/views/users/views/users/users.html | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 935b1bdfb1..f3c28fdb9d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -14,7 +14,7 @@ vm.userStates = []; vm.selection = []; vm.newUser = {}; - vm.usersOptions = {filter:null}; + vm.usersOptions = {}; vm.userSortData = [ { label: "Name (A-Z)", key: "Name", direction: "Ascending" }, { label: "Name (Z-A)", key: "Name", direction: "Descending" }, @@ -112,6 +112,7 @@ vm.selectAll = selectAll; vm.areAllSelected = areAllSelected; vm.searchUsers = searchUsers; + vm.onBlurSearch = onBlurSearch; vm.getFilterName = getFilterName; vm.setUserStatesFilter = setUserStatesFilter; vm.setUserGroupFilter = setUserGroupFilter; @@ -150,10 +151,12 @@ function initViewOptions() { // Start with default view options. + vm.usersOptions.filter = ""; vm.usersOptions.orderBy = "Name"; vm.usersOptions.orderDirection = "Ascending"; // Update from querystring if available. + initViewOptionFromQueryString("filter"); initViewOptionFromQueryString("orderBy"); initViewOptionFromQueryString("orderDirection"); initViewOptionFromQueryString("pageNumber"); @@ -451,7 +454,8 @@ var search = _.debounce(function () { $scope.$apply(function () { - changePageNumber(1); + vm.usersOptions.pageNumber = 1; + getUsers(); }); }, 500); @@ -459,6 +463,10 @@ search(); } + function onBlurSearch() { + updateLocation("filter", vm.usersOptions.filter); + } + function getFilterName(array) { var name = vm.labels.all; var found = false; @@ -547,6 +555,7 @@ } function updateLocation(key, value) { + $location.search("filter", vm.usersOptions.filter);// update filter, but first when something else requests a url update. $location.search(key, value); } @@ -657,7 +666,8 @@ function usersOptionsAsQueryString() { var qs = "?orderBy=" + vm.usersOptions.orderBy + "&orderDirection=" + vm.usersOptions.orderDirection + - "&pageNumber=" + vm.usersOptions.pageNumber; + "&pageNumber=" + vm.usersOptions.pageNumber + + "&filter=" + vm.usersOptions.filter; qs += addUsersOptionsFilterCollectionToQueryString("userStates", vm.usersOptions.userStates); qs += addUsersOptionsFilterCollectionToQueryString("userGroups", vm.usersOptions.userGroups); 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 638e6376c3..bb53413060 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 @@ -28,7 +28,7 @@ - + From a7ff82f29a9fb41cba693d023b391ef17ae1e25a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 11 Feb 2020 11:52:44 +0000 Subject: [PATCH 347/610] Implements Shans notes on changins security stamp for chaning email/username --- .../Repositories/Implement/UserRepository.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 96abc37662..3be5102b83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -557,6 +557,16 @@ ORDER BY colName"; } } + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + + changedCols.Add("emailConfirmedDate"); + changedCols.Add("securityStampToken"); + } + //only update the changed cols if (changedCols.Count > 0) { From 4ff91ed4d1e4643998897dc6e347c132003839c9 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Tue, 11 Feb 2020 20:15:22 +0100 Subject: [PATCH 348/610] Use button element in user detail and user group views (#7584) * Use button element on user detail page * Use button element on user group page --- .../src/views/users/group.html | 59 ++++++++----------- .../src/views/users/views/user/details.html | 32 +++++----- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index 0244819655..8333528ddd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -40,13 +40,12 @@ on-remove="vm.removeSelectedItem($index, vm.userGroup.sections)"> - + @@ -61,15 +60,13 @@ on-remove="vm.clearStartNode('content')"> - + @@ -85,14 +82,13 @@ on-remove="vm.clearStartNode('media')"> - + @@ -127,13 +123,12 @@ on-edit="vm.setPermissionsForNode(node)"> - + @@ -155,13 +150,11 @@ on-remove="vm.removeSelectedItem($index, vm.userGroup.users)"> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index e0cf09da50..bb3efaede2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -75,13 +75,11 @@ on-remove="model.removeSelectedItem($index, model.user.userGroups)"> - + @@ -100,13 +98,12 @@ name="model.labels.noStartNodes"> - + @@ -125,13 +122,12 @@ name="model.labels.noStartNodes"> - + From b2141b665768b289e5fe2a1385f27cae12f6843e Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Wed, 5 Feb 2020 15:00:46 +0000 Subject: [PATCH 349/610] Slider format function should return a number --- .../src/views/propertyeditors/slider/slider.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js index a20289d076..f5f0f7f2a2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js @@ -56,7 +56,7 @@ return value.toFixed(stepDecimalPlaces); }, from: function (value) { - return value; + return Number(value); } }, "range": { From 53ca5eec88694f5c062357717b428e429a91f11b Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Tue, 11 Feb 2020 20:36:42 +0100 Subject: [PATCH 350/610] Adjust child selector sorting (#7582) * Only add move cursor to sortable child elements * Don't sort add button * Add missing docs for onSort * Change add button to use button element * Use button element for remove button and add aria-hidden to icons * Reset styling of button element * Ensure add button is full width * Remove unnecessary cursor styles since these now inherit from button element --- .../components/umbchildselector.directive.js | 17 ++++++++----- .../less/components/umb-child-selector.less | 24 ++++++++++--------- .../views/components/umb-child-selector.html | 12 ++++++---- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js index a33fd4be53..96ce8735eb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js @@ -99,13 +99,18 @@ Use this directive to render a ui component for selecting child items to a paren @param {string} parentName (binding): The parent name. @param {string} parentIcon (binding): The parent icon. @param {number} parentId (binding): The parent id. -@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. +@param {callback} onRemove (binding): Callback when removing an item.

    The callback returns:

    • child: The selected item.
    • $index: The selected item index.
    -@param {callback} onAdd (binding): Callback when the add button is clicked. +@param {callback} onAdd (binding): Callback when adding an item. +

    The callback returns:

    +
      +
    • $event: The select event.
    • +
    +@param {callback} onSort (binding): Callback when sorting an item.

    The callback returns:

    • $event: The select event.
    • @@ -174,16 +179,15 @@ Use this directive to render a ui component for selecting child items to a paren eventBindings.push(scope.$watch('parentName', function(newValue, oldValue){ if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } + if (oldValue === undefined || newValue === undefined) { return; } syncParentName(); - })); eventBindings.push(scope.$watch('parentIcon', function(newValue, oldValue){ if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } + if (oldValue === undefined || newValue === undefined) { return; } syncParentIcon(); })); @@ -191,6 +195,7 @@ Use this directive to render a ui component for selecting child items to a paren // sortable options for allowed child content types scope.sortableOptions = { axis: "y", + cancel: ".unsortable", containment: "parent", distance: 10, opacity: 0.7, @@ -199,7 +204,7 @@ Use this directive to render a ui component for selecting child items to a paren zIndex: 6000, update: function (e, ui) { if(scope.onSort) { - scope.onSort(); + scope.onSort(); } } }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less index da690663d0..937f746c56 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less @@ -16,23 +16,24 @@ .umb-child-selector__child.-placeholder { border: 1px dashed @gray-8; background: none; - cursor: pointer; text-align: center; justify-content: center; - - color:@ui-action-type; + width: 100%; + color: @ui-action-type; + &:hover { - color:@ui-action-type-hover; - border-color:@ui-action-type-hover; - text-decoration:none; + color: @ui-action-type-hover; + border-color: @ui-action-type-hover; + text-decoration: none; } } .umb-child-selector__children-container { - margin-left: 30px; - .umb-child-selector__child { - cursor: move; - } + margin-left: 30px; + + .umb-child-selector__child.ui-sortable-handle { + cursor: move; + } } .umb-child-selector__child-description { @@ -65,5 +66,6 @@ } .umb-child-selector__child-remove { - cursor: pointer; + background: none; + border: none; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html index 0866e1bdf7..1d88c0eb96 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html @@ -19,18 +19,20 @@
      - +
      - {{ selectedChild.name }} + {{selectedChild.name}}
      - +
      - + From 32960219078bc599deb461ec26a1b008d726a8ad Mon Sep 17 00:00:00 2001 From: Mark Drake Date: Tue, 11 Feb 2020 15:10:28 -0500 Subject: [PATCH 351/610] Add an Active State for Expand (...) in the Top Section Navigation --- .../components/application/umbsections.directive.js | 11 +++++++++++ src/Umbraco.Web.UI.Client/src/less/sections.less | 9 ++++++++- .../views/components/application/umb-sections.html | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index b8ee797c82..a33796ab6d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -133,6 +133,17 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se } }; + scope.currentSectionInOverflow = function () { + if (scope.overflowingSections === 0) { + return false; + } + + var currentSection = scope.sections.filter(s => s.alias === scope.currentSection); + + return (scope.sections.indexOf(currentSection[0]) >= scope.maxSections); + + }; + loadSections(); } diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 27b11b1c3b..1f19786b3c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -55,7 +55,7 @@ ul.sections { transition: opacity .1s linear, box-shadow .1s; } - &.current a { + &.current > a { color: @ui-active; &::after { @@ -76,6 +76,13 @@ ul.sections { transition: opacity .1s linear; } + &.current { + i { + opacity: 1; + background: @ui-active; + } + } + &:hover i { opacity: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html index 5688ba0e3b..0dac7176ee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html @@ -11,7 +11,7 @@ -
    • +
    • From 2fc495128e21aaadf1fad80fd12859680998c492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 29 Jan 2020 10:51:56 +0100 Subject: [PATCH 352/610] make icon of umb-preview-node align vertically. --- .../src/less/components/umb-node-preview.less | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 f754a09368..939fd79826 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 @@ -29,7 +29,8 @@ .umb-node-preview__icon { display: flex; width: 25px; - height: 25px; + min-height: 25px; + height: 100%; justify-content: center; align-items: center; font-size: 20px; From 8dc9d4ace464910bfbbe53f0418af60f348a561a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 12 Feb 2020 11:44:57 +1100 Subject: [PATCH 353/610] Updates c# ShouldRenderChildrenOfContainer to allow for nested list views --- src/Umbraco.Web/Trees/ContentTreeControllerBase.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 1069df0ec4..6d156e3fc8 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -364,7 +364,12 @@ namespace Umbraco.Web.Trees var startNodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); //if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell // the UI that this node does have children and that it isn't a container - if (startNodes.Any(x => x.ParentId == e.Id)) + + if (startNodes.Any(x => + { + var pathParts = x.Path.Split(','); + return pathParts.Contains(e.Id.ToInvariantString()); + })) { renderChildren = true; } From 7182be93d960dc621232ec911e12caba0fffcf4d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 12 Feb 2020 13:47:48 +1100 Subject: [PATCH 354/610] Ensures that we don't set IsChildOfListView when it's a user's start node since we are actually rendering that tree node when that is the case --- src/Umbraco.Web/Editors/ContentController.cs | 27 +++++++++-------- .../Models/Mapping/ContentMapDefinition.cs | 30 +++++++++++++++++-- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index f5d72894ca..521626f666 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -869,7 +869,7 @@ namespace Umbraco.Web.Editors return true; } - + /// /// Helper method to perform the saving of the content and add the notifications to the result @@ -1161,14 +1161,14 @@ namespace Umbraco.Web.Editors //validate if we can publish based on the mandatory language requirements var canPublish = ValidatePublishingMandatoryLanguages( cultureErrors, - contentItem, cultureVariants, mandatoryCultures, + contentItem, cultureVariants, mandatoryCultures, mandatoryVariant => mandatoryVariant.Publish); //Now check if there are validation errors on each variant. //If validation errors are detected on a variant and it's state is set to 'publish', then we //need to change it to 'save'. //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. - + foreach (var variant in contentItem.Variants) { if (cultureErrors.Contains(variant.Culture)) @@ -1656,14 +1656,14 @@ namespace Umbraco.Web.Editors [HttpPost] public DomainSave PostSaveLanguageAndDomains(DomainSave model) { - foreach(var domain in model.Domains) + foreach (var domain in model.Domains) { try { var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, Request.RequestUri); } catch (UriFormatException) - { + { var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain/invalidDomain")); throw new HttpResponseException(response); } @@ -1829,7 +1829,7 @@ namespace Umbraco.Web.Editors base.HandleInvalidModelState(display); } - + /// /// Maps the dto property values and names to the persisted model /// @@ -1842,7 +1842,7 @@ namespace Umbraco.Web.Editors var culture = property.PropertyType.VariesByCulture() ? variant.Culture : null; var segment = property.PropertyType.VariesBySegment() ? variant.Segment : null; return (culture, segment); - } + } var variantIndex = 0; @@ -1884,15 +1884,15 @@ namespace Umbraco.Web.Editors (save, property) => { // Get property value - (var culture, var segment) = PropertyCultureAndSegment(property, variant); - return property.GetValue(culture, segment); + (var culture, var segment) = PropertyCultureAndSegment(property, variant); + return property.GetValue(culture, segment); }, (save, property, v) => { // Set property value (var culture, var segment) = PropertyCultureAndSegment(property, variant); - property.SetValue(v, culture, segment); - }, + property.SetValue(v, culture, segment); + }, variant.Culture); variantIndex++; @@ -2172,7 +2172,10 @@ namespace Umbraco.Web.Editors /// private ContentItemDisplay MapToDisplay(IContent content) { - var display = Mapper.Map(content); + var display = Mapper.Map(content, context => + { + context.Items["CurrentUser"] = Security.CurrentUser; + }); display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; return display; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index dc0df4ca96..be84b13a7e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -5,6 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Routing; @@ -80,7 +81,7 @@ namespace Umbraco.Web.Models.Mapping target.Icon = source.ContentType.Icon; target.Id = source.Id; target.IsBlueprint = source.Blueprint; - target.IsChildOfListView = DermineIsChildOfListView(source); + target.IsChildOfListView = DetermineIsChildOfListView(source, context); target.IsContainer = source.ContentType.IsContainer; target.IsElement = source.ContentType.IsElement; target.Key = source.Key; @@ -211,8 +212,33 @@ namespace Umbraco.Web.Models.Mapping return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"({source.Name})"; } - private bool DermineIsChildOfListView(IContent source) + /// + /// Checks if the content item is a descendant of a list view + /// + /// + /// + /// + /// Returns true if the content item is a descendant of a list view and where the content is + /// not a current user's start node. + /// + /// + /// We must check if it's the current user's start node because in that case we will actually be + /// rendering the tree node underneath the list view to visually show context. In this case we return + /// false because the item is technically not being rendered as part of a list view but instead as a + /// real tree node. If we didn't perform this check then tree syncing wouldn't work correctly. + /// + private bool DetermineIsChildOfListView(IContent source, MapperContext context) { + // In cases where a user's start node is below a list view, we will actually render + // out the tree to that start node and in that case for that start node, we want to return + // false here. + if (context.HasItems && context.Items.TryGetValue("CurrentUser", out var usr) && usr is IUser currentUser) + { + if (currentUser.StartContentIds.Contains(source.Id)) + return false; + } + + // map the IsChildOfListView (this is actually if it is a descendant of a list view!) var parent = _contentService.GetParent(source); return parent != null && (parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(parent.Path)); From e90e32b871b1d86269f47f7b60b159f8629a4773 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 12 Feb 2020 10:11:28 +0000 Subject: [PATCH 355/610] Add unit test to verify SecurityStamp changes/invalidates when the userlogin changes --- .../Repositories/UserRepositoryTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 3ba00e54cf..bbefb79f6b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -421,6 +421,35 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Invalidate_SecurityStamp_On_Username_Change() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + var originalSecurityStamp = user.SecurityStamp; + + // Ensure when user generated a security stamp is present + Assert.That(user.SecurityStamp, Is.Not.Null); + Assert.That(user.SecurityStamp, Is.Not.Empty); + + // Update username + user.Username = user.Username + "UPDATED"; + repository.Save(user); + + // Get the user + var updatedUser = repository.Get(user.Id); + + // Ensure the Security Stamp is invalidated & no longer the same + Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); + } + } + private void AssertPropertyValues(IUser updatedItem, IUser originalUser) { Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); From ffd43131c47bbd516434e9e71d67ab0090a5811b Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 12 Feb 2020 10:41:31 +0000 Subject: [PATCH 356/610] Merge pull request #7627 from umbraco/v8/bugfix/AB4828-resetpassword-mail AB4828 - Reset Password Email (cherry picked from commit f00680bfe69d09f9eda123b8218b2508d8f5ca3d) --- .../Repositories/Implement/UserRepository.cs | 10 +++++++ .../Repositories/UserRepositoryTest.cs | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 96abc37662..3be5102b83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -557,6 +557,16 @@ ORDER BY colName"; } } + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + + changedCols.Add("emailConfirmedDate"); + changedCols.Add("securityStampToken"); + } + //only update the changed cols if (changedCols.Count > 0) { diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 3ba00e54cf..bbefb79f6b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -421,6 +421,35 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Invalidate_SecurityStamp_On_Username_Change() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + var originalSecurityStamp = user.SecurityStamp; + + // Ensure when user generated a security stamp is present + Assert.That(user.SecurityStamp, Is.Not.Null); + Assert.That(user.SecurityStamp, Is.Not.Empty); + + // Update username + user.Username = user.Username + "UPDATED"; + repository.Save(user); + + // Get the user + var updatedUser = repository.Get(user.Id); + + // Ensure the Security Stamp is invalidated & no longer the same + Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); + } + } + private void AssertPropertyValues(IUser updatedItem, IUser originalUser) { Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); From 9c428b7f0194197ab235c5799bb237d35b8b3a10 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 12 Feb 2020 11:55:51 +0000 Subject: [PATCH 357/610] Bump version to 8.5.4 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 841e986f15..fda69955d8 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.5.3")] -[assembly: AssemblyInformationalVersion("8.5.3")] +[assembly: AssemblyFileVersion("8.5.4")] +[assembly: AssemblyInformationalVersion("8.5.4")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index e12eb8d19b..b465f89cb5 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8530 + 8540 / - http://localhost:8530 + http://localhost:8540 False False @@ -429,4 +429,4 @@ - + \ No newline at end of file From 6a6978d8da08ba6f6e74c6b948cab35b01a0041a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 12 Feb 2020 10:41:31 +0000 Subject: [PATCH 358/610] Merge pull request #7627 from umbraco/v8/bugfix/AB4828-resetpassword-mail AB4828 - Reset Password Email (cherry picked from commit f00680bfe69d09f9eda123b8218b2508d8f5ca3d) --- .../Repositories/Implement/UserRepository.cs | 10 +++++++ .../Repositories/UserRepositoryTest.cs | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 96abc37662..3be5102b83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -557,6 +557,16 @@ ORDER BY colName"; } } + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + + changedCols.Add("emailConfirmedDate"); + changedCols.Add("securityStampToken"); + } + //only update the changed cols if (changedCols.Count > 0) { diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 3e5919d7f3..a5787d59e4 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -409,6 +409,35 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Invalidate_SecurityStamp_On_Username_Change() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + var originalSecurityStamp = user.SecurityStamp; + + // Ensure when user generated a security stamp is present + Assert.That(user.SecurityStamp, Is.Not.Null); + Assert.That(user.SecurityStamp, Is.Not.Empty); + + // Update username + user.Username = user.Username + "UPDATED"; + repository.Save(user); + + // Get the user + var updatedUser = repository.Get(user.Id); + + // Ensure the Security Stamp is invalidated & no longer the same + Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); + } + } + private void AssertPropertyValues(IUser updatedItem, IUser originalUser) { Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); From 1574263c6966f77d11f8dcf469d510ac2592ca98 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 12 Feb 2020 10:41:31 +0000 Subject: [PATCH 359/610] Merge pull request #7627 from umbraco/v8/bugfix/AB4828-resetpassword-mail AB4828 - Reset Password Email (cherry picked from commit f00680bfe69d09f9eda123b8218b2508d8f5ca3d) --- .../Repositories/Implement/UserRepository.cs | 10 +++++++ .../Repositories/UserRepositoryTest.cs | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 96abc37662..3be5102b83 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -557,6 +557,16 @@ ORDER BY colName"; } } + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + + changedCols.Add("emailConfirmedDate"); + changedCols.Add("securityStampToken"); + } + //only update the changed cols if (changedCols.Count > 0) { diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 3ba00e54cf..bbefb79f6b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -421,6 +421,35 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Invalidate_SecurityStamp_On_Username_Change() + { + // Arrange + var provider = TestObjects.GetScopeProvider(Logger); + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + var originalSecurityStamp = user.SecurityStamp; + + // Ensure when user generated a security stamp is present + Assert.That(user.SecurityStamp, Is.Not.Null); + Assert.That(user.SecurityStamp, Is.Not.Empty); + + // Update username + user.Username = user.Username + "UPDATED"; + repository.Save(user); + + // Get the user + var updatedUser = repository.Get(user.Id); + + // Ensure the Security Stamp is invalidated & no longer the same + Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); + } + } + private void AssertPropertyValues(IUser updatedItem, IUser originalUser) { Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); From a7ad464b40f3b3039f0137317cdc6a8170b181c0 Mon Sep 17 00:00:00 2001 From: abi Date: Sun, 2 Feb 2020 01:43:15 +0000 Subject: [PATCH 360/610] Remove Empty Statements --- src/Umbraco.Core/Cache/WebCachingAppCache.cs | 2 +- src/Umbraco.Core/HashGenerator.cs | 2 +- .../MergeDateAndDateTimePropertyEditor.cs | 2 +- .../Packaging/ConflictingPackageData.cs | 4 ++-- .../Packaging/PackagesRepository.cs | 11 ++++----- .../Repositories/Implement/MacroRepository.cs | 3 +-- .../Implement/TemplateRepository.cs | 1 - src/Umbraco.Core/Xml/XmlHelper.cs | 6 ++--- .../UmbracoTestDataController.cs | 14 +++++------ src/Umbraco.Tests/Logging/LogviewerTests.cs | 2 +- .../Repositories/MacroRepositoryTest.cs | 3 +-- .../Templates/Gallery.cshtml | 4 ++-- .../Partials/Grid/Bootstrap3-Fluid.cshtml | 14 +++++------ .../Views/Partials/Grid/Bootstrap3.cshtml | 4 ++-- src/Umbraco.Web/Cache/MacroCacheRefresher.cs | 2 +- src/Umbraco.Web/Editors/DataTypeController.cs | 2 +- .../Editors/PackageInstallController.cs | 4 ++-- .../Media/Exif/ExifBitConverter.cs | 2 +- .../Media/Exif/ExifExtendedProperty.cs | 24 +++++++++---------- .../Media/Exif/JFIFExtendedProperty.cs | 2 +- src/Umbraco.Web/Media/Exif/JPEGSection.cs | 2 +- src/Umbraco.Web/Media/Exif/MathEx.cs | 24 +++++++++---------- 22 files changed, 65 insertions(+), 69 deletions(-) diff --git a/src/Umbraco.Core/Cache/WebCachingAppCache.cs b/src/Umbraco.Core/Cache/WebCachingAppCache.cs index c6e104221a..044aab4c42 100644 --- a/src/Umbraco.Core/Cache/WebCachingAppCache.cs +++ b/src/Umbraco.Core/Cache/WebCachingAppCache.cs @@ -86,7 +86,7 @@ namespace Umbraco.Core.Cache protected override void EnterWriteLock() { - _locker.EnterWriteLock();; + _locker.EnterWriteLock(); } protected override void ExitReadLock() diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 80cc3c3de4..58034f05d7 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core internal void AddDateTime(DateTime d) { - _writer.Write(d.Ticks);; + _writer.Write(d.Ticks); } internal void AddString(string s) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index 0d451e8460..37a83bdd67 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 if (string.IsNullOrEmpty(dataType.Configuration)) { config.Format = "YYYY-MM-DD"; - }; + } } catch (Exception ex) { diff --git a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs index 82693677fb..33cb9c40d3 100644 --- a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs +++ b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Packaging { - internal class ConflictingPackageData + internal class ConflictingPackageData { private readonly IMacroService _macroService; private readonly IFileService _fileService; @@ -23,7 +23,7 @@ namespace Umbraco.Core.Packaging return stylesheetNodes .Select(n => { - var xElement = n.Element("Name") ?? n.Element("name"); ; + var xElement = n.Element("Name") ?? n.Element("name"); if (xElement == null) throw new FormatException("Missing \"Name\" element"); diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 72954b238d..5549b502fa 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -82,7 +82,7 @@ namespace Umbraco.Core.Packaging { var packagesXml = EnsureStorage(out _); if (packagesXml?.Root == null) - yield break;; + yield break; foreach (var packageXml in packagesXml.Root.Elements("package")) yield return _parser.ToPackageDefinition(packageXml); @@ -139,7 +139,7 @@ namespace Umbraco.Core.Packaging var updatedXml = _parser.ToXml(definition); packageXml.ReplaceWith(updatedXml); } - + packagesXml.Save(packagesFile); return true; @@ -212,7 +212,7 @@ namespace Umbraco.Core.Packaging compiledPackageXml.Save(packageXmlFileName); // check if there's a packages directory below media - + if (Directory.Exists(IOHelper.MapPath(_mediaFolderPath)) == false) Directory.CreateDirectory(IOHelper.MapPath(_mediaFolderPath)); @@ -510,7 +510,6 @@ namespace Umbraco.Core.Packaging private XElement GetStylesheetXml(string name, bool includeProperties) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); -; var sts = _fileService.GetStylesheetByName(name); if (sts == null) return null; var stylesheetXml = new XElement("Stylesheet"); @@ -562,7 +561,7 @@ namespace Umbraco.Core.Packaging package.Add(new XElement("url", definition.Url)); var requirements = new XElement("requirements"); - + requirements.Add(new XElement("major", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Major.ToInvariantString() : definition.UmbracoVersion.Major.ToInvariantString())); requirements.Add(new XElement("minor", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Minor.ToInvariantString() : definition.UmbracoVersion.Minor.ToInvariantString())); requirements.Add(new XElement("patch", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Patch.ToInvariantString() : definition.UmbracoVersion.Build.ToInvariantString())); @@ -589,7 +588,7 @@ namespace Umbraco.Core.Packaging contributors.Add(new XElement("contributor", contributor)); } } - + info.Add(contributors); info.Add(new XElement("readme", new XCData(definition.Readme))); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs index f0044e225d..1f0b45614b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs @@ -40,7 +40,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (macroDto == null) return null; - + var entity = MacroFactory.BuildEntity(macroDto); // reset dirty initial properties (U4-1946) @@ -153,7 +153,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMacro entity) { entity.UpdatingEntity(); -; var dto = MacroFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs index b348317989..2175780916 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs @@ -215,7 +215,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //Save updated entity to db template.UpdateDate = DateTime.Now; - ; var dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); Database.Update(dto.NodeDto); diff --git a/src/Umbraco.Core/Xml/XmlHelper.cs b/src/Umbraco.Core/Xml/XmlHelper.cs index 2dd955086d..d6461ec8c6 100644 --- a/src/Umbraco.Core/Xml/XmlHelper.cs +++ b/src/Umbraco.Core/Xml/XmlHelper.cs @@ -214,9 +214,9 @@ namespace Umbraco.Core.Xml var xmlDoc = new XmlDocument(); //Load the file into the XmlDocument xmlDoc.Load(reader); - + return xmlDoc; - } + } } /// @@ -335,7 +335,7 @@ namespace Umbraco.Core.Xml var child = parent.SelectSingleNode(name); if (child != null) { - child.InnerXml = ""; ; + child.InnerXml = ""; return child; } return AddCDataNode(xd, name, value); diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 402c05cc1c..02949d5345 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -68,7 +68,7 @@ namespace Umbraco.TestData scope.Complete(); } - + return Content("Done"); } @@ -89,7 +89,7 @@ namespace Umbraco.TestData message = "Count not high enough for specified for number of levels required"; return false; } - + return true; } @@ -140,7 +140,7 @@ namespace Umbraco.TestData currChildCount = prev.childCount; // restore the parent parent = prev.parent; - + } else if (contentItem.Level < depth) { @@ -149,10 +149,10 @@ namespace Umbraco.TestData // not at max depth, create below parent = created.container(); - + currChildCount = 0; } - + } } @@ -219,7 +219,7 @@ namespace Umbraco.TestData { var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); content.SetValue("review", faker.Rant.Review()); - content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); ; + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); Services.ContentService.Save(content); @@ -259,7 +259,7 @@ namespace Umbraco.TestData return docType; } - private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); diff --git a/src/Umbraco.Tests/Logging/LogviewerTests.cs b/src/Umbraco.Tests/Logging/LogviewerTests.cs index ed9deec177..08ba070b70 100644 --- a/src/Umbraco.Tests/Logging/LogviewerTests.cs +++ b/src/Umbraco.Tests/Logging/LogviewerTests.cs @@ -163,7 +163,7 @@ namespace Umbraco.Tests.Logging //Query @Level='Warning' BUT we pass in array of LogLevels for Debug & Info (Expect to get 0 results) string[] logLevelMismatch = { "Debug", "Information" }; - var filterLevelQuery = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, filterExpression: "@Level='Warning'", logLevels: logLevelMismatch); ; + var filterLevelQuery = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, filterExpression: "@Level='Warning'", logLevels: logLevelMismatch); Assert.AreEqual(0, filterLevelQuery.TotalItems); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs index 6f215f4a35..9eca06d4e0 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs @@ -38,7 +38,6 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = new MacroRepository((IScopeAccessor) provider, AppCaches.Disabled, Mock.Of()); var macro = new Macro("test1", "Test", "~/views/macropartials/test.cshtml", MacroTypes.PartialView); - ; Assert.Throws(() => repository.Save(macro)); } @@ -56,7 +55,7 @@ namespace Umbraco.Tests.Persistence.Repositories var macro = repository.Get(1); macro.Alias = "test2"; - + Assert.Throws(() => repository.Save(macro)); } diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml index 5aa09a3cb3..64be8eb60c 100755 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml @@ -29,13 +29,13 @@ @* a single image *@ if (media.IsDocumentType("Image")) { - @Render(media); + @Render(media) } @* a folder with images under it *@ foreach (var image in media.Children()) { - @Render(image); + @Render(image) } } diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml index defe59d808..131b0515ae 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml @@ -2,31 +2,31 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* +@* Razor helpers located at the bottom of this file *@ @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; - +
      @if (oneColumn) { foreach (var section in Model.sections) {
      @foreach (var row in section.rows) { - @renderRow(row); + @renderRow(row) }
      - } - }else { + } + }else {
      @foreach (var s in Model.sections) {
      @foreach (var row in s.rows) { - @renderRow(row); + @renderRow(row) }
      @@ -85,4 +85,4 @@ return new MvcHtmlString(string.Join(" ", attrs)); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index 9333628ed6..23fee33043 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -12,7 +12,7 @@ foreach (var section in Model.sections) {
      @foreach (var row in section.rows) { - @renderRow(row, true); + @renderRow(row, true) }
      } @@ -23,7 +23,7 @@
      @foreach (var row in s.rows) { - @renderRow(row, false); + @renderRow(row, false) }
      diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs index 24479b8415..0cecba7b7b 100644 --- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs @@ -53,7 +53,7 @@ namespace Umbraco.Web.Cache { macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); } - }; + } base.Refresh(json); } diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 5f5f5104cb..ad92d40ecf 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -427,7 +427,7 @@ namespace Umbraco.Web.Editors { var propertyEditor = propertyEditors.SingleOrDefault(x => x.Alias == dataType.Alias); if (propertyEditor != null) - dataType.HasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); ; + dataType.HasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); } var grouped = dataTypes diff --git a/src/Umbraco.Web/Editors/PackageInstallController.cs b/src/Umbraco.Web/Editors/PackageInstallController.cs index 6b2bd07fbd..1030498734 100644 --- a/src/Umbraco.Web/Editors/PackageInstallController.cs +++ b/src/Umbraco.Web/Editors/PackageInstallController.cs @@ -102,8 +102,8 @@ namespace Umbraco.Web.Editors model.LicenseUrl = ins.LicenseUrl; model.Readme = ins.Readme; model.ConflictingMacroAliases = ins.Warnings.ConflictingMacros.ToDictionary(x => x.Name, x => x.Alias); - model.ConflictingStyleSheetNames = ins.Warnings.ConflictingStylesheets.ToDictionary(x => x.Name, x => x.Alias); ; - model.ConflictingTemplateAliases = ins.Warnings.ConflictingTemplates.ToDictionary(x => x.Name, x => x.Alias); ; + model.ConflictingStyleSheetNames = ins.Warnings.ConflictingStylesheets.ToDictionary(x => x.Name, x => x.Alias); + model.ConflictingTemplateAliases = ins.Warnings.ConflictingTemplates.ToDictionary(x => x.Name, x => x.Alias); model.ContainsUnsecureFiles = ins.Warnings.UnsecureFiles.Any(); model.Url = ins.Url; model.Version = ins.Version; diff --git a/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs b/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs index 86bd12c15e..1dcae62acd 100644 --- a/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs +++ b/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Media.Exif public ExifBitConverter(ByteOrder from, ByteOrder to) : base(from, to) { - ; + } #endregion diff --git a/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs b/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs index 63c1ce3365..8889a13e1d 100644 --- a/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs +++ b/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Media.Exif public ExifEnumProperty(ExifTag tag, T value) : this(tag, value, false) { - ; + } public override ExifInterOperability Interoperability @@ -210,13 +210,13 @@ namespace Umbraco.Web.Media.Exif public ExifPointSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) - : base(tag, new ushort[] { x, y }) + : base(tag, new ushort[] {x, y}) { - ; + } } @@ -239,13 +239,13 @@ namespace Umbraco.Web.Media.Exif public ExifCircularSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) : base(tag, new ushort[] { x, y, d }) { - ; + } } @@ -269,13 +269,13 @@ namespace Umbraco.Web.Media.Exif public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifRectangularSubjectArea(ExifTag tag, ushort x, ushort y, ushort w, ushort h) : base(tag, new ushort[] { x, y, w, h }) { - ; + } } @@ -303,13 +303,13 @@ namespace Umbraco.Web.Media.Exif public GPSLatitudeLongitude(ExifTag tag, MathEx.UFraction32[] value) : base(tag, value) { - ; + } public GPSLatitudeLongitude(ExifTag tag, float d, float m, float s) : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) { - ; + } } @@ -331,13 +331,13 @@ namespace Umbraco.Web.Media.Exif public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) : base(tag, value) { - ; + } public GPSTimeStamp(ExifTag tag, float h, float m, float s) : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(h), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) { - ; + } } diff --git a/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs b/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs index 94b255f4d1..24b3ac74be 100644 --- a/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs +++ b/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Media.Exif public JFIFVersion(ExifTag tag, ushort value) : base(tag, value) { - ; + } public override string ToString() diff --git a/src/Umbraco.Web/Media/Exif/JPEGSection.cs b/src/Umbraco.Web/Media/Exif/JPEGSection.cs index a1bc420fe4..78565d2bfa 100644 --- a/src/Umbraco.Web/Media/Exif/JPEGSection.cs +++ b/src/Umbraco.Web/Media/Exif/JPEGSection.cs @@ -45,7 +45,7 @@ public JPEGSection(JPEGMarker marker) : this(marker, new byte[0], new byte[0]) { - ; + } #endregion diff --git a/src/Umbraco.Web/Media/Exif/MathEx.cs b/src/Umbraco.Web/Media/Exif/MathEx.cs index 735358c40a..94cbccfbda 100644 --- a/src/Umbraco.Web/Media/Exif/MathEx.cs +++ b/src/Umbraco.Web/Media/Exif/MathEx.cs @@ -403,37 +403,37 @@ namespace Umbraco.Web.Media.Exif public Fraction32(int numerator, int denominator) : this(numerator, denominator, 0) { - ; + } public Fraction32(int numerator) : this(numerator, (int)1) { - ; + } public Fraction32(Fraction32 f) : this(f.Numerator, f.Denominator, f.Error) { - ; + } public Fraction32(float value) : this((double)value) { - ; + } public Fraction32(double value) : this(FromDouble(value)) { - ; + } public Fraction32(string s) : this(FromString(s)) { - ; + } #endregion @@ -1033,37 +1033,37 @@ namespace Umbraco.Web.Media.Exif public UFraction32(uint numerator, uint denominator) : this(numerator, denominator, 0) { - ; + } public UFraction32(uint numerator) : this(numerator, (uint)1) { - ; + } public UFraction32(UFraction32 f) : this(f.Numerator, f.Denominator, f.Error) { - ; + } public UFraction32(float value) : this((double)value) { - ; + } public UFraction32(double value) : this(FromDouble(value)) { - ; + } public UFraction32(string s) : this(FromString(s)) { - ; + } #endregion From 450a77fff56b17dca9e714ad6153bbb4778331ef Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Wed, 12 Feb 2020 15:21:03 +0100 Subject: [PATCH 361/610] Missing onClose for tour (#7595) --- .../components/application/umbtour/umbtourstep.directive.js | 6 +++--- .../src/views/components/application/umb-tour.html | 2 +- .../views/components/application/umbtour/umb-tour-step.html | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js index 2dc0ebdf93..381ecc76c3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js @@ -18,9 +18,9 @@ function link(scope, element, attrs, ctrl) { - scope.close = function() { - if(scope.onClose) { - scope.onClose(); + scope.close = function () { + if (scope.onClose) { + scope.onClose(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index e358d75b9e..064dcf6945 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -70,7 +70,7 @@
      - +

      Oh, we got lost!

      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html index a2caff4ff2..9e161d6b4e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html @@ -1,8 +1,7 @@
      -
      -
      /// For generic properties, the value is null. [DataMember] - internal Lazy PropertyGroupId + public Lazy PropertyGroupId { get => _propertyGroupId; set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); From 5307a8cf13975a07f987b93b8a45d73eb88ea5df Mon Sep 17 00:00:00 2001 From: ksingercoffey <48767938+ksingercoffey@users.noreply.github.com> Date: Wed, 12 Feb 2020 22:41:19 -0800 Subject: [PATCH 363/610] #7643: add type="button" to prevent default clicks by pressing Enter (#7651) --- .../src/views/propertyeditors/mediapicker/mediapicker.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index b17906272d..c4dba4d373 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -36,16 +36,16 @@
      - -
    • -
    • From 69a1729967da863219326292da959d0686a7928e Mon Sep 17 00:00:00 2001 From: Paul Seal Date: Thu, 13 Feb 2020 08:07:46 +0000 Subject: [PATCH 364/610] Change the icon in the Document Types Tree to be the chosen icon instead of a default one. (#7358) * - changed the icon in the document types tree to be the icon chose rather than the default doc type icon * - rendered icons for media types and member types * - used null coalescing for icon setting for better null handling Co-authored-by: paulmoriyama <48755798+paulmoriyama@users.noreply.github.com> --- src/Umbraco.Web/Trees/ContentTypeTreeController.cs | 9 ++++++--- src/Umbraco.Web/Trees/MediaTypeTreeController.cs | 9 +++++++-- src/Umbraco.Web/Trees/MemberTypeTreeController.cs | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index e8f8fb9f75..ece16229b2 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -69,15 +69,18 @@ namespace Umbraco.Web.Trees .OrderBy(entity => entity.Name) .Select(dt => { + // get the content type here so we can get the icon from it to use when we create the tree node + // and we can enrich the result with content type data that's not available in the entity service output + var contentType = contentTypes[dt.Id]; + // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, Constants.Icons.ContentType, hasChildren); + var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, contentType?.Icon ?? Constants.Icons.ContentType, hasChildren); node.Path = dt.Path; - // enrich the result with content type data that's not available in the entity service output - var contentType = contentTypes[dt.Id]; + // now we can enrich the result with content type data that's not available in the entity service output node.Alias = contentType.Alias; node.AdditionalData["isElement"] = contentType.IsElement; diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 3d0046c319..7a9a80c8fc 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -25,10 +25,12 @@ namespace Umbraco.Web.Trees public class MediaTypeTreeController : TreeController, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher; + private readonly IMediaTypeService _mediaTypeService; - public MediaTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + public MediaTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, IMediaTypeService mediaTypeService) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _treeSearcher = treeSearcher; + _mediaTypeService = mediaTypeService; } protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) @@ -54,6 +56,8 @@ namespace Umbraco.Web.Trees // if the request is for folders only then just return if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + var mediaTypes = _mediaTypeService.GetAll(); + nodes.AddRange( Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.MediaType) .OrderBy(entity => entity.Name) @@ -62,7 +66,8 @@ namespace Umbraco.Web.Trees // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, Constants.Icons.MediaType, hasChildren); + var mt = mediaTypes.FirstOrDefault(x => x.Id == dt.Id); + var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, mt?.Icon ?? Constants.Icons.MediaType, hasChildren); node.Path = dt.Path; return node; diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index 2046baf2d3..5db9088f20 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Trees { return Services.MemberTypeService.GetAll() .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, Constants.Icons.MemberType, false)); + .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, dt?.Icon ?? Constants.Icons.MemberType, false)); } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) From 68ee537520cee054adbb5ca1151a7a7caf99aaab Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Thu, 13 Feb 2020 08:10:06 +0000 Subject: [PATCH 365/610] Accessibility improvements to user group screen (#7414) --- .../components/users/umb-user-preview.html | 18 ++++++++---------- .../src/views/users/group.html | 3 +++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html index 32aa5ed1a2..014297ae51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html @@ -1,12 +1,11 @@
      - +
      @@ -15,7 +14,6 @@
      - Remove -
      - -
      \ No newline at end of file + +
      + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index 8333528ddd..eae6dbd75c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -60,6 +60,7 @@ on-remove="vm.clearStartNode('content')"> + + @@ -82,6 +84,7 @@ on-remove="vm.clearStartNode('media')"> + - + @@ -60,7 +60,7 @@
      Open in split view
      diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index a1d62dc1dd..de4d4847fb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function PublishController($scope, localizationService, contentEditingHelper, contentVariantUtilities) { + function PublishController($scope, localizationService, contentEditingHelper) { var vm = this; vm.loading = true; @@ -12,8 +12,6 @@ vm.dirtyVariantFilter = dirtyVariantFilter; vm.pristineVariantFilter = pristineVariantFilter; - $scope.getVariantDisplayName = contentVariantUtilities.getDisplayName; - /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index a99b8f3ca3..e9e179c64f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -21,7 +21,7 @@ on-change="vm.changeSelection(variant)" disabled="(variant.canPublish === false)" server-validation-field="{{variant.htmlId}}" - text="{{getVariantDisplayName(variant)}}" + text="{{variant.displayName}}" />
      @@ -51,7 +51,7 @@
      - + *
      diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs index aff79d7b9d..2a2ec49002 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs @@ -24,6 +24,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "name", IsRequired = true)] public string Name { get; set; } + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + /// /// Defines the tabs containing display properties /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index c811c236d4..8c9b65e312 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Models.Mapping _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); - _contentVariantMapper = new ContentVariantMapper(_localizationService); + _contentVariantMapper = new ContentVariantMapper(_localizationService, localizedTextService); } public void DefineMaps(UmbracoMapper mapper) @@ -102,7 +102,7 @@ namespace Umbraco.Web.Models.Mapping target.ContentDto.Properties = context.MapEnumerable(source.Properties); } - // Umbraco.Code.MapAll -Segment -Language + // Umbraco.Code.MapAll -Segment -Language -DisplayName private void Map(IContent source, ContentVariantDisplay target, MapperContext context) { target.CreateDate = source.CreateDate; diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 0ed8ffb80e..406c2090c2 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Umbraco.Core; using Umbraco.Core.Mapping; @@ -13,10 +14,12 @@ namespace Umbraco.Web.Models.Mapping internal class ContentVariantMapper { private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService _localizedTextService; - public ContentVariantMapper(ILocalizationService localizationService) + public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) { _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } public IEnumerable Map(IContent source, MapperContext context) @@ -138,8 +141,31 @@ namespace Umbraco.Web.Models.Mapping variantDisplay.Segment = segment; variantDisplay.Language = language; variantDisplay.Name = content.GetCultureName(language?.IsoCode); + variantDisplay.DisplayName = GetDisplayName(language, segment); return variantDisplay; } + + private string GetDisplayName(Language language, string segment) + { + var isCultureVariant = language != null; + var isSegmentVariant = !segment.IsNullOrWhiteSpace(); + + if(!isCultureVariant && !isSegmentVariant) + { + return _localizedTextService.Localize("general/default"); + } + + var parts = new List(); + + if (isCultureVariant) + parts.Add(new CultureInfo(language.IsoCode).NativeName); + + if (isSegmentVariant) + parts.Add(segment); + + return string.Join(" - ", parts); + + } } } From cac745fa78f9729b41789bd32812b3687cb08da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 18 Feb 2020 13:51:03 +0100 Subject: [PATCH 376/610] segment before language in GetDisplayName --- .../Models/Mapping/ContentVariantMapper.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 406c2090c2..0b6be53045 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -141,7 +141,7 @@ namespace Umbraco.Web.Models.Mapping variantDisplay.Segment = segment; variantDisplay.Language = language; variantDisplay.Name = content.GetCultureName(language?.IsoCode); - variantDisplay.DisplayName = GetDisplayName(language, segment); + variantDisplay.DisplayName = GetDisplayName(language, segment); return variantDisplay; } @@ -152,20 +152,20 @@ namespace Umbraco.Web.Models.Mapping var isSegmentVariant = !segment.IsNullOrWhiteSpace(); if(!isCultureVariant && !isSegmentVariant) - { + { return _localizedTextService.Localize("general/default"); } var parts = new List(); - if (isCultureVariant) - parts.Add(new CultureInfo(language.IsoCode).NativeName); - if (isSegmentVariant) parts.Add(segment); - return string.Join(" - ", parts); - + if (isCultureVariant) + parts.Add(language.Name); + + return string.Join(" — ", parts); + } } } From 166c04b1b644a31ee4b12f3493d45c2b2ea7ef03 Mon Sep 17 00:00:00 2001 From: elitsa Date: Tue, 18 Feb 2020 14:45:49 +0100 Subject: [PATCH 377/610] Injecting a service interface instead of calling an old web service on Our --- .../Editors/UpdateCheckController.cs | 25 +++++++++++++------ src/Umbraco.Web/Install/InstallHelper.cs | 25 +++++++++---------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web/Editors/UpdateCheckController.cs b/src/Umbraco.Web/Editors/UpdateCheckController.cs index 132526576b..480298fea7 100644 --- a/src/Umbraco.Web/Editors/UpdateCheckController.cs +++ b/src/Umbraco.Web/Editors/UpdateCheckController.cs @@ -2,11 +2,14 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading.Tasks; using System.Web.Http.Filters; +using Semver; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Web.Mvc; @@ -15,8 +18,17 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class UpdateCheckController : UmbracoAuthorizedJsonController { + private readonly IUpgradeService _upgradeService; + + public UpdateCheckController() { } + + public UpdateCheckController(IUpgradeService upgradeService) + { + _upgradeService = upgradeService; + } + [UpdateCheckResponseFilter] - public UpgradeCheckResponse GetCheck() + public async Task GetCheck() { var updChkCookie = Request.Headers.GetCookies("UMB_UPDCHK").FirstOrDefault(); var updateCheckCookie = updChkCookie != null ? updChkCookie["UMB_UPDCHK"].Value : ""; @@ -24,14 +36,11 @@ namespace Umbraco.Web.Editors { try { - var check = new org.umbraco.update.CheckForUpgrade { Timeout = 2000 }; + var version = new SemVersion(UmbracoVersion.Current.Major, UmbracoVersion.Current.Minor, + UmbracoVersion.Current.Build, UmbracoVersion.Comment); + var result = await _upgradeService.CheckUpgrade(version); - var result = check.CheckUpgrade(UmbracoVersion.Current.Major, - UmbracoVersion.Current.Minor, - UmbracoVersion.Current.Build, - UmbracoVersion.Comment); - - return new UpgradeCheckResponse(result.UpgradeType.ToString(), result.Comment, result.UpgradeUrl); + return new UpgradeCheckResponse(result.UpgradeType, result.Comment, result.UpgradeUrl); } catch (System.Net.WebException) { diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index effb46c9b7..981b1f64a0 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -10,8 +10,10 @@ using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Install.Models; @@ -24,16 +26,18 @@ namespace Umbraco.Web.Install private readonly HttpContextBase _httpContext; private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; + private readonly IInstallationService _installationService; private InstallationType? _installationType; public InstallHelper(IUmbracoContextAccessor umbracoContextAccessor, DatabaseBuilder databaseBuilder, - ILogger logger, IGlobalSettings globalSettings) + ILogger logger, IGlobalSettings globalSettings, IInstallationService installationService) { _httpContext = umbracoContextAccessor.UmbracoContext.HttpContext; _logger = logger; _globalSettings = globalSettings; _databaseBuilder = databaseBuilder; + _installationService = installationService; } public InstallationType GetInstallationType() @@ -70,18 +74,13 @@ namespace Umbraco.Web.Install dbProvider = GetDbProviderString(Current.SqlContext); } - var check = new org.umbraco.update.CheckForUpgrade(); - check.Install(installId, - IsBrandNewInstall == false, - isCompleted, - DateTime.Now, - UmbracoVersion.Current.Major, - UmbracoVersion.Current.Minor, - UmbracoVersion.Current.Build, - UmbracoVersion.Comment, - errorMsg, - userAgent, - dbProvider); + var installLog = new InstallLog(installId: installId, isUpgrade: IsBrandNewInstall == false, + installCompleted: isCompleted, timestamp: DateTime.Now, versionMajor: UmbracoVersion.Current.Major, + versionMinor: UmbracoVersion.Current.Minor, versionPatch: UmbracoVersion.Current.Build, + versionComment: UmbracoVersion.Comment, error: errorMsg, userAgent: userAgent, + dbProvider: dbProvider); + + _installationService.Install(installLog); } catch (Exception ex) { From b2ddc4a2b060dfa4d55d5e10b7684dddeb906895 Mon Sep 17 00:00:00 2001 From: elitsa Date: Tue, 18 Feb 2020 14:48:36 +0100 Subject: [PATCH 378/610] Creating the service interfaces and their implementations. Registering them in CoreInitialComposer. These services delegate the work to repositories --- .../Runtime/CoreInitialComposer.cs | 5 ++++ .../Services/IInstallationService.cs | 10 ++++++++ src/Umbraco.Core/Services/IUpgradeService.cs | 11 +++++++++ .../Services/Implement/InstallationService.cs | 21 +++++++++++++++++ src/Umbraco.Core/UpgradeService.cs | 23 +++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 src/Umbraco.Core/Services/IInstallationService.cs create mode 100644 src/Umbraco.Core/Services/IUpgradeService.cs create mode 100644 src/Umbraco.Core/Services/Implement/InstallationService.cs create mode 100644 src/Umbraco.Core/UpgradeService.cs diff --git a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs index d95ada499b..617dbcd9ea 100644 --- a/src/Umbraco.Core/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Core/Runtime/CoreInitialComposer.cs @@ -18,6 +18,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Core.Sync; using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator; @@ -129,6 +130,10 @@ namespace Umbraco.Core.Runtime // by default, register a noop rebuilder composition.RegisterUnique(); + + // will be injected in controllers when needed to invoke rest endpoints on Our + composition.RegisterUnique(); + composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Core/Services/IInstallationService.cs b/src/Umbraco.Core/Services/IInstallationService.cs new file mode 100644 index 0000000000..195169b300 --- /dev/null +++ b/src/Umbraco.Core/Services/IInstallationService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + public interface IInstallationService + { + Task Install(InstallLog installLog); + } +} diff --git a/src/Umbraco.Core/Services/IUpgradeService.cs b/src/Umbraco.Core/Services/IUpgradeService.cs new file mode 100644 index 0000000000..70bbb68085 --- /dev/null +++ b/src/Umbraco.Core/Services/IUpgradeService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Semver; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + public interface IUpgradeService + { + Task CheckUpgrade(SemVersion version); + } +} diff --git a/src/Umbraco.Core/Services/Implement/InstallationService.cs b/src/Umbraco.Core/Services/Implement/InstallationService.cs new file mode 100644 index 0000000000..75bab53cf2 --- /dev/null +++ b/src/Umbraco.Core/Services/Implement/InstallationService.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Core.Services.Implement +{ + public class InstallationService : IInstallationService + { + private readonly IInstallationRepository _installationRepository; + + public InstallationService(IInstallationRepository installationRepository) + { + _installationRepository = installationRepository; + } + + public async Task Install(InstallLog installLog) + { + await _installationRepository.SaveInstall(installLog); + } + } +} diff --git a/src/Umbraco.Core/UpgradeService.cs b/src/Umbraco.Core/UpgradeService.cs new file mode 100644 index 0000000000..d93ed88ad3 --- /dev/null +++ b/src/Umbraco.Core/UpgradeService.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Semver; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Services; + +namespace Umbraco.Core +{ + internal class UpgradeService : IUpgradeService + { + private readonly IUpgradeCheckRepository _upgradeCheckRepository; + + public UpgradeService(IUpgradeCheckRepository upgradeCheckRepository) + { + _upgradeCheckRepository = upgradeCheckRepository; + } + + public async Task CheckUpgrade(SemVersion version) + { + return await _upgradeCheckRepository.CheckUpgrade(version); + } + } +} From 1b3769f5d76116f49ddd16cfd27ceff0ddcebd41 Mon Sep 17 00:00:00 2001 From: elitsa Date: Tue, 18 Feb 2020 14:50:30 +0100 Subject: [PATCH 379/610] Creating models for to serialize and deserialize when calling the rest API --- src/Umbraco.Core/Models/InstallLog.cs | 38 ++++++++++++++++++++++++ src/Umbraco.Core/Models/UpgradeResult.cs | 16 ++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/Umbraco.Core/Models/InstallLog.cs create mode 100644 src/Umbraco.Core/Models/UpgradeResult.cs diff --git a/src/Umbraco.Core/Models/InstallLog.cs b/src/Umbraco.Core/Models/InstallLog.cs new file mode 100644 index 0000000000..d55558fd8b --- /dev/null +++ b/src/Umbraco.Core/Models/InstallLog.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Models +{ + public class InstallLog + { + public Guid InstallId { get; } + public bool IsUpgrade { get; } + public bool InstallCompleted { get; } + public DateTime Timestamp { get; } + public int VersionMajor { get; } + public int VersionMinor { get; } + public int VersionPatch { get; } + public string VersionComment { get; } + public string Error { get; } + public string UserAgent { get; } + public string DbProvider { get; } + + public InstallLog(Guid installId, bool isUpgrade, bool installCompleted, DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider) + { + InstallId = installId; + IsUpgrade = isUpgrade; + InstallCompleted = installCompleted; + Timestamp = timestamp; + VersionMajor = versionMajor; + VersionMinor = versionMinor; + VersionPatch = versionPatch; + VersionComment = versionComment; + Error = error; + UserAgent = userAgent; + DbProvider = dbProvider; + } + } +} diff --git a/src/Umbraco.Core/Models/UpgradeResult.cs b/src/Umbraco.Core/Models/UpgradeResult.cs new file mode 100644 index 0000000000..a27f6bb6a3 --- /dev/null +++ b/src/Umbraco.Core/Models/UpgradeResult.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Models +{ + public class UpgradeResult + { + public string UpgradeType { get; } + public string Comment { get; } + public string UpgradeUrl { get; } + + public UpgradeResult(string upgradeType, string comment, string upgradeUrl) + { + UpgradeType = upgradeType; + Comment = comment; + UpgradeUrl = upgradeUrl; + } + } +} From 79b5b739231cf4408b5320ffda8051064bac7ed8 Mon Sep 17 00:00:00 2001 From: elitsa Date: Tue, 18 Feb 2020 14:53:01 +0100 Subject: [PATCH 380/610] Creating Repositories - abstraction + implementation which use HttpClient and call the endpoints. Registering the Repositories in repositories' composition --- .../CompositionExtensions/Repositories.cs | 2 + .../Repositories/IInstallationRepository.cs | 11 +++++ .../Repositories/IUpgradeCheckRepository.cs | 11 +++++ .../Implement/InstallationRepository.cs | 34 ++++++++++++++++ .../Implement/UpgradeCheckRepository.cs | 40 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 10 +++++ 6 files changed, 108 insertions(+) create mode 100644 src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/Repositories.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Repositories.cs index 23dc9e67c6..00b29dd97f 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/Repositories.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/Repositories.cs @@ -47,6 +47,8 @@ namespace Umbraco.Core.Composing.CompositionExtensions composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(); return composition; } diff --git a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs new file mode 100644 index 0000000000..f786aee319 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IInstallationRepository + { + Task SaveInstall(InstallLog installLog); + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs new file mode 100644 index 0000000000..4575ddaa2c --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Semver; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IUpgradeCheckRepository + { + Task CheckUpgrade(SemVersion version); + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs new file mode 100644 index 0000000000..2ceb389fc4 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + internal class InstallationRepository : IInstallationRepository + { + private static HttpClient _httpClient; + private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; + + + public async Task SaveInstall(InstallLog installLog) + { + try + { + if (_httpClient == null) + _httpClient = new HttpClient(); + + var jsonObj = JsonConvert.SerializeObject(installLog); + var content = new StringContent(jsonObj, Encoding.UTF8, "application/json"); + await _httpClient.PostAsync(RestApiInstallUrl, content); + } + // this occurs if the server for Our is down or cannot be reached + catch (HttpRequestException) + { } + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs new file mode 100644 index 0000000000..048ca23aa4 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs @@ -0,0 +1,40 @@ +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Semver; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + internal class UpgradeCheckRepository : IUpgradeCheckRepository + { + private static HttpClient _httpClient; + private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; + + + public async Task CheckUpgrade(SemVersion version) + { + try + { + if (_httpClient == null) + _httpClient = new HttpClient(); + + var jsonObj = new { VersionMajor = version.Major, VersionMinor = version.Minor, VersionPatch = version.Patch, VersionComment = version.Prerelease }; + var json = JsonConvert.SerializeObject(jsonObj); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); + var jsonResponse = await task.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(jsonResponse); + + return result ?? new UpgradeResult("None", "", ""); + } + catch (HttpRequestException) + { + // this occurs if the server for Our is down or cannot be reached + return new UpgradeResult("None", "", ""); + } + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1fc3709e88..af58075e60 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -131,12 +131,21 @@ + + + + + + + + + @@ -1552,6 +1561,7 @@ + From 3d10a061e9842ba259fef06cc5557966305df998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Tue, 18 Feb 2020 15:05:07 +0100 Subject: [PATCH 381/610] Update CONTRIBUTING.md (#7680) * Update CONTRIBUTING.md Syncs header name with summary and fixes link syntax * Update CONTRIBUTING.md Changed header back and updated summary instead --- .github/CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index cea5859486..0101ac9d16 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -28,7 +28,7 @@ This project and everyone participating in it, is governed by the [our Code of C [Working with the code](#working-with-the-code) * [Building Umbraco from source code](#building-umbraco-from-source-code) * [Working with the source code](#working-with-the-source-code) - * [Making changes after the PR was opened](#making-changes-after-the-pr-was-opened) + * [Making changes after the PR is open](#making-changes-after-the-pr-is-open) * [Which branch should I target for my contributions?](#which-branch-should-i-target-for-my-contributions) * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) @@ -65,7 +65,7 @@ Great question! The short version goes like this: * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first. * **Push** - great, now you can push the changes up to your fork on GitHub - * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. + * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here](https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. ![Create a pull request](img/createpullrequest.png) From a058703004c748997f56e027b299228938ebde33 Mon Sep 17 00:00:00 2001 From: elitsa Date: Tue, 18 Feb 2020 16:28:07 +0100 Subject: [PATCH 382/610] Making call async and adding setters for InstallLog --- src/Umbraco.Core/Models/InstallLog.cs | 8 ++++---- src/Umbraco.Web/Install/InstallHelper.cs | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Models/InstallLog.cs b/src/Umbraco.Core/Models/InstallLog.cs index d55558fd8b..cb14ebd650 100644 --- a/src/Umbraco.Core/Models/InstallLog.cs +++ b/src/Umbraco.Core/Models/InstallLog.cs @@ -9,16 +9,16 @@ namespace Umbraco.Core.Models public class InstallLog { public Guid InstallId { get; } - public bool IsUpgrade { get; } - public bool InstallCompleted { get; } - public DateTime Timestamp { get; } + public bool IsUpgrade { get; set; } + public bool InstallCompleted { get; set; } + public DateTime Timestamp { get; set; } public int VersionMajor { get; } public int VersionMinor { get; } public int VersionPatch { get; } public string VersionComment { get; } public string Error { get; } public string UserAgent { get; } - public string DbProvider { get; } + public string DbProvider { get; set; } public InstallLog(Guid installId, bool isUpgrade, bool installCompleted, DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider) { diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index 981b1f64a0..b609eb7a29 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -4,6 +4,7 @@ using System.Configuration; using System.IO; using System.Linq; using System.Net.Http; +using System.Threading.Tasks; using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -45,7 +46,7 @@ namespace Umbraco.Web.Install return _installationType ?? (_installationType = IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade).Value; } - internal void InstallStatus(bool isCompleted, string errorMsg) + internal async Task InstallStatus(bool isCompleted, string errorMsg) { try { @@ -80,7 +81,7 @@ namespace Umbraco.Web.Install versionComment: UmbracoVersion.Comment, error: errorMsg, userAgent: userAgent, dbProvider: dbProvider); - _installationService.Install(installLog); + await _installationService.Install(installLog); } catch (Exception ex) { From 653616912aaefa2819bdfacea2a550c47c323b24 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 19 Feb 2020 13:43:48 +1100 Subject: [PATCH 383/610] Fixes issue when a user's start node is below a list view and when there's list views below that --- .../IContentTypeRepositoryBase.cs | 7 ++++ .../Implement/ContentTypeRepositoryBase.cs | 14 ++++--- .../Services/IContentTypeServiceBase.cs | 7 ++++ ...peServiceBaseOfTRepositoryTItemTService.cs | 9 ++++ .../Models/Mapping/ContentMapDefinition.cs | 42 +++++++++++++++---- 5 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 254e04d2d5..4020244733 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -27,6 +27,13 @@ namespace Umbraco.Core.Persistence.Repositories /// bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + /// /// Returns true or false depending on whether content nodes have been created based on the provided content type id. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 6f714ff187..357798a8a9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1309,14 +1309,16 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", return test; } - /// - /// Given the path of a content item, this will return true if the content item exists underneath a list view content item - /// - /// - /// + /// public bool HasContainerInPath(string contentPath) { - var ids = contentPath.Split(',').Select(int.Parse); + var ids = contentPath.Split(',').Select(int.Parse).ToArray(); + return HasContainerInPath(ids); + } + + /// + public bool HasContainerInPath(params int[] ids) + { var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType INNER JOIN {Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Constants.DatabaseSchema.Tables.Content}.contentTypeId WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", new { ids, isContainer = true }); diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index 6ed3c85e91..82e5c6f171 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -69,6 +69,13 @@ namespace Umbraco.Core.Services /// bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + Attempt> CreateContainer(int parentContainerId, string name, int userId = Constants.Security.SuperUserId); Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId); EntityContainer GetContainer(int containerId); diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index da532e2765..fdd2d9ceae 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -321,6 +321,15 @@ namespace Umbraco.Core.Services.Implement } } + public bool HasContainerInPath(params int[] ids) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // can use same repo for both content and media + return Repository.HasContainerInPath(ids); + } + } + public IEnumerable GetDescendants(int id, bool andSelf) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index be84b13a7e..d6def081e8 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -28,6 +28,7 @@ namespace Umbraco.Web.Models.Mapping private readonly ILocalizationService _localizationService; private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IEntityService _entityService; private readonly TabsAndPropertiesMapper _tabsAndPropertiesMapper; private readonly ContentSavedStateMapper _stateMapper; private readonly ContentBasicSavedStateMapper _basicStateMapper; @@ -35,7 +36,7 @@ namespace Umbraco.Web.Models.Mapping public ContentMapDefinition(CommonMapper commonMapper, ILocalizedTextService localizedTextService, IContentService contentService, IContentTypeService contentTypeService, IFileService fileService, IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, ILocalizationService localizationService, ILogger logger, - IUserService userService) + IUserService userService, IEntityService entityService) { _commonMapper = commonMapper; _localizedTextService = localizedTextService; @@ -47,7 +48,7 @@ namespace Umbraco.Web.Models.Mapping _localizationService = localizationService; _logger = logger; _userService = userService; - + _entityService = entityService; _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); @@ -229,19 +230,46 @@ namespace Umbraco.Web.Models.Mapping /// private bool DetermineIsChildOfListView(IContent source, MapperContext context) { + var userStartNodes = Array.Empty(); + // In cases where a user's start node is below a list view, we will actually render // out the tree to that start node and in that case for that start node, we want to return // false here. if (context.HasItems && context.Items.TryGetValue("CurrentUser", out var usr) && usr is IUser currentUser) { - if (currentUser.StartContentIds.Contains(source.Id)) - return false; + userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService); + if (!userStartNodes.Contains(Constants.System.Root)) + { + // return false if this is the user's actual start node, the node will be rendered in the tree + // regardless of if it's a list view or not + if (userStartNodes.Contains(source.Id)) + return false; + } } - - // map the IsChildOfListView (this is actually if it is a descendant of a list view!) var parent = _contentService.GetParent(source); - return parent != null && (parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(parent.Path)); + + if (parent == null) + return false; + + var pathParts = parent.Path.Split(',').Select(x => int.TryParse(x, out var i) ? i : 0).ToList(); + + // reduce the path parts so we exclude top level content items that + // are higher up than a user's start nodes + foreach (var n in userStartNodes) + { + var index = pathParts.IndexOf(n); + if (index != -1) + { + // now trim all top level start nodes to the found index + for (var i = 0; i < index; i++) + { + pathParts.RemoveAt(0); + } + } + } + + return parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(pathParts.ToArray()); } private DateTime? GetScheduledDate(IContent source, ContentScheduleAction action, MapperContext context) From 9ed94f753d8d531881064f441c87ba643aa6604f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 19 Feb 2020 08:32:47 +0100 Subject: [PATCH 384/610] Fixed issue with wrong installId in cookie. + Refactored to not use Newtonsoft.Json + Minor renames --- .../Repositories/IInstallationRepository.cs | 2 +- .../Repositories/IUpgradeCheckRepository.cs | 2 +- .../Implement/InstallationRepository.cs | 14 +- .../Implement/UpgradeCheckRepository.cs | 29 +- .../Services/IInstallationService.cs | 2 +- .../Services/Implement/InstallationService.cs | 4 +- src/Umbraco.Core/UpgradeService.cs | 2 +- src/Umbraco.Web.UI.Client/package-lock.json | 422 +++++++----------- src/Umbraco.Web/Install/InstallHelper.cs | 8 +- 9 files changed, 197 insertions(+), 288 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs index f786aee319..fbc7d2cfbc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IInstallationRepository.cs @@ -6,6 +6,6 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IInstallationRepository { - Task SaveInstall(InstallLog installLog); + Task SaveInstallLogAsync(InstallLog installLog); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs index 4575ddaa2c..6d56994781 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUpgradeCheckRepository.cs @@ -6,6 +6,6 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IUpgradeCheckRepository { - Task CheckUpgrade(SemVersion version); + Task CheckUpgradeAsync(SemVersion version); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs index 2ceb389fc4..49fbfe096b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/InstallationRepository.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; +using System.Net.Http; +using System.Net.Http.Formatting; using System.Threading.Tasks; -using Newtonsoft.Json; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories.Implement @@ -15,16 +11,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private const string RestApiInstallUrl = "https://our.umbraco.com/umbraco/api/Installation/Install"; - public async Task SaveInstall(InstallLog installLog) + public async Task SaveInstallLogAsync(InstallLog installLog) { try { if (_httpClient == null) _httpClient = new HttpClient(); - var jsonObj = JsonConvert.SerializeObject(installLog); - var content = new StringContent(jsonObj, Encoding.UTF8, "application/json"); - await _httpClient.PostAsync(RestApiInstallUrl, content); + await _httpClient.PostAsync(RestApiInstallUrl, installLog, new JsonMediaTypeFormatter()); } // this occurs if the server for Our is down or cannot be reached catch (HttpRequestException) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs index 048ca23aa4..365e8ba481 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs @@ -1,7 +1,6 @@ using System.Net.Http; -using System.Text; +using System.Net.Http.Formatting; using System.Threading.Tasks; -using Newtonsoft.Json; using Semver; using Umbraco.Core.Models; @@ -13,20 +12,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private const string RestApiUpgradeChecklUrl = "https://our.umbraco.com/umbraco/api/UpgradeCheck/CheckUpgrade"; - public async Task CheckUpgrade(SemVersion version) + public async Task CheckUpgradeAsync(SemVersion version) { try { if (_httpClient == null) _httpClient = new HttpClient(); - var jsonObj = new { VersionMajor = version.Major, VersionMinor = version.Minor, VersionPatch = version.Patch, VersionComment = version.Prerelease }; - var json = JsonConvert.SerializeObject(jsonObj); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content); - var jsonResponse = await task.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(jsonResponse); + var task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, new CheckUpgradeDto(version), new JsonMediaTypeFormatter()); + var result = await task.Content.ReadAsAsync(); return result ?? new UpgradeResult("None", "", ""); } @@ -36,5 +30,20 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return new UpgradeResult("None", "", ""); } } + private class CheckUpgradeDto + { + public CheckUpgradeDto(SemVersion version) + { + VersionMajor = version.Major; + VersionMinor = version.Minor; + VersionPatch = version.Patch; + VersionComment = version.Prerelease; + } + + public int VersionMajor { get; } + public int VersionMinor { get; } + public int VersionPatch { get; } + public string VersionComment { get; } + } } } diff --git a/src/Umbraco.Core/Services/IInstallationService.cs b/src/Umbraco.Core/Services/IInstallationService.cs index 195169b300..334088f8ae 100644 --- a/src/Umbraco.Core/Services/IInstallationService.cs +++ b/src/Umbraco.Core/Services/IInstallationService.cs @@ -5,6 +5,6 @@ namespace Umbraco.Core.Services { public interface IInstallationService { - Task Install(InstallLog installLog); + Task LogInstall(InstallLog installLog); } } diff --git a/src/Umbraco.Core/Services/Implement/InstallationService.cs b/src/Umbraco.Core/Services/Implement/InstallationService.cs index 75bab53cf2..a1f74e0862 100644 --- a/src/Umbraco.Core/Services/Implement/InstallationService.cs +++ b/src/Umbraco.Core/Services/Implement/InstallationService.cs @@ -13,9 +13,9 @@ namespace Umbraco.Core.Services.Implement _installationRepository = installationRepository; } - public async Task Install(InstallLog installLog) + public async Task LogInstall(InstallLog installLog) { - await _installationRepository.SaveInstall(installLog); + await _installationRepository.SaveInstallLogAsync(installLog); } } } diff --git a/src/Umbraco.Core/UpgradeService.cs b/src/Umbraco.Core/UpgradeService.cs index d93ed88ad3..8116c0e738 100644 --- a/src/Umbraco.Core/UpgradeService.cs +++ b/src/Umbraco.Core/UpgradeService.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core public async Task CheckUpgrade(SemVersion version) { - return await _upgradeCheckRepository.CheckUpgrade(version); + return await _upgradeCheckRepository.CheckUpgradeAsync(version); } } } diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 68d0bda427..42a89c5d13 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1198,7 +1198,7 @@ }, "ansi-colors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "requires": { @@ -1279,7 +1279,7 @@ "anymatch": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=", "dev": true, "requires": { "micromatch": "^2.1.5", @@ -1427,7 +1427,7 @@ "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", "dev": true, "requires": { "sprintf-js": "~1.0.2" @@ -1451,7 +1451,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", "dev": true }, "arr-map": { @@ -1526,7 +1526,7 @@ "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "integrity": "sha1-42jqFfibxwaff/uJrsOmx9SsItQ=", "dev": true }, "array-sort": { @@ -1569,7 +1569,7 @@ "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=", "dev": true }, "asap": { @@ -1744,7 +1744,7 @@ "base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", "dev": true, "requires": { "cache-base": "^1.0.1", @@ -1768,7 +1768,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -1777,7 +1777,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -1786,7 +1786,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -1806,8 +1806,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true, - "optional": true + "dev": true }, "base64id": { "version": "1.0.0", @@ -2054,7 +2053,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2093,10 +2091,9 @@ }, "bl": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, - "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2106,15 +2103,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2127,10 +2122,9 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2213,7 +2207,7 @@ "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -2223,7 +2217,7 @@ "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", "dev": true, "requires": { "arr-flatten": "^1.1.0", @@ -2265,7 +2259,6 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "dev": true, - "optional": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -2274,7 +2267,7 @@ "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=", "dev": true, "requires": { "buffer-alloc-unsafe": "^1.1.0", @@ -2284,15 +2277,14 @@ "buffer-alloc-unsafe": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA=", "dev": true }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true, - "optional": true + "dev": true }, "buffer-equal": { "version": "1.0.0", @@ -2330,7 +2322,7 @@ "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", "dev": true, "requires": { "collection-visit": "^1.0.0", @@ -2424,7 +2416,7 @@ }, "callsites": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, @@ -2483,7 +2475,6 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", "dev": true, - "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -2605,7 +2596,7 @@ "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", "dev": true, "requires": { "arr-union": "^3.1.0", @@ -2739,7 +2730,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -2754,7 +2745,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -2851,7 +2842,7 @@ "color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "integrity": "sha1-k4NDeaHMmgxh+C9S8NBDIiUb1aI=", "dev": true }, "colornames": { @@ -2917,7 +2908,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, - "optional": true, "requires": { "graceful-readlink": ">= 1.0.0" } @@ -2966,7 +2956,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -2981,7 +2971,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -2993,7 +2983,7 @@ "concat-with-sourcemaps": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", - "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "integrity": "sha1-1OqT8FriV5CVG5nns7CeOQikCC4=", "dev": true, "requires": { "source-map": "^0.6.1" @@ -3002,7 +2992,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true } } @@ -3012,7 +3002,6 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "dev": true, - "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3068,7 +3057,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3076,7 +3064,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", "dev": true }, "convert-source-map": { @@ -3472,7 +3460,6 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3489,7 +3476,6 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, - "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3498,8 +3484,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } } @@ -3510,7 +3495,6 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, - "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3520,7 +3504,6 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, - "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3531,8 +3514,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3541,7 +3523,6 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3554,8 +3535,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true, - "optional": true + "dev": true } } }, @@ -3564,7 +3544,6 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3575,8 +3554,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3585,7 +3563,6 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, - "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3597,15 +3574,13 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, - "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3615,8 +3590,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3661,7 +3635,7 @@ "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", "dev": true, "requires": { "is-descriptor": "^1.0.2", @@ -3671,7 +3645,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -3680,7 +3654,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -3689,7 +3663,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -3881,8 +3855,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3899,8 +3872,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true, - "optional": true + "dev": true }, "duplexify": { "version": "3.7.1", @@ -3922,7 +3894,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -3937,7 +3909,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -4042,7 +4014,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -4058,7 +4030,7 @@ }, "engine.io-client": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "dev": true, "requires": { @@ -4084,7 +4056,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -4132,7 +4104,7 @@ "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "integrity": "sha1-RoTXF3mtOa8Xfj8AeZb3xnyFJhg=", "dev": true, "optional": true, "requires": { @@ -4414,7 +4386,7 @@ "esquery": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "integrity": "sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=", "dev": true, "requires": { "estraverse": "^4.0.0" @@ -4423,7 +4395,7 @@ "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", "dev": true, "requires": { "estraverse": "^4.1.0" @@ -4478,7 +4450,7 @@ "exec-buffer": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz", - "integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==", + "integrity": "sha1-sWhtvZBMfPmC5lLB9aebHlVzCCs=", "dev": true, "optional": true, "requires": { @@ -4503,7 +4475,6 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, - "optional": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -4519,7 +4490,6 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, - "optional": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -4659,7 +4629,6 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, - "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4669,7 +4638,6 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, - "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -4694,7 +4662,7 @@ "is-extendable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", "dev": true, "requires": { "is-plain-object": "^2.0.4" @@ -4716,7 +4684,7 @@ "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", "dev": true, "requires": { "array-unique": "^0.3.2", @@ -4750,7 +4718,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -4759,7 +4727,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -4768,7 +4736,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -4951,7 +4919,6 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, - "optional": true, "requires": { "pend": "~1.2.0" } @@ -4990,15 +4957,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true, - "optional": true + "dev": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", "dev": true, - "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5347,9 +5312,8 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "optional": true + "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", + "dev": true }, "fs-extra": { "version": "1.0.0", @@ -5415,8 +5379,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5437,14 +5400,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5459,20 +5420,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5589,8 +5547,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5602,7 +5559,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5617,7 +5573,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5625,14 +5580,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5651,7 +5604,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5732,8 +5684,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5745,7 +5696,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5831,8 +5781,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5868,7 +5817,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5888,7 +5836,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5932,21 +5879,19 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", "dev": true }, "functional-red-black-tree": { @@ -5966,7 +5911,6 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", "dev": true, - "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -5975,15 +5919,13 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true + "dev": true }, "get-value": { "version": "2.0.6", @@ -6191,7 +6133,7 @@ "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "integrity": "sha1-bXcPDrUjrHgWTXK15xqIdyZcw+o=", "dev": true, "requires": { "global-prefix": "^1.0.1", @@ -6298,8 +6240,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true, - "optional": true + "dev": true }, "growly": { "version": "1.3.0", @@ -6606,7 +6547,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", "dev": true }, @@ -6818,7 +6759,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -6894,7 +6835,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -6915,7 +6856,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -7079,7 +7020,7 @@ "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=", "dev": true, "requires": { "isarray": "2.0.1" @@ -7118,8 +7059,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true, - "optional": true + "dev": true }, "has-symbols": { "version": "1.0.0", @@ -7132,7 +7072,6 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", "dev": true, - "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7333,8 +7272,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true, - "optional": true + "dev": true }, "ignore": { "version": "4.0.6", @@ -7464,7 +7402,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, - "optional": true, "requires": { "repeating": "^2.0.0" } @@ -7506,7 +7443,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=", "dev": true }, "inquirer": { @@ -7629,7 +7566,7 @@ "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "integrity": "sha1-OV4a6EsR8mrReV5zwXN45IowFXY=", "dev": true, "requires": { "is-relative": "^1.0.0", @@ -7680,13 +7617,13 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", "dev": true }, "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "integrity": "sha1-HhrfIZ4e62hNaR+dagX/DTCiTXU=", "dev": true }, "is-color-stop": { @@ -7732,7 +7669,7 @@ "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", @@ -7743,7 +7680,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", "dev": true } } @@ -7786,7 +7723,6 @@ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7839,8 +7775,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true, - "optional": true + "dev": true }, "is-negated-glob": { "version": "1.0.0", @@ -7878,20 +7813,18 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true, - "optional": true + "dev": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "optional": true + "dev": true }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "dev": true, "requires": { "isobject": "^3.0.1" @@ -7934,7 +7867,7 @@ "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "integrity": "sha1-obtpNc6MXboei5dUubLcwCDiJg0=", "dev": true, "requires": { "is-unc-path": "^1.0.0" @@ -7943,15 +7876,14 @@ "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "integrity": "sha1-+xj4fOH+uSUWnJpAfBkxijIG7Yg=", "dev": true }, "is-retry-allowed": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true, - "optional": true + "dev": true }, "is-stream": { "version": "1.1.0", @@ -7986,7 +7918,7 @@ "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "integrity": "sha1-1zHoiY7QkKEsNSrS6u1Qla0yLJ0=", "dev": true, "requires": { "unc-path-regex": "^0.1.2" @@ -8007,7 +7939,7 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", "dev": true }, "is-wsl": { @@ -8054,7 +7986,6 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, - "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -8149,7 +8080,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -8434,7 +8365,7 @@ "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", "dev": true }, "klaw": { @@ -8502,7 +8433,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -8517,7 +8448,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -8871,9 +8802,8 @@ "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true, - "optional": true + "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", + "dev": true }, "lpad-align": { "version": "1.1.2", @@ -8927,7 +8857,7 @@ "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "integrity": "sha1-KbM/MSqo9UfEpeSQ9Wr87JkTOtY=", "dev": true, "requires": { "kind-of": "^6.0.2" @@ -8943,8 +8873,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -9050,7 +8979,7 @@ "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", "dev": true, "requires": { "arr-diff": "^4.0.0", @@ -9093,15 +9022,14 @@ "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=", "dev": true }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "optional": true + "dev": true }, "minimatch": { "version": "3.0.4", @@ -9114,7 +9042,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -9165,7 +9093,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true } @@ -9243,7 +9171,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -12403,7 +12331,6 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", "dev": true, - "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12413,8 +12340,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -12423,7 +12349,6 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, - "optional": true, "requires": { "path-key": "^2.0.0" } @@ -12654,7 +12579,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, @@ -12745,7 +12670,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -12779,8 +12704,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "optional": true + "dev": true }, "p-is-promise": { "version": "1.1.0", @@ -12817,7 +12741,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13049,7 +12972,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -13505,7 +13428,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -13530,7 +13453,7 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", "dev": true, "optional": true, "requires": { @@ -13541,8 +13464,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true + "dev": true }, "prr": { "version": "1.0.1", @@ -13599,7 +13521,7 @@ "qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "integrity": "sha1-xF6cYYAL0IfviNfiVkI73Unl0HE=", "dev": true }, "qs": { @@ -13724,7 +13646,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -13739,7 +13661,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -13795,7 +13717,7 @@ "regex-cache": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=", "dev": true, "requires": { "is-equal-shallow": "^0.1.3" @@ -13804,7 +13726,7 @@ "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", "dev": true, "requires": { "extend-shallow": "^3.0.2", @@ -13898,7 +13820,6 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, - "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14049,7 +13970,7 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", "dev": true }, "reusify": { @@ -14112,7 +14033,7 @@ "run-sequence": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-2.2.1.tgz", - "integrity": "sha512-qkzZnQWMZjcKbh3CNly2srtrkaO/2H/SI5f2eliMCapdRD3UhMrwjfOAZJAnZ2H8Ju4aBzFZkBGXUqFs9V0yxw==", + "integrity": "sha1-HOZD2jb9jH6n4akynaM/wriJhJU=", "dev": true, "requires": { "chalk": "^1.1.3", @@ -14150,7 +14071,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -14172,7 +14093,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", "dev": true }, @@ -14209,7 +14130,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", "dev": true }, "safe-regex": { @@ -14224,13 +14145,13 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", "dev": true }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=", "dev": true }, "seek-bzip": { @@ -14238,7 +14159,6 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", "dev": true, - "optional": true, "requires": { "commander": "~2.8.1" } @@ -14395,7 +14315,7 @@ "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", "dev": true, "requires": { "base": "^0.11.1", @@ -14446,7 +14366,7 @@ "snapdragon-node": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", "dev": true, "requires": { "define-property": "^1.0.0", @@ -14466,7 +14386,7 @@ "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -14475,7 +14395,7 @@ "is-data-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", "dev": true, "requires": { "kind-of": "^6.0.0" @@ -14484,7 +14404,7 @@ "is-descriptor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", @@ -14497,7 +14417,7 @@ "snapdragon-util": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", "dev": true, "requires": { "kind-of": "^3.2.0" @@ -14598,7 +14518,7 @@ }, "socket.io-parser": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "dev": true, "requires": { @@ -14616,7 +14536,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -14641,7 +14561,6 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, - "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -14651,7 +14570,6 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, - "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -14706,7 +14624,7 @@ "spdx-expression-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -14727,7 +14645,7 @@ "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", "dev": true, "requires": { "extend-shallow": "^3.0.0" @@ -14760,7 +14678,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "optional": true, @@ -14801,7 +14719,7 @@ "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "integrity": "sha1-g26zyDgv4pNv6vVEYxAXzn1Ho88=", "dev": true }, "stack-trace": { @@ -14976,7 +14894,6 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, - "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -14985,8 +14902,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "optional": true + "dev": true }, "strip-indent": { "version": "1.0.1", @@ -15007,9 +14923,8 @@ "strip-outer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15129,7 +15044,6 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, - "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15144,15 +15058,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15168,7 +15080,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15179,15 +15090,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true, - "optional": true + "dev": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, - "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15235,7 +15144,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -15250,7 +15159,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -15288,8 +15197,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "optional": true + "dev": true }, "timers-ext": { "version": "0.1.7", @@ -15320,7 +15228,7 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", "dev": true, "requires": { "os-tmpdir": "~1.0.2" @@ -15345,9 +15253,8 @@ "to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true, - "optional": true + "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", + "dev": true }, "to-fast-properties": { "version": "2.0.0", @@ -15378,7 +15285,7 @@ "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", "dev": true, "requires": { "define-property": "^2.0.2", @@ -15442,7 +15349,6 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15464,7 +15370,6 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -15572,7 +15477,7 @@ "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", "dev": true }, "unbzip2-stream": { @@ -15580,7 +15485,6 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", "dev": true, - "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -15755,7 +15659,7 @@ "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", "dev": true, "requires": { "punycode": "^2.1.0" @@ -15781,8 +15685,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true, - "optional": true + "dev": true }, "use": { "version": "3.1.1", @@ -15809,7 +15712,7 @@ "util.promisify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "integrity": "sha1-RA9xZaRZyaFtwUXrjnLzVocJcDA=", "dev": true, "requires": { "define-properties": "^1.1.2", @@ -16102,7 +16005,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -16128,7 +16031,7 @@ "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", "dev": true, "requires": { "async-limiter": "~1.0.0", @@ -16201,7 +16104,6 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, - "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index b609eb7a29..b74a20c27c 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -64,8 +64,12 @@ namespace Umbraco.Web.Install if (installId == Guid.Empty) installId = Guid.NewGuid(); } + else + { + installId = Guid.NewGuid(); // Guid.TryParse will have reset installId to Guid.Empty + } } - _httpContext.Response.Cookies.Set(new HttpCookie(Constants.Web.InstallerCookieName, "1")); + _httpContext.Response.Cookies.Set(new HttpCookie(Constants.Web.InstallerCookieName, installId.ToString())); var dbProvider = string.Empty; if (IsBrandNewInstall == false) @@ -81,7 +85,7 @@ namespace Umbraco.Web.Install versionComment: UmbracoVersion.Comment, error: errorMsg, userAgent: userAgent, dbProvider: dbProvider); - await _installationService.Install(installLog); + await _installationService.LogInstall(installLog); } catch (Exception ex) { From 2cd50822ea252d498c93a94fa134c9e0c800a105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 09:35:36 +0100 Subject: [PATCH 385/610] adjustments of Save and publish dialog --- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../umb-variant-selector-overlay.less | 19 +++++++++++ .../src/less/components/umb-list.less | 9 +++-- .../content/overlays/publish.controller.js | 12 +++---- .../src/views/content/overlays/publish.html | 33 +++++++++---------- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 10 +++++- .../Umbraco/config/lang/en_us.xml | 8 +++++ 7 files changed, 65 insertions(+), 27 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index f6fadc9a43..174f9f41d7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -138,6 +138,7 @@ @import "components/tooltip/umb-tooltip-list.less"; @import "components/overlays/umb-overlay-backdrop.less"; @import "components/overlays/umb-itempicker.less"; +@import "components/overlays/umb-variant-selector-overlay"; @import "components/umb-grid.less"; @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less new file mode 100644 index 0000000000..2b38d31633 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less @@ -0,0 +1,19 @@ +.umb-variant-selector-overlay { + + + .umb-variant-selector-entry { + .umb_check_info { + margin-top: 2px; + } + } + .umb-variant-selector-entry__title { + + } + .umb-variant-selector-entry__description { + display: block; + font-size: 13px; + color: @gray-4; + } + + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less index 94cfa6f62c..44955e8f8e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less @@ -1,15 +1,18 @@ .umb-list--condensed { .umb-list-item { - padding-top: 5px; - padding-bottom: 5px; + padding-top: 7px; + padding-bottom: 7px; } } .umb-list-item { - border-bottom: 1px solid @gray-9; + border-bottom: 1px solid @gray-11; padding-top: 15px; padding-bottom: 15px; display: flex; + &:last-of-type { + border-bottom: none; + } } a.umb-list-item:hover, diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index de4d4847fb..d1b17021b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -11,6 +11,7 @@ vm.changeSelection = changeSelection; vm.dirtyVariantFilter = dirtyVariantFilter; vm.pristineVariantFilter = pristineVariantFilter; + vm.notPublishedMandatoryFilter = notPublishedMandatoryFilter; /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { @@ -63,12 +64,10 @@ function dirtyVariantFilter(variant) { //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's - // * the active one // * it's editor is in a $dirty state // * it has pending saves // * it is unpublished - // * it is in NotCreated state - return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges" || variant.state === "NotCreated"); + return (variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); } function hasAnyData(variant) { @@ -100,6 +99,9 @@ function pristineVariantFilter(variant) { return !(dirtyVariantFilter(variant)); } + function notPublishedMandatoryFilter(variant) { + return !dirtyVariantFilter(variant) && variant.isMandatory === true; + } function onInit() { @@ -184,9 +186,7 @@ $scope.model.disableSubmitButton = true; } - var labelKey = vm.isNew ? "content_languagesToPublishForFirstTime" : "content_languagesToPublish"; - - localizationService.localize(labelKey).then(function (value) { + localizationService.localize("content_variantsToPublish").then(function (value) { vm.headline = value; vm.loading = false; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index e9e179c64f..619efa6d40 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -1,4 +1,4 @@ -
      +

      {{vm.headline}}

      @@ -13,7 +13,7 @@
      -
      +
      -
      - - - - - - + > + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + - - {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} - + - -
      @@ -45,10 +44,10 @@
      -

      +

      -
      +
      diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 7d174e73fc..00a4a3af05 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -302,6 +302,14 @@ Unpublished Languages Unmodified Languages These languages haven't been created + + Which variants you would like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + Unpublished mandatory variants + Ready to Publish? Ready to Save? Send for approval @@ -1854,7 +1862,7 @@ To manage your website, simply open the Umbraco back office and start adding con Reset password Your password has been changed! Password changed - Please confirm the new password + Please confirm the new password Enter your new password Your new password cannot be blank! Current password 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 fd3d6ac70c..2bbfc22c70 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -306,6 +306,14 @@ Unpublished Languages Unmodified Languages These languages haven't been created + + Which variants you would like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + Unpublished mandatory variants + Ready to Publish? Ready to Save? Send for approval From 088c51e84d88b2f1229e5d1826f18641072873ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 09:36:33 +0100 Subject: [PATCH 386/610] enabling transclude for umb-form-check --- .../components/forms/umbcheckbox.directive.js | 1 + .../forms/umbradiobutton.directive.js | 1 + .../src/less/components/umb-form-check.less | 17 +++++---- .../views/components/forms/umb-checkbox.html | 38 +++++++++++-------- .../components/forms/umb-radiobutton.html | 30 ++++++++------- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js index d562b21d52..9a9d6d4a76 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js @@ -71,6 +71,7 @@ templateUrl: 'views/components/forms/umb-checkbox.html', controller: UmbCheckboxController, controllerAs: 'vm', + transclude: true, bindings: { model: "=", inputId: "@", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js index 7ed84547f1..d79140f947 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js @@ -69,6 +69,7 @@ templateUrl: 'views/components/forms/umb-radiobutton.html', controller: UmbRadiobuttonController, controllerAs: 'vm', + transclude: true, bindings: { model: "=", inputId: "@", diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index 76a4df0056..3763dfe869 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -6,12 +6,19 @@ flex-wrap: wrap; align-items: center; position: relative; - padding: 0 0 0 26px !important; + padding: 0 0 0 0px !important; margin: 0; min-height: 22px; - line-height: 22px; cursor: pointer !important; + .umb-form-check__symbol { + margin-right: 10px; + } + .umb-form-check__info { + + } + + &.-small-text{ font-size: 13px; } @@ -22,7 +29,6 @@ &__text { position: relative; - top: 1px; user-select: none; } @@ -90,10 +96,6 @@ &__state { display: flex; height: 18px; - margin-top: 2px; - position: absolute; - top: 0; - left: 0; } &__check { @@ -101,6 +103,7 @@ position: relative; background: @white; border: 1px solid @inputBorder; + border-radius: @baseBorderRadius; width: @checkboxWidth; height: @checkboxHeight; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html index 82b21e4c3b..ba5adf199a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html @@ -1,22 +1,28 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html index 9ee50bcae1..a34d548ea6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html @@ -1,16 +1,20 @@ From 278f8cc99f683288fb7f676827184e594b06ef43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 10:13:51 +0100 Subject: [PATCH 387/610] css change --- .../src/less/components/umb-form-check.less | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index 3763dfe869..b9efd8f7d8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -3,15 +3,14 @@ .umb-form-check { display: flex; - flex-wrap: wrap; - align-items: center; position: relative; - padding: 0 0 0 0px !important; + padding-left: 0px; margin: 0; min-height: 22px; cursor: pointer !important; .umb-form-check__symbol { + margin-top: 1px; margin-right: 10px; } .umb-form-check__info { From b808443558a768c7bdf23e8d53f74108388ddab9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 19 Feb 2020 12:34:21 +0100 Subject: [PATCH 388/610] Removed old soap reference from the project --- src/Umbraco.Web/Umbraco.Web.csproj | 21 -- .../org.umbraco.update/Reference.cs | 286 ------------------ .../org.umbraco.update/Reference.map | 7 - .../UpgradeResult.datasource | 0 .../UpgradeResult1.datasource | 10 - .../org.umbraco.update/checkforupgrade.disco | 6 - .../org.umbraco.update/checkforupgrade.wsdl | 142 --------- 7 files changed, 472 deletions(-) delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.update/Reference.map delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult.datasource delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult1.datasource delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.disco delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.wsdl diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0111a36993..e6a50aa066 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1187,11 +1187,6 @@ - - True - True - Reference.map - @@ -1215,9 +1210,6 @@ - - - @@ -1225,22 +1217,9 @@ - - - - MSDiscoCodeGenerator - Reference.cs - Designer - Mvc\web.config - - Reference.map - - - Reference.map - SettingsSingleFileGenerator Settings.Designer.cs diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs deleted file mode 100644 index 230594fcbd..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.cs +++ /dev/null @@ -1,286 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -// -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. -// -#pragma warning disable 1591 - -namespace Umbraco.Web.org.umbraco.update { - using System; - using System.Web.Services; - using System.Diagnostics; - using System.Web.Services.Protocols; - using System.Xml.Serialization; - using System.ComponentModel; - - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name="CheckForUpgradeSoap", Namespace="http://update.umbraco.org/")] - public partial class CheckForUpgrade : System.Web.Services.Protocols.SoapHttpClientProtocol { - - private System.Threading.SendOrPostCallback InstallOperationCompleted; - - private System.Threading.SendOrPostCallback CheckUpgradeOperationCompleted; - - private bool useDefaultCredentialsSetExplicitly; - - /// - public CheckForUpgrade() { - this.Url = "http://update.umbraco.org/checkforupgrade.asmx"; - if ((this.IsLocalFileSystemWebService(this.Url) == true)) { - this.UseDefaultCredentials = true; - this.useDefaultCredentialsSetExplicitly = false; - } - else { - this.useDefaultCredentialsSetExplicitly = true; - } - } - - public new string Url { - get { - return base.Url; - } - set { - if ((((this.IsLocalFileSystemWebService(base.Url) == true) - && (this.useDefaultCredentialsSetExplicitly == false)) - && (this.IsLocalFileSystemWebService(value) == false))) { - base.UseDefaultCredentials = false; - } - base.Url = value; - } - } - - public new bool UseDefaultCredentials { - get { - return base.UseDefaultCredentials; - } - set { - base.UseDefaultCredentials = value; - this.useDefaultCredentialsSetExplicitly = true; - } - } - - /// - public event InstallCompletedEventHandler InstallCompleted; - - /// - public event CheckUpgradeCompletedEventHandler CheckUpgradeCompleted; - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://update.umbraco.org/Install", RequestNamespace="http://update.umbraco.org/", ResponseNamespace="http://update.umbraco.org/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public void Install(System.Guid installId, bool isUpgrade, bool installCompleted, System.DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider) { - this.Invoke("Install", new object[] { - installId, - isUpgrade, - installCompleted, - timestamp, - versionMajor, - versionMinor, - versionPatch, - versionComment, - error, - userAgent, - dbProvider}); - } - - /// - public void InstallAsync(System.Guid installId, bool isUpgrade, bool installCompleted, System.DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider) { - this.InstallAsync(installId, isUpgrade, installCompleted, timestamp, versionMajor, versionMinor, versionPatch, versionComment, error, userAgent, dbProvider, null); - } - - /// - public void InstallAsync(System.Guid installId, bool isUpgrade, bool installCompleted, System.DateTime timestamp, int versionMajor, int versionMinor, int versionPatch, string versionComment, string error, string userAgent, string dbProvider, object userState) { - if ((this.InstallOperationCompleted == null)) { - this.InstallOperationCompleted = new System.Threading.SendOrPostCallback(this.OnInstallOperationCompleted); - } - this.InvokeAsync("Install", new object[] { - installId, - isUpgrade, - installCompleted, - timestamp, - versionMajor, - versionMinor, - versionPatch, - versionComment, - error, - userAgent, - dbProvider}, this.InstallOperationCompleted, userState); - } - - private void OnInstallOperationCompleted(object arg) { - if ((this.InstallCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.InstallCompleted(this, new System.ComponentModel.AsyncCompletedEventArgs(invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://update.umbraco.org/CheckUpgrade", RequestNamespace="http://update.umbraco.org/", ResponseNamespace="http://update.umbraco.org/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public UpgradeResult CheckUpgrade(int versionMajor, int versionMinor, int versionPatch, string versionComment) { - object[] results = this.Invoke("CheckUpgrade", new object[] { - versionMajor, - versionMinor, - versionPatch, - versionComment}); - return ((UpgradeResult)(results[0])); - } - - /// - public void CheckUpgradeAsync(int versionMajor, int versionMinor, int versionPatch, string versionComment) { - this.CheckUpgradeAsync(versionMajor, versionMinor, versionPatch, versionComment, null); - } - - /// - public void CheckUpgradeAsync(int versionMajor, int versionMinor, int versionPatch, string versionComment, object userState) { - if ((this.CheckUpgradeOperationCompleted == null)) { - this.CheckUpgradeOperationCompleted = new System.Threading.SendOrPostCallback(this.OnCheckUpgradeOperationCompleted); - } - this.InvokeAsync("CheckUpgrade", new object[] { - versionMajor, - versionMinor, - versionPatch, - versionComment}, this.CheckUpgradeOperationCompleted, userState); - } - - private void OnCheckUpgradeOperationCompleted(object arg) { - if ((this.CheckUpgradeCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.CheckUpgradeCompleted(this, new CheckUpgradeCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - public new void CancelAsync(object userState) { - base.CancelAsync(userState); - } - - private bool IsLocalFileSystemWebService(string url) { - if (((url == null) - || (url == string.Empty))) { - return false; - } - System.Uri wsUri = new System.Uri(url); - if (((wsUri.Port >= 1024) - && (string.Compare(wsUri.Host, "localHost", System.StringComparison.OrdinalIgnoreCase) == 0))) { - return true; - } - return false; - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://update.umbraco.org/")] - public partial class UpgradeResult { - - private string commentField; - - private UpgradeType upgradeTypeField; - - private string upgradeUrlField; - - /// - public string Comment { - get { - return this.commentField; - } - set { - this.commentField = value; - } - } - - /// - public UpgradeType UpgradeType { - get { - return this.upgradeTypeField; - } - set { - this.upgradeTypeField = value; - } - } - - /// - public string UpgradeUrl { - get { - return this.upgradeUrlField; - } - set { - this.upgradeUrlField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://update.umbraco.org/")] - public enum UpgradeType { - - /// - None, - - /// - Patch, - - /// - Minor, - - /// - Major, - - /// - Critical, - - /// - Error, - - /// - OutOfSync, - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void InstallCompletedEventHandler(object sender, System.ComponentModel.AsyncCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void CheckUpgradeCompletedEventHandler(object sender, CheckUpgradeCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class CheckUpgradeCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal CheckUpgradeCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public UpgradeResult Result { - get { - this.RaiseExceptionIfNecessary(); - return ((UpgradeResult)(this.results[0])); - } - } - } -} - -#pragma warning restore 1591 \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.map b/src/Umbraco.Web/Web References/org.umbraco.update/Reference.map deleted file mode 100644 index ded6650264..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/Reference.map +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult.datasource b/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult.datasource deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult1.datasource b/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult1.datasource deleted file mode 100644 index c42c98b0b7..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/UpgradeResult1.datasource +++ /dev/null @@ -1,10 +0,0 @@ - - - - umbraco.presentation.org.umbraco.update.UpgradeResult, Web References.org.umbraco.update.Reference.cs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.disco b/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.disco deleted file mode 100644 index 366f4fdd6e..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.disco +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.wsdl b/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.wsdl deleted file mode 100644 index e2aba65c7c..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.update/checkforupgrade.wsdl +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 490ce0808a5f7b4aebc64d479b6f70dbb62a1365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 13:28:46 +0100 Subject: [PATCH 389/610] Save & publish dialog change for a simpler experience accommodating segments --- .../umb-variant-selector-overlay.less | 11 +- .../content/overlays/publish.controller.js | 275 ++++++++---------- .../src/views/content/overlays/publish.html | 59 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 6 +- .../Umbraco/config/lang/en_us.xml | 6 +- 5 files changed, 176 insertions(+), 181 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less index 2b38d31633..af9c57b4ff 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less @@ -2,16 +2,19 @@ .umb-variant-selector-entry { - .umb_check_info { - margin-top: 2px; + .umb-form-check { + .umb-form-check__symbol { + margin-top: 2px; + } } } .umb-variant-selector-entry__title { - + font-weight: 600; + font-size: 14px; } .umb-variant-selector-entry__description { display: block; - font-size: 13px; + font-size: 12px; color: @gray-4; } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index d1b17021b6..c533cb5125 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -5,61 +5,58 @@ var vm = this; vm.loading = true; - vm.hasPristineVariants = false; vm.isNew = true; vm.changeSelection = changeSelection; - vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; - vm.notPublishedMandatoryFilter = notPublishedMandatoryFilter; - /** Returns true if publishing is possible based on if there are un-published mandatory languages */ + /** Returns true if publish meets the requirements of mandatory languages */ function canPublish() { - var possible = false; + var hasSomethingToPublish = false; + for (var i = 0; i < vm.variants.length; i++) { var variant = vm.variants[i]; - var state = canVariantPublish(variant); - if (state === true) { - possible = true; - } - if (state === false) { + + // if varaint is mandatory and not already published: + if (variant.publish === false && notPublishedMandatoryFilter(variant)) { return false; } + if (variant.publish === true) { + hasSomethingToPublish = true; + } + } - return possible; - } - - /** Returns true if publishing is possible based on if the variant is a un-published mandatory language */ - function canVariantPublish(variant) { - - //if this variant will show up in the publish-able list - var publishable = dirtyVariantFilter(variant); - var published = !(variant.state === "NotCreated" || variant.state === "Draft"); - - // is this variant mandatory: - if (variant.language && variant.language.isMandatory && !published && !variant.publish) { - //if a mandatory variant isn't published or set to be published - //then we cannot continue - - return false; - } - - // is this variant selected for publish: - if (variant.publish === true) { - return publishable; - } - - return null; + return hasSomethingToPublish; } function changeSelection(variant) { - + // update submit button state: $scope.model.disableSubmitButton = !canPublish(); - //need to set the Save state to true if publish is true + //need to set the Save state to same as publish. variant.save = variant.publish; - - variant.willPublish = canVariantPublish(variant); + } + + + function hasAnyDataFilter(variant) { + + if (variant.name == null || variant.name.length === 0) { + return false; + } + + if(variant.isDirty === true) { + return true; + } + + for (var t=0; t < variant.tabs.length; t++){ + for (var p=0; p < variant.tabs[t].properties.length; p++){ + var property = variant.tabs[t].properties[p]; + if (property.value != null && property.value.length > 0) { + return true; + } + } + } + + return false; } function dirtyVariantFilter(variant) { @@ -70,128 +67,112 @@ return (variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); } - function hasAnyData(variant) { - - if (variant.name == null || variant.name.length === 0) { - return false; - } - - var result = variant.isDirty != null; - - if(result) return true; - - for (var t=0; t < variant.tabs.length; t++){ - for (var p=0; p < variant.tabs[t].properties.length; p++){ - - var property = variant.tabs[t].properties[p]; - - if(property.culture == null) continue; - - result = result || (property.value != null && property.value.length > 0); - - if(result) return true; - } - } - - return result; + function publishableVariantFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * variant is active + // * it's editor is in a $dirty state + // * it has pending saves + // * it is unpublished + return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); } - function pristineVariantFilter(variant) { - return !(dirtyVariantFilter(variant)); - } function notPublishedMandatoryFilter(variant) { - return !dirtyVariantFilter(variant) && variant.isMandatory === true; + return variant.state !== "Published" && isMandatoryFilter(variant); + } + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); + } + function notPublishableButMandatoryFilter(variant) { + //determine a variant is needed, but not already a choice. + // * publishable — aka. displayed as a publish option. + // * published — its already published and everything is then fine. + // * mandatory — this is needed, and thats why we highlight it. + return !publishableVariantFilter(variant) && variant.state !== "Published" && variant.isMandatory === true; } function onInit() { - - vm.variants = $scope.model.variants; - if (!$scope.model.title) { - localizationService.localize("content_readyToPublish").then(function (value) { - $scope.model.title = value; - }); - } + _.each(vm.variants, (variant) => { + + // reset to not be published + variant.publish = false; + variant.save = false; + + variant.isMandatory = isMandatoryFilter(variant); - vm.hasPristineVariants = false; - - _.each(vm.variants, - function (variant) { - if(variant.state === "NotCreated") { - vm.isNew = true; - } + // If we have a variant thats not in the state of NotCreated, then we know we have adata and its not a new content node. + if(variant.state !== "NotCreated") { + vm.isNew = false; } - ); - - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - - // reset to not be published - variant.publish = false; - variant.save = false; - - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); - } - - // If the variant havent been created jet. - if(variant.state === "NotCreated") { - // If the variant is mandatory, then set the variant to be published. - if (variant.language.isMandatory === true) { - variant.publish = true; - variant.save = true; - } - } - - variant.canPublish = dirtyVariantFilter(variant); - - // if we have data on this variant. - if(variant.canPublish && hasAnyData(variant)) { - // and if some varaints havent been saved before, or they dont have a publishing date set, then we set it for publishing. - if(vm.isNew || variant.publishDate == null){ - variant.publish = true; - variant.save = true; - } - } - - variant.willPublish = canVariantPublish(variant); - } - ); - - if (vm.variants.length !== 0) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - - var active = _.find(vm.variants, function (v) { - return v.active; - }); - - if (active) { - //ensure that the current one is selected - active.publish = true; - active.save = true; - } - - $scope.model.disableSubmitButton = !canPublish(); - - } else { - //disable Publish button if we have nothing to publish, should not happen - $scope.model.disableSubmitButton = true; - } - - localizationService.localize("content_variantsToPublish").then(function (value) { - vm.headline = value; - vm.loading = false; }); + _.each(vm.variants, (variant) => { + variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); + variant.htmlId = "_content_variant_" + variant.compositeId; + + // if this is a new node and we have data on this variant. + if(vm.isNew === true && hasAnyDataFilter(variant)) { + variant.save = true; + } + + }); + vm.availableVariants = vm.variants.filter(publishableVariantFilter); + vm.missingMandatoryVariants = vm.variants.filter(notPublishableButMandatoryFilter); + + // if any active varaiant that is available for publish, we set it to be published: + _.each(vm.availableVariants, (v) => { + if(v.active) { + v.save = v.publish = true; + } + }); + + if (vm.availableVariants.length !== 0) { + //now sort it so that the current one is at the top + vm.availableVariants = vm.availableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; + }); + } + + + $scope.model.disableSubmitButton = !canPublish(); + + if (vm.missingMandatoryVariants.length > 0) { + localizationService.localize("content_notReadyToPublish").then(function (value) { + $scope.model.title = value; + vm.loading = false; + }); + } else { + if (!$scope.model.title) { + localizationService.localize("content_readyToPublish").then(function (value) { + $scope.model.title = value; + vm.loading = false; + }); + } else { + vm.loading = false; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index 619efa6d40..ae0d35c293 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -1,17 +1,17 @@
      -
      -

      {{vm.headline}}

      -
      -
      -
      +
      + +
      +

      +
      + ng-repeat="variant in vm.availableVariants track by variant.compositeId">
      @@ -19,14 +19,16 @@ name="publishVariantSelector" model="variant.publish" on-change="vm.changeSelection(variant)" - disabled="(variant.canPublish === false)" server-validation-field="{{variant.htmlId}}" > - + + + * + - - - + - + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} @@ -39,28 +41,33 @@
      -
      +
      +
      +

      +
      -
      -
      -

      +
      +
      +

      -
      -
      -
      +
      +
      + - * -
      + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + -
      - -
      - -
      -
      {{notification.message}}
      -
      +
      diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 00a4a3af05..1067265ff8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -303,14 +303,16 @@ Unmodified Languages These languages haven't been created + All new variants will be saved. Which variants you would like to publish? Choose which variants to be saved. Pick variants to send for approval. Set scheduled publishing... Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. - Unpublished mandatory variants + The following variants is required for publishing to take place: - Ready to Publish? + We are not ready to Publish + Save & Publish Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. 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 2bbfc22c70..2dc63ecd65 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -307,14 +307,16 @@ Unmodified Languages These languages haven't been created + All new variants will be saved. Which variants you would like to publish? Choose which variants to be saved. Pick variants to send for approval. Set scheduled publishing... Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. - Unpublished mandatory variants + The following variants is required for publishing to take place: - Ready to Publish? + We are not ready to Publish + Save & Publish Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. From 64cffcb9679a9d8a66a7735b3e86cbc9ea490088 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 19 Feb 2020 14:10:21 +0100 Subject: [PATCH 390/610] Fix Nested Content JS errors when enforcing min items (#7628) --- .../nestedcontent/nestedcontent.controller.js | 6 ++++++ 1 file changed, 6 insertions(+) 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 7de3a5b567..f20304f324 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 @@ -480,10 +480,12 @@ } // Enforce min items if we only have one scaffold type + var modelWasChanged = false; if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { for (var i = vm.nodes.length; i < model.config.minItems; i++) { addNode(vm.scaffolds[0].contentTypeAlias); } + modelWasChanged = true; } // If there is only one item, set it as current node @@ -495,6 +497,10 @@ vm.inited = true; + if (modelWasChanged) { + updateModel(); + } + updatePropertyActionStates(); checkAbilityToPasteContent(); } From 99ad8ea773354f788bf387df18d4b2559b7346c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 15:02:33 +0100 Subject: [PATCH 391/610] apply search params instead of overwritting them. --- .../components/content/umbvariantcontenteditors.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d929098175..503ea911ee 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 @@ -200,7 +200,7 @@ if (editorIndex === 0) { //If we've made it this far, then update the query string. //The editor will respond to this query string changing. - $location.search({"cculture": variantCulture, "csegment": variant.segment}); + $location.search("cculture", variantCulture).search("csegment", variant.segment); } else { From 602ee33334b7b14de496b5270ae9c787c41b92a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 15:35:46 +0100 Subject: [PATCH 392/610] use "and" instead of & --- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 1067265ff8..28a9d953b8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -312,7 +312,7 @@ The following variants is required for publishing to take place: We are not ready to Publish - Save & Publish + Save and Publish Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. 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 2dc63ecd65..eae9acf49b 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -316,7 +316,7 @@ The following variants is required for publishing to take place: We are not ready to Publish - Save & Publish + Save and Publish Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. From 27b239cc088243e830084815ec77ddc188f826e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 17:25:08 +0100 Subject: [PATCH 393/610] making variant dialogs up to date with Segments. --- .../src/less/properties.less | 6 +- .../content/overlays/publish.controller.js | 1 - .../overlays/publishdescendants.controller.js | 37 +++++++++--- .../content/overlays/publishdescendants.html | 56 +++++++++---------- .../views/content/overlays/save.controller.js | 54 ++++++++++-------- .../src/views/content/overlays/save.html | 26 --------- .../content/overlays/schedule.controller.js | 22 +++++++- .../src/views/content/overlays/schedule.html | 33 ++++++----- .../overlays/sendtopublish.controller.js | 24 ++++++-- .../content/overlays/unpublish.controller.js | 56 +++++++++++-------- .../src/views/content/overlays/unpublish.html | 55 ++++++++---------- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 8 +-- .../Umbraco/config/lang/en_us.xml | 8 +-- 13 files changed, 211 insertions(+), 175 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index 8523fe9300..9e951feb1a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -49,7 +49,7 @@ } .date-wrapper-mini--checkbox{ - margin: 0 0 0 26px; + margin: 0 0 0 28px; } .date-wrapper-mini__date { @@ -62,6 +62,10 @@ &:first-of-type { margin-left: 0; } + .flatpickr-input > button:first-of-type { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } .date-wrapper-mini__date .flatpickr-input > a { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index c533cb5125..58f5aeaeaa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -133,7 +133,6 @@ }); if (vm.availableVariants.length !== 0) { - //now sort it so that the current one is at the top vm.availableVariants = vm.availableVariants.sort(function (a, b) { if (a.language && b.language) { if (a.language.name > b.language.name) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js index 0ca2fe65c9..2dfb907922 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js @@ -5,11 +5,14 @@ var vm = this; + vm.includeUnpublished = false; + vm.changeSelection = changeSelection; + vm.toggleIncludeUnpublished = toggleIncludeUnpublished; + function onInit() { - vm.includeUnpublished = false; vm.variants = $scope.model.variants; vm.labels = {}; @@ -27,9 +30,24 @@ if (vm.variants.length > 1) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + vm.variants = vm.variants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); var active = _.find(vm.variants, function (v) { @@ -48,14 +66,17 @@ // localize help text for invariant content vm.labels.help = { "key": "content_publishDescendantsHelp", - "tokens": [] + "tokens": [vm.variants[0].name] }; - // add the node name as a token so it will show up in the translated text - vm.labels.help.tokens.push(vm.variants[0].name); } } + function toggleIncludeUnpublished() { + console.log("toggleIncludeUnpublished") + vm.includeUnpublished = !vm.includeUnpublished; + } + /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { var selected = []; @@ -64,7 +85,7 @@ var published = !(variant.state === "NotCreated" || variant.state === "Draft"); - if (variant.language.isMandatory && !published && !variant.publish) { + if (variant.segment == null && variant.language && variant.language.isMandatory && !published && !variant.publish) { //if a mandatory variant isn't published //and not flagged for saving //then we cannot continue diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html index 0bf434f9a4..8a58e02dd2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html @@ -1,4 +1,4 @@ -
      +
      @@ -6,11 +6,12 @@
      - +
      @@ -22,44 +23,43 @@
      - -
      - -
      - +
      -
      +
      + > -
      - - - - - - - - - {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} - - - + + + * -
      + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + +
      + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js index aacea9fe0e..9508cb6917 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js @@ -10,7 +10,6 @@ vm.changeSelection = changeSelection; vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; function changeSelection(variant) { var firstSelected = _.find(vm.variants, function (v) { @@ -23,14 +22,9 @@ //determine a variant is 'dirty' (meaning it will show up as save-able) if it's // * the active one // * it's editor is in a $dirty state - // * it is in NotCreated state return (variant.active || variant.isDirty); } - function pristineVariantFilter(variant) { - return !(dirtyVariantFilter(variant)); - } - function hasAnyData(variant) { if(variant.name == null || variant.name.length === 0) { return false; @@ -64,10 +58,13 @@ }); } - vm.hasPristineVariants = false; - _.each(vm.variants, function (variant) { + + //reset state: + variant.save = false; + variant.publish = false; + if(variant.state !== "NotCreated"){ vm.isNew = false; } @@ -78,31 +75,40 @@ variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); variant.htmlId = "_content_variant_" + variant.compositeId; - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); - } - if(vm.isNew && hasAnyData(variant)){ variant.save = true; } }); if (vm.variants.length !== 0) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + + vm.variants = vm.variants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); - - var active = _.find(vm.variants, function (v) { - return v.active; + + _.find(vm.variants, function (v) { + if(v.active) { + //ensure that the current one is selected + v.save = true; + } }); - if (active) { - //ensure that the current one is selected - active.save = true; - } - } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html index 552f6003b0..516446a676 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html @@ -46,31 +46,5 @@

      - -
      -
      -

      - - -

      -
      - -
      -
      -
      - {{ variant.language.name }} - * -
      - -
      - -
      - -
      -
      {{notification.message}}
      -
      -
      -
      -
      diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js index 5aa7eff1ef..a55f0e0171 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js @@ -54,9 +54,25 @@ } }); - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + + vm.variants = vm.variants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); var active = _.find(vm.variants, function (v) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index 4f4e3c2874..80cffbc998 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -1,4 +1,4 @@ -
      +
      @@ -86,23 +86,28 @@
      -
      +
      + > + + + * + + + + - + +
      - - - - - +
      @@ -117,7 +122,7 @@ on-open="vm.datePickerShow(variant, 'publish')" on-close="vm.datePickerClose(variant, 'publish')">
      - @@ -126,9 +131,9 @@
      - +
      @@ -143,7 +148,7 @@ on-open="vm.datePickerShow(variant, 'unpublish')" on-close="vm.datePickerClose(variant, 'unpublish')">
      - @@ -152,7 +157,7 @@
      - +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js index c85e8d7013..296698f576 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js @@ -20,8 +20,7 @@ $scope.model.title = value; }); } - - + if (vm.variants.length !== 0) { _.each(vm.variants, function (variant) { @@ -29,9 +28,24 @@ variant.htmlId = "_content_variant_" + variant.compositeId; }); - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + vm.variants = vm.variants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); var active = _.find(vm.variants, function (v) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js index 3011bf49ee..06f7be3a17 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js @@ -8,7 +8,6 @@ vm.changeSelection = changeSelection; vm.publishedVariantFilter = publishedVariantFilter; - vm.unpublishedVariantFilter = unpublishedVariantFilter; function onInit() { @@ -29,16 +28,32 @@ // node has variants if (vm.variants.length !== 1) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + + vm.variants = vm.variants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); var active = _.find(vm.variants, function (v) { return v.active; }); - if (active) { + if (active && publishedVariantFilter(active)) { //ensure that the current one is selected active.save = true; } @@ -51,21 +66,15 @@ function changeSelection(selectedVariant) { - // disable submit button if nothing is selected - var firstSelected = _.find(vm.variants, function (v) { - return v.save; - }); - $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected - - // if a mandatory variant is selected we want to select all other variants + // if a mandatory variant is selected we want to select all other variants, we cant have anything published if a mandatory variants gets unpublished. // and disable selection for the others - if(selectedVariant.save && selectedVariant.language.isMandatory) { + if(selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { angular.forEach(vm.variants, function(variant){ - if(!variant.save && publishedVariantFilter(variant)) { + if(!variant.save) { // keep track of the variants we automaically select // so we can remove the selection again - autoSelectedVariants.push(variant.language.culture); + autoSelectedVariants.push(variant); variant.save = true; } variant.disabled = true; @@ -79,12 +88,12 @@ // if a mandatory variant is deselected we want to deselet all the variants // that was automatically selected so it goes back to the state before the mandatory language was selected. // We also want to enable all checkboxes again - if(!selectedVariant.save && selectedVariant.language.isMandatory) { + if(!selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { angular.forEach(vm.variants, function(variant){ // check if variant was auto selected, then deselect - if(_.contains(autoSelectedVariants, variant.language.culture)) { + if(_.contains(autoSelectedVariants, variant)) { variant.save = false; }; @@ -93,6 +102,12 @@ autoSelectedVariants = []; } + // disable submit button if nothing is selected + var firstSelected = _.find(vm.variants, function (v) { + return v.save; + }); + $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected + } function publishedVariantFilter(variant) { @@ -102,13 +117,6 @@ return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); } - function unpublishedVariantFilter(variant) { - //determine a variant is 'modified' (meaning it will NOT show up as able to unpublish) - // * it's editor is in a $dirty state - // * it is published with pending changes - return (variant.state !== "Published" && variant.state !== "PublishedPendingChanges"); - } - //when this dialog is closed, remove all unpublish and disabled flags $scope.$on('$destroy', function () { for (var i = 0; i < vm.variants.length; i++) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html index e384823cc3..adcba5c259 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html @@ -1,4 +1,4 @@ -
      +
      @@ -10,12 +10,12 @@

      - +
      - +
      -
      +
      - -
      - - - - + > + + + * -
      + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + +
      + + +
      - +

      - -
      -
      -

      -
      - -
      -
      -
      - {{ variant.language.name }} - * -
      - -
      - -
      -
      -
      -
      - +
      diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 28a9d953b8..9cbc8536a8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -250,8 +250,8 @@ Published Published (pending changes) Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + %0% and all content items underneath and thereby making their content publicly available.]]> + Publish at Unpublish at Clear Date @@ -288,7 +288,7 @@ Add another text box Remove this text box Content root - Include drafts: also publish unpublished content items. + Include drafts and unpublished content items. This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. What languages would you like to publish? All languages with content are saved! @@ -312,7 +312,7 @@ The following variants is required for publishing to take place: We are not ready to Publish - Save and Publish + Ready to publish? Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. 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 eae9acf49b..dc115c98c2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -253,8 +253,8 @@ Published Published (pending changes)> Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + %0% and all content items underneath and thereby making their content publicly available.]]> + Publish at Unpublish at Clear Date @@ -292,7 +292,7 @@ Add another text box Remove this text box Content root - Include drafts: also publish unpublished content items. + Include drafts and unpublished content items. This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. What languages would you like to publish? All languages with content are saved! @@ -316,7 +316,7 @@ The following variants is required for publishing to take place: We are not ready to Publish - Save and Publish + Ready to publish? Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. From dcdeae9ea3a9b12c0370360eb579c91983390820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 Feb 2020 17:27:04 +0100 Subject: [PATCH 394/610] only pure language variants can be mandatory --- .../src/views/content/overlays/schedule.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js index a55f0e0171..087fba49cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js @@ -337,7 +337,7 @@ return true; } - var isMandatory = variant.language && variant.language.isMandatory; + var isMandatory = variant.segment == null && variant.language && variant.language.isMandatory; //if this variant will show up in the publish-able list var publishable = dirtyVariantFilter(variant); From 65750d2b32ac4376bd9458785f20caba1a9aed67 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 20 Feb 2020 08:59:46 +1000 Subject: [PATCH 395/610] fixes regression where validation object doesn't always exist on the property, depending on context --- .../directives/validation/valpropertymsg.directive.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 5b0c4c11ae..149c2b5087 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -39,7 +39,9 @@ function valPropertyMsg(serverValidationManager, localizationService) { scope.currentProperty = currentProperty; var currentCulture = currentProperty.culture; - var isMandatory = currentProperty.validation.mandatory; + + // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) + var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; var labels = {}; localizationService.localize("errors_propertyHasErrors").then(function (data) { @@ -141,7 +143,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) { // errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared - if (!isMandatory && !currentProperty.value) { + if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) { hasError = false; showValidation = false; scope.errorMsg = ""; From 510ecf5168ad62dc21b6d740d2adef0807a002ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 20 Feb 2020 10:22:09 +0100 Subject: [PATCH 396/610] update save dialogs --- .../umb-variant-selector-overlay.less | 4 ++ .../editor/umb-editor-content-header.html | 2 +- .../src/views/content/overlays/publish.html | 14 ++++- .../overlays/publishdescendants.controller.js | 19 +++++-- .../content/overlays/publishdescendants.html | 14 +++-- .../views/content/overlays/save.controller.js | 11 +++- .../src/views/content/overlays/save.html | 40 ++++++++------ .../content/overlays/schedule.controller.js | 45 +++++++-------- .../src/views/content/overlays/schedule.html | 9 ++- .../overlays/sendtopublish.controller.js | 55 +++++++++---------- .../views/content/overlays/sendtopublish.html | 55 ++++++++----------- .../content/overlays/unpublish.controller.js | 18 ++++-- .../src/views/content/overlays/unpublish.html | 7 ++- 13 files changed, 168 insertions(+), 125 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less index af9c57b4ff..b50a622f98 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less @@ -11,6 +11,10 @@ .umb-variant-selector-entry__title { font-weight: 600; font-size: 14px; + .__secondarytitle { + font-weight: normal; + color: @gray-5; + } } .umb-variant-selector-entry__description { display: block; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 544a04515f..a17529ca79 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -53,7 +53,7 @@ -
    • -
    • From f4cd1c2f0cdaada66b28c60c9a3dd8742dcd8a7e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 14:28:37 +0100 Subject: [PATCH 412/610] Fix JS errors for Nested Content in single mode --- .../nestedcontent/nestedcontent.controller.js | 7 +++++++ 1 file changed, 7 insertions(+) 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 875efa45f3..f2538c66c1 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 @@ -277,6 +277,9 @@ }; vm.getName = function (idx) { + if (!model.value || !model.value.length) { + return ""; + } var name = ""; @@ -326,6 +329,10 @@ }; vm.getIcon = function (idx) { + if (!model.value || !model.value.length) { + return ""; + } + var scaffold = getScaffold(model.value[idx].ncContentTypeAlias); return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder"; } From b48e288d717cf51dedfe80623fff27b5e5e36a43 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 14:28:37 +0100 Subject: [PATCH 413/610] Fix JS errors for Nested Content in single mode (cherry picked from commit f4cd1c2f0cdaada66b28c60c9a3dd8742dcd8a7e) --- .../nestedcontent/nestedcontent.controller.js | 7 +++++++ 1 file changed, 7 insertions(+) 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 1c6d305c6b..19547c38e4 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 @@ -277,6 +277,9 @@ }; vm.getName = function (idx) { + if (!model.value || !model.value.length) { + return ""; + } var name = ""; @@ -326,6 +329,10 @@ }; vm.getIcon = function (idx) { + if (!model.value || !model.value.length) { + return ""; + } + var scaffold = getScaffold(model.value[idx].ncContentTypeAlias); return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder"; } From 5e155a871cbb30f99cd7949330b6859f6a035f71 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 11 Feb 2020 07:42:54 +0100 Subject: [PATCH 414/610] Fix the create dialog on empty installs --- .../src/views/content/content.create.controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 64f601f8b7..1a1254fcf1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -22,10 +22,14 @@ function contentCreateController($scope, function initialize() { $scope.loading = true; $scope.allowedTypes = null; - $scope.countTypes = contentTypeResource.getCount; var getAllowedTypes = contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); + if ($scope.allowedTypes.length === 0) { + contentTypeResource.getCount().then(function(count) { + $scope.countTypes = count; + }); + } }); var getCurrentUser = authResource.getCurrentUser().then(function (currentUser) { if (currentUser.allowedSections.indexOf("settings") > -1) { From e4edd47a484407724948f8e719aae95766b9263e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 8 Feb 2020 20:24:02 +0100 Subject: [PATCH 415/610] Ensure that empty asset URLs in plugin manifests do not break the application --- src/Umbraco.Web/JavaScript/AssetInitialization.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/JavaScript/AssetInitialization.cs b/src/Umbraco.Web/JavaScript/AssetInitialization.cs index 00605ece1f..42c750ffd3 100644 --- a/src/Umbraco.Web/JavaScript/AssetInitialization.cs +++ b/src/Umbraco.Web/JavaScript/AssetInitialization.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Web; using ClientDependency.Core; using ClientDependency.Core.Config; +using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Web.PropertyEditors; @@ -35,7 +36,7 @@ namespace Umbraco.Web.JavaScript var requestUrl = httpContext.Request.Url; if (requestUrl == null) throw new ArgumentException("HttpContext.Request.Url is null.", nameof(httpContext)); - var dependencies = assets.Select(x => + var dependencies = assets.Where(x => x.IsNullOrWhiteSpace() == false).Select(x => { // most declarations with be made relative to the /umbraco folder, so things // like lib/blah/blah.js so we need to turn them into absolutes here From ea29eb993c8c77a49a76fd61fa9c24f5b7f3116b Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 8 Feb 2020 20:12:38 +0100 Subject: [PATCH 416/610] Align "Field is mandatory" label with the actual toggle --- .../propertysettings/propertysettings.controller.js | 4 +++- .../propertysettings/propertysettings.html | 11 ++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js index b8581d28d0..9640f2eba2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js @@ -56,7 +56,8 @@ "validation_validateAsEmail", "validation_validateAsNumber", "validation_validateAsUrl", - "validation_enterCustomValidation" + "validation_enterCustomValidation", + "validation_fieldIsMandatory" ]; localizationService.localizeMany(labels) @@ -66,6 +67,7 @@ vm.labels.validateAsNumber = data[1]; vm.labels.validateAsUrl = data[2]; vm.labels.customValidation = data[3]; + vm.labels.fieldIsMandatory = data[4]; vm.validationTypes = [ { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 4474390199..77ee276e3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -84,14 +84,15 @@
      - - + on-click="vm.toggleValidation()" + label-on="{{vm.labels.fieldIsMandatory}}" + label-off="{{vm.labels.fieldIsMandatory}}" + show-labels="true" + label-position="right" focus-when="{{vm.focusOnMandatoryField}}" + class="mb1"> Date: Sat, 8 Feb 2020 20:15:22 +0100 Subject: [PATCH 417/610] Only show culture for content links if there is more than one culture --- .../components/content/umbcontentnodeinfo.directive.js | 3 +++ .../src/views/components/content/umb-content-node-info.html | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 0ed1b12000..95b2a520d1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -328,6 +328,9 @@ // invariant nodes scope.currentUrls = scope.node.urls; } + + // figure out if multiple cultures apply across the content urls + scope.currentUrlsHaveMultipleCultures = _.keys(_.groupBy(scope.currentUrls, url => url.culture)).length > 1; } // load audit trail and redirects when on the info tab 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 c35686acd1..d8241b4dd7 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 @@ -8,13 +8,13 @@
        -
      • - - - - - +
      • +
      @@ -137,22 +148,22 @@ ng-model="model.value.styles">
    • - + - - +
      -
    • - - - - +
    • +
    From 867232531c626e328cc21fc2bee95abcbf611675 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Sat, 29 Feb 2020 21:36:56 +0000 Subject: [PATCH 429/610] Create Content Blueprints accessibility improvements (#7020) * Create Content Blueprints - Disallow element types and some accessibility improvements * re-enabled element types for content blueprint --- .../src/less/modals.less | 10 +++++--- .../src/views/contentblueprints/create.html | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 925f845c4c..4ce907d06f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -88,9 +88,13 @@ background: @white; } -.umb-dialog .umb-btn-toolbar .umb-control-group{ - border: none; - padding: none; +.umb-dialog .abstract{ + margin-bottom:20px; +} + +.umb-dialog .umb-btn-toolbar .umb-control-group { + border: none; + padding: none; } .umb-dialog-body{ diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html index 6146c007b1..2e1d398409 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html @@ -1,18 +1,20 @@