From 8a60c95a36242ee6cbae36232599bed38e902ec9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 12 Sep 2019 17:04:29 +1000 Subject: [PATCH 001/159] 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 002/159] #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 003/159] #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 f967f2111938d8954b5a61b7db54f23c43c45b49 Mon Sep 17 00:00:00 2001 From: Dirk De Grave Date: Tue, 1 Oct 2019 13:39:32 +0200 Subject: [PATCH 004/159] Fixes #6499 by reloading the content in list view after bulk copy/move --- .../listview/listview.controller.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 9b4c01f4f9..0812bcff3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -472,7 +472,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie // a specific value from one of the methods, so we'll have to try this way. Even though the first method // will fire once per every node moved, the destination path will be the same and we need to use that to sync. var newPath = null; - applySelected( + var attempt = applySelected( function(selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }) .then(function(path) { @@ -509,6 +509,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }); } }); + if (attempt) { + attempt.then(function () { + $scope.getContent(); + }); + } } $scope.copy = function () { @@ -536,7 +541,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }; function performCopy(target, relateToOriginal) { - applySelected( + var attempt = applySelected( function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, function (count, total) { var key = (total === 1 ? "bulk_copiedItemOfItem" : "bulk_copiedItemOfItems"); @@ -546,6 +551,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie var key = (total === 1 ? "bulk_copiedItem" : "bulk_copiedItems"); return localizationService.localize(key, [total]); }); + if (attempt) { + attempt.then(function () { + $scope.getContent(); + }); + } } function getCustomPropertyValue(alias, properties) { From b05ef082fdb76e923b50461cad42d668b4ed5448 Mon Sep 17 00:00:00 2001 From: Dirk De Grave Date: Tue, 1 Oct 2019 13:39:32 +0200 Subject: [PATCH 005/159] Fixes #6499 by reloading the content in list view after bulk copy/move (cherry picked from commit f967f2111938d8954b5a61b7db54f23c43c45b49) --- .../listview/listview.controller.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 9b4c01f4f9..0812bcff3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -472,7 +472,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie // a specific value from one of the methods, so we'll have to try this way. Even though the first method // will fire once per every node moved, the destination path will be the same and we need to use that to sync. var newPath = null; - applySelected( + var attempt = applySelected( function(selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }) .then(function(path) { @@ -509,6 +509,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }); } }); + if (attempt) { + attempt.then(function () { + $scope.getContent(); + }); + } } $scope.copy = function () { @@ -536,7 +541,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie }; function performCopy(target, relateToOriginal) { - applySelected( + var attempt = applySelected( function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, function (count, total) { var key = (total === 1 ? "bulk_copiedItemOfItem" : "bulk_copiedItemOfItems"); @@ -546,6 +551,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie var key = (total === 1 ? "bulk_copiedItem" : "bulk_copiedItems"); return localizationService.localize(key, [total]); }); + if (attempt) { + attempt.then(function () { + $scope.getContent(); + }); + } } function getCustomPropertyValue(alias, properties) { From 9c28a1ff808d4ca0059002cd78d90eab7676bf69 Mon Sep 17 00:00:00 2001 From: jmayntzhusen Date: Tue, 1 Oct 2019 20:40:46 +0200 Subject: [PATCH 006/159] Update styles of API docs to match new identity --- apidocs/umbracotemplate/styles/main.css | 274 ++++++++++++++++++------ 1 file changed, 208 insertions(+), 66 deletions(-) diff --git a/apidocs/umbracotemplate/styles/main.css b/apidocs/umbracotemplate/styles/main.css index d74d51b150..69dde09875 100644 --- a/apidocs/umbracotemplate/styles/main.css +++ b/apidocs/umbracotemplate/styles/main.css @@ -1,73 +1,215 @@ body { - color: rgba(0,0,0,.8); -} -.navbar-inverse { - background: #a3db78; -} -.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text { - color: rgba(0,0,0,.8); -} - -.navbar-inverse { - border-color: transparent; -} - -.sidetoc { - background-color: #f5fbf1; -} -body .toc { - background-color: #f5fbf1; -} -.sidefilter { - background-color: #daf0c9; -} -.subnav { - background-color: #f5fbf1; -} - -.navbar-inverse .navbar-nav>.active>a { - color: rgba(0,0,0,.8); - background-color: #daf0c9; -} - -.navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover { - color: rgba(0,0,0,.8); - background-color: #daf0c9; -} - -.btn-primary { - color: rgba(0,0,0,.8); - background-color: #fff; - border-color: rgba(0,0,0,.8); -} -.btn-primary:hover { - background-color: #daf0c9; - color: rgba(0,0,0,.8); - border-color: rgba(0,0,0,.8); -} - -.toc .nav > li > a { - color: rgba(0,0,0,.8); -} - -button, a { - color: #f36f21; -} - -button:hover, -button:focus, -a:hover, -a:focus { - color: #143653; - text-decoration: none; + color: #34393e; + font-family: 'Roboto', sans-serif; + line-height: 1.5; + font-size: 16px; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + word-wrap: break-word } .navbar-header .navbar-brand { - background: url(https://our.umbraco.com/assets/images/logo.svg) left center no-repeat; - background-size: 40px auto; - width:50px; + background: url(https://our.umbraco.com/assets/images/logo.svg) left center no-repeat; + background-size: 40px auto; + width:50px; } -.toc .nav > li.active > a { - color: #f36f21; +#_content>a { + margin-top: 5px; } +/* HEADINGS */ + +h1 { + font-weight: 600; + font-size: 32px; +} + +h2 { + font-weight: 600; + font-size: 24px; + line-height: 1.8; +} + +h3 { + font-weight: 600; + font-size: 20px; + line-height: 1.8; +} + +h5 { + font-size: 14px; + padding: 10px 0px; +} + +article h1, +article h2, +article h3, +article h4 { + margin-top: 35px; + margin-bottom: 15px; +} + +article h4 { + padding-bottom: 8px; + border-bottom: 2px solid #ddd; +} + +/* NAVBAR */ + +.navbar-brand>img { + color: #fff; +} + +.navbar { + border: none; + /* Both navbars use box-shadow */ + -webkit-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + -moz-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); +} + +.subnav { + border-top: 1px solid #ddd; + background-color: #fff; +} + +.navbar-inverse { + background-color: #303ea1; + z-index: 100; +} + +.navbar-inverse .navbar-nav>li>a, +.navbar-inverse .navbar-text { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid transparent; + padding-bottom: 12px; +} + +.navbar-inverse .navbar-nav>li>a:focus, +.navbar-inverse .navbar-nav>li>a:hover { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid white; +} + +.navbar-inverse .navbar-nav>.active>a, +.navbar-inverse .navbar-nav>.active>a:focus, +.navbar-inverse .navbar-nav>.active>a:hover { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid white; +} + +.navbar-form .form-control { + border: none; + border-radius: 20px; +} + +/* SIDEBAR */ + +.toc .level1>li { + font-weight: 400; +} + +.toc .nav>li>a { + color: #34393e; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.toc-filter { + padding: 10px; + margin: 0; +} + +.toc-filter>input { + border: 2px solid #ddd; + border-radius: 20px; +} + +.toc-filter>.filter-icon { + display: none; +} + +.sidetoc>.toc { + background-color: #fff; + overflow-x: hidden; +} + +.sidetoc { + background-color: #fff; + border: none; +} + +/* ALERTS */ + +.alert { + padding: 0px 0px 5px 0px; + color: inherit; + background-color: inherit; + border: none; + box-shadow: 0px 2px 2px 0px rgba(100, 100, 100, 0.4); +} + +.alert>p { + margin-bottom: 0; + padding: 5px 10px; +} + +.alert>ul { + margin-bottom: 0; + padding: 5px 40px; +} + +.alert>h5 { + padding: 10px 15px; + margin-top: 0; + text-transform: uppercase; + font-weight: bold; + border-radius: 4px 4px 0 0; +} + +.alert-info>h5 { + color: #1976d2; + border-bottom: 4px solid #1976d2; + background-color: #e3f2fd; +} + +.alert-warning>h5 { + color: #f57f17; + border-bottom: 4px solid #f57f17; + background-color: #fff3e0; +} + +.alert-danger>h5 { + color: #d32f2f; + border-bottom: 4px solid #d32f2f; + background-color: #ffebee; +} + +/* CODE HIGHLIGHT */ +pre { +padding: 9.5px; +margin: 0 0 10px; +font-size: 13px; +word-break: break-all; +word-wrap: break-word; +background-color: #fffaef; +border-radius: 4px; +box-shadow: 0px 1px 4px 1px rgba(100, 100, 100, 0.4); +} + +.sideaffix { + overflow: visible; +} \ No newline at end of file From e0ea4ef36ffb6ff5289c34541f006cdbba9394e2 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 2 Oct 2019 14:16:03 -0700 Subject: [PATCH 007/159] Add null check in allowed template check --- src/Umbraco.Web/PublishedContentExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index c80f41d9fc..d1bc7dffff 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -135,6 +135,9 @@ namespace Umbraco.Web public static bool IsAllowedTemplate(this IPublishedContent content, int templateId) { + if (content == null) + return false; + if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == true) return content.TemplateId == templateId; From 96a2af2653da49ebdc793c9aa53acd54cc173110 Mon Sep 17 00:00:00 2001 From: Rasmus Olofsson Date: Thu, 24 Oct 2019 18:43:37 +0200 Subject: [PATCH 008/159] Add Swedish translations for prompt --- src/Umbraco.Web.UI/umbraco/config/lang/sv.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index 982f31e383..0a2f37ce02 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -182,10 +182,11 @@ Besök - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Stanna + Avfärda ändringar + Du har inte sparat dina ändringar + Är du säker på att du vill navigera bort från denna sida? - du har inte sparat dina ändringar + Avpublicering kommer att ta bort denna sida och alla dess ättlingar från hemsidan. Done From 8a02b4f58723d2c9746e7a59d417449ff5e2e933 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 30 Oct 2019 11:24:08 +0100 Subject: [PATCH 009/159] Editors should always be allowed to delete their own content (#6799) * Users should always be allowed to delete their own content * Changed comment to be more precise --- src/Umbraco.Web/Editors/ContentController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 8bb7af0077..bfc9ae071f 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -28,6 +28,7 @@ using Constants = Umbraco.Core.Constants; using umbraco.cms.businesslogic; using System.Collections; using umbraco; +using umbraco.BusinessLogic.Actions; namespace Umbraco.Web.Editors { @@ -1171,6 +1172,12 @@ namespace Umbraco.Web.Editors var path = contentItem != null ? contentItem.Path : nodeId.ToString(); var permission = userService.GetPermissionsForPath(user, path); + // users are allowed to delete their own content - see ContentTreeControllerBase.GetAllowedUserMenuItemsForNode() + if(contentItem != null && contentItem.CreatorId == user.Id) + { + permission.PermissionsSet.Add(new EntityPermission(0, contentItem.Id, new [] { ActionDelete.Instance.Letter.ToString() } )); + } + var allowed = true; foreach (var p in permissionsToCheck) { From f172760f961218760ce4f9e8dc0e9b7908ad90b0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 30 Oct 2019 11:24:08 +0100 Subject: [PATCH 010/159] Editors should always be allowed to delete their own content (#6799) * Users should always be allowed to delete their own content * Changed comment to be more precise (cherry picked from commit 8a02b4f58723d2c9746e7a59d417449ff5e2e933) --- src/Umbraco.Web/Editors/ContentController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 8bb7af0077..bfc9ae071f 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -28,6 +28,7 @@ using Constants = Umbraco.Core.Constants; using umbraco.cms.businesslogic; using System.Collections; using umbraco; +using umbraco.BusinessLogic.Actions; namespace Umbraco.Web.Editors { @@ -1171,6 +1172,12 @@ namespace Umbraco.Web.Editors var path = contentItem != null ? contentItem.Path : nodeId.ToString(); var permission = userService.GetPermissionsForPath(user, path); + // users are allowed to delete their own content - see ContentTreeControllerBase.GetAllowedUserMenuItemsForNode() + if(contentItem != null && contentItem.CreatorId == user.Id) + { + permission.PermissionsSet.Add(new EntityPermission(0, contentItem.Id, new [] { ActionDelete.Instance.Letter.ToString() } )); + } + var allowed = true; foreach (var p in permissionsToCheck) { From d38f275467a84a031e89276557b1e81769d362c4 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Wed, 30 Oct 2019 12:15:32 +0000 Subject: [PATCH 011/159] Media uploader in RTE doesn't select uploaded image --- .../common/overlays/mediaPicker/mediapicker.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index fc62c0ce67..136e035b85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -251,7 +251,12 @@ angular.module("umbraco") if (files.length === 1 && $scope.model.selectedImages.length === 0) { var image = $scope.images[$scope.images.length - 1]; $scope.target = image; - $scope.target.url = mediaHelper.resolveFile(image); + // handle both entity and full media object + if (image.image) { + $scope.target.url = image.image; + } else { + $scope.target.url = mediaHelper.resolveFile(image); + } selectImage(image); } }); From 38d241952047f00b8c11ef0662d2b60093dc41f0 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Wed, 30 Oct 2019 12:15:32 +0000 Subject: [PATCH 012/159] Media uploader in RTE doesn't select uploaded image (cherry picked from commit d38f275467a84a031e89276557b1e81769d362c4) --- .../common/overlays/mediaPicker/mediapicker.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index fc62c0ce67..136e035b85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -251,7 +251,12 @@ angular.module("umbraco") if (files.length === 1 && $scope.model.selectedImages.length === 0) { var image = $scope.images[$scope.images.length - 1]; $scope.target = image; - $scope.target.url = mediaHelper.resolveFile(image); + // handle both entity and full media object + if (image.image) { + $scope.target.url = image.image; + } else { + $scope.target.url = mediaHelper.resolveFile(image); + } selectImage(image); } }); From b0e2f2d5113d36049a32c9a9be2523f9964b88a4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Nov 2019 11:51:02 +1100 Subject: [PATCH 013/159] bumps cdf requirements --- build/NuSpecs/UmbracoCms.Core.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 9f507b4915..7f180b4ca0 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -29,8 +29,8 @@ - - + + From 09273b6d6fa37a298941eeb06be195276c2a8af2 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 12 Nov 2019 08:53:35 +0100 Subject: [PATCH 014/159] Remove JS error in console when editing document types with Nested Content properties --- .../propertyeditors/nestedcontent/nestedcontent.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f2a2fc0cc2..6f2b918642 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 @@ -690,7 +690,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop } function updatePropertyActionStates() { - copyAllEntriesAction.isDisabled = $scope.model.value.length === 0; + copyAllEntriesAction.isDisabled = !$scope.model.value || !$scope.model.value.length; } $scope.$watch("currentNode", function (newVal) { From 091b3844aded43fe5847977b7ea88c2c04cb8df8 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 12 Nov 2019 20:34:30 +0100 Subject: [PATCH 015/159] Make sure the media picker can survive a logout and subsequent login --- .../infiniteeditors/mediapicker/mediapicker.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ba103a2761..a27fc757f7 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 @@ -311,7 +311,7 @@ angular.module("umbraco") // also make sure the node is not trashed if (nodePath.indexOf($scope.startNodeId.toString()) !== -1 && node.trashed === false) { - gotoFolder({ id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder", path: node.path }); + gotoFolder({ id: $scope.lastOpenedNode || $scope.startNodeId, name: "Media", icon: "icon-folder", path: node.path }); return true; } else { gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); From 6f67105645e616a8b36dcddb1db7cabd8c9a3c2d Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:25:30 +0100 Subject: [PATCH 016/159] 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 017/159] 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 018/159] 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 019/159] 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 a33d1b4fef4fc30144b2b29a0fd1d273a19ca9dd Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jan 2020 13:04:27 +0100 Subject: [PATCH 020/159] Added UnitTest project --- .../Builders/DictionaryItemBuilder.cs | 10 ++++++ .../Umbraco.Tests.Shared.csproj | 23 +++++++++++++ .../Models/DictionaryItemTests.cs | 18 ++++++++++ .../Umbraco.Tests.UnitTests.csproj | 34 +++++++++++++++++++ src/umbraco.sln | 14 ++++++++ 5 files changed, 99 insertions(+) create mode 100644 src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs new file mode 100644 index 0000000000..0bb16826e8 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Tests.Shared.Builders +{ + public class DictionaryItemBuilder + { + public object Build() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj b/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj new file mode 100644 index 0000000000..65d17b16df --- /dev/null +++ b/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs new file mode 100644 index 0000000000..ac3283df5d --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; +using Umbraco.Tests.Shared.Builders; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models +{ + [TestFixture] + public class DictionaryItemTests + { + [Test] + public void Can_Deep_Clone() + { + var builder = new DictionaryItemBuilder(); + + var item = builder + .Build(); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj new file mode 100644 index 0000000000..8dad14e747 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/umbraco.sln b/src/umbraco.sln index c70f10bdb3..e0f20287bd 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -110,6 +110,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Infrastructure", "U EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Configuration", "Umbraco.Configuration\Umbraco.Configuration.csproj", "{FBE7C065-DAC0-4025-A78B-63B24D3AB00B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.UnitTests", "Umbraco.Tests.UnitTests\Umbraco.Tests.UnitTests.csproj", "{9102ABDF-E537-4E46-B525-C9ED4833EED0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Shared", "Umbraco.Tests.Shared\Umbraco.Tests.Shared.csproj", "{58F1DFC6-5096-4B99-B57B-984F2EEDFADE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -158,6 +162,14 @@ Global {FBE7C065-DAC0-4025-A78B-63B24D3AB00B}.Debug|Any CPU.Build.0 = Debug|Any CPU {FBE7C065-DAC0-4025-A78B-63B24D3AB00B}.Release|Any CPU.ActiveCfg = Release|Any CPU {FBE7C065-DAC0-4025-A78B-63B24D3AB00B}.Release|Any CPU.Build.0 = Release|Any CPU + {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.Build.0 = Release|Any CPU + {58F1DFC6-5096-4B99-B57B-984F2EEDFADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {58F1DFC6-5096-4B99-B57B-984F2EEDFADE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {58F1DFC6-5096-4B99-B57B-984F2EEDFADE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {58F1DFC6-5096-4B99-B57B-984F2EEDFADE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -170,6 +182,8 @@ 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} + {9102ABDF-E537-4E46-B525-C9ED4833EED0} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} + {58F1DFC6-5096-4B99-B57B-984F2EEDFADE} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 997796758e55a736f533c8ed7b72604cb27a7f58 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jan 2020 14:24:20 +0100 Subject: [PATCH 021/159] Added Builders --- .../Builders/BuilderBase.cs | 7 ++ .../Builders/ChildBuilderBase.cs | 19 +++++ .../Builders/DictionaryItemBuilder.cs | 53 +++++++++++++- .../Builders/DictionaryTranslationBuilder.cs | 71 +++++++++++++++++++ .../Builders/LanguageBuilder.cs | 35 +++++++++ .../Umbraco.Tests.Shared.csproj | 10 +++ .../Models/DictionaryItemTests.cs | 55 +++++++++++++- 7 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Tests.Shared/Builders/BuilderBase.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/ChildBuilderBase.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs diff --git a/src/Umbraco.Tests.Shared/Builders/BuilderBase.cs b/src/Umbraco.Tests.Shared/Builders/BuilderBase.cs new file mode 100644 index 0000000000..d026999351 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/BuilderBase.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Tests.Shared.Builders +{ + public abstract class BuilderBase + { + public abstract T Build(); + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/ChildBuilderBase.cs b/src/Umbraco.Tests.Shared/Builders/ChildBuilderBase.cs new file mode 100644 index 0000000000..742b86d16e --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/ChildBuilderBase.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Tests.Shared.Builders +{ + public abstract class ChildBuilderBase : BuilderBase + { + private readonly TParent _parentBuilder; + + protected ChildBuilderBase(TParent parentBuilder) + { + _parentBuilder = parentBuilder; + } + + + public TParent Done() + { + return _parentBuilder; + } + + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs index 0bb16826e8..64f74417a4 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs @@ -1,10 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Umbraco.Core.Models; + namespace Umbraco.Tests.Shared.Builders { public class DictionaryItemBuilder { - public object Build() + private string _itemkey = null; + private readonly List _translationBuilders = new List(); + private DateTime? _createDate; + private DateTime? _updateDate; + private int? _id = null; + + + public DictionaryItem Build() { - throw new System.NotImplementedException(); + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var id = _id ?? 1; + + var result = new DictionaryItem(_itemkey ?? Guid.NewGuid().ToString()); + result.Translations = _translationBuilders.Select(x => x.Build()); + result.CreateDate = createDate; + result.UpdateDate = updateDate; + result.Id = id; + return result; + } + + public DictionaryTranslationBuilder AddTranslation() + { + var builder = new DictionaryTranslationBuilder(this); + + _translationBuilders.Add(builder); + + return builder; + } + + public DictionaryItemBuilder WithCreateData(DateTime createDate) + { + _createDate = createDate; + return this; + } + + public DictionaryItemBuilder WithUpdateData(DateTime updateDate) + { + _updateDate = updateDate; + return this; + } + + public DictionaryItemBuilder WithId(int id) + { + _id = id; + return this; } } } diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs new file mode 100644 index 0000000000..fb82133030 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs @@ -0,0 +1,71 @@ +using System; +using System.Globalization; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Shared.Builders +{ + public class DictionaryTranslationBuilder : ChildBuilderBase + { + private string _value = null; + private Guid? _uniqueId = null; + private DateTime? _createDate; + private DateTime? _updateDate; + + + private LanguageBuilder _languageBuilder; + private int? _id = null; + + public DictionaryTranslationBuilder(DictionaryItemBuilder parentBuilder) : base(parentBuilder) + { + _languageBuilder = new LanguageBuilder(this); + + } + + public override IDictionaryTranslation Build() + { + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var id = _id ?? 1; + + var result = new DictionaryTranslation( + _languageBuilder.Build(), + _value ?? Guid.NewGuid().ToString(), + _uniqueId ?? Guid.NewGuid()); + + result.CreateDate = createDate; + result.UpdateDate = updateDate; + result.Id = id; + + return result; + } + + public LanguageBuilder WithLanguage() + { + return _languageBuilder; + } + + public DictionaryTranslationBuilder WithValue(string value) + { + _value = value; + return this; + } + + public DictionaryTranslationBuilder WithCreateData(DateTime createDate) + { + _createDate = createDate; + return this; + } + + public DictionaryTranslationBuilder WithUpdateData(DateTime updateDate) + { + _updateDate = updateDate; + return this; + } + + public DictionaryTranslationBuilder WithId(int id) + { + _id = id; + return this; + } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs new file mode 100644 index 0000000000..3adc6262a2 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs @@ -0,0 +1,35 @@ +using System.Globalization; +using Moq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Shared.Builders +{ + public class LanguageBuilder : ChildBuilderBase + { + private int? _id = null; + private string _isoCode = null; + + public LanguageBuilder(TParent parentBuilder) : base(parentBuilder) + { + } + + public override ILanguage Build() + { + var culture = CultureInfo.GetCultureInfo("en-US"); + var isoCode = _isoCode ?? culture.Name; + return new Language(Mock.Of(), isoCode) + { + Id = _id ?? 1, + CultureName = culture.TwoLetterISOLanguageName, + IsoCode = new RegionInfo(culture.LCID).Name, + }; + } + + public LanguageBuilder WithId(int id) + { + _id = id; + return this; + } + } +} diff --git a/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj b/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj index 65d17b16df..93afb6f531 100644 --- a/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj +++ b/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj @@ -18,6 +18,16 @@ + + + + + + + + + + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs index ac3283df5d..57ac00c026 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs @@ -1,4 +1,7 @@ +using System.Linq; +using Newtonsoft.Json; using NUnit.Framework; +using Umbraco.Core.Models; using Umbraco.Tests.Shared.Builders; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models @@ -6,13 +9,59 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [TestFixture] public class DictionaryItemTests { + private readonly DictionaryItemBuilder _builder = new DictionaryItemBuilder(); + [Test] public void Can_Deep_Clone() { - var builder = new DictionaryItemBuilder(); - - var item = builder + var item = _builder + .AddTranslation() + .WithValue("Colour") + .Done() + .AddTranslation() + .WithValue("Color") + .Done() .Build(); + + var clone = (DictionaryItem)item.DeepClone(); + + Assert.AreNotSame(clone, item); + Assert.AreEqual(clone, item); + Assert.AreEqual(clone.CreateDate, item.CreateDate); + Assert.AreEqual(clone.Id, item.Id); + Assert.AreEqual(clone.ItemKey, item.ItemKey); + Assert.AreEqual(clone.Key, item.Key); + Assert.AreEqual(clone.ParentId, item.ParentId); + Assert.AreEqual(clone.UpdateDate, item.UpdateDate); + Assert.AreEqual(clone.Translations.Count(), item.Translations.Count()); + for (var i = 0; i < item.Translations.Count(); i++) + { + Assert.AreNotSame(clone.Translations.ElementAt(i), item.Translations.ElementAt(i)); + Assert.AreEqual(clone.Translations.ElementAt(i), item.Translations.ElementAt(i)); + } + + //This double verifies by reflection + var allProps = clone.GetType().GetProperties(); + foreach (var propertyInfo in allProps) + { + Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); + } + + } + + [Test] + public void Can_Serialize_Without_Error() + { + var item = _builder + .AddTranslation() + .WithValue("Colour") + .Done() + .AddTranslation() + .WithValue("Color") + .Done() + .Build(); + + Assert.DoesNotThrow(() => JsonConvert.SerializeObject(item)); } } } From 9d6d43c2d0e66a1ca5b9a31947ed8d76d99c4f7e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jan 2020 14:49:11 +0100 Subject: [PATCH 022/159] Added language tests --- .../Builders/DictionaryItemBuilder.cs | 9 ++++ .../Builders/LanguageBuilder.cs | 8 ++++ .../Models/DictionaryItemTests.cs | 14 +----- .../Models/LanguageTests.cs | 44 +++++++++++++++++++ 4 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs index 64f74417a4..992c197854 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs @@ -55,5 +55,14 @@ namespace Umbraco.Tests.Shared.Builders _id = id; return this; } + + public DictionaryItemBuilder WithRandomTranslations(int count) + { + for (var i = 0; i < count; i++) + { + AddTranslation().Done(); + } + return this; + } } } diff --git a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs index 3adc6262a2..82375434a6 100644 --- a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs @@ -5,6 +5,14 @@ using Umbraco.Core.Models; namespace Umbraco.Tests.Shared.Builders { + public class LanguageBuilder : LanguageBuilder + { + public LanguageBuilder() : base(null) + { + } + } + + public class LanguageBuilder : ChildBuilderBase { private int? _id = null; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs index 57ac00c026..85c0e56089 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DictionaryItemTests.cs @@ -15,12 +15,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models public void Can_Deep_Clone() { var item = _builder - .AddTranslation() - .WithValue("Colour") - .Done() - .AddTranslation() - .WithValue("Color") - .Done() + .WithRandomTranslations(2) .Build(); var clone = (DictionaryItem)item.DeepClone(); @@ -53,12 +48,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models public void Can_Serialize_Without_Error() { var item = _builder - .AddTranslation() - .WithValue("Colour") - .Done() - .AddTranslation() - .WithValue("Color") - .Done() + .WithRandomTranslations(2) .Build(); Assert.DoesNotThrow(() => JsonConvert.SerializeObject(item)); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs new file mode 100644 index 0000000000..57ed20310a --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models +{ + [TestFixture] + public class LanguageTests + { + private readonly LanguageBuilder _builder = new LanguageBuilder(); + + [Test] + public void Can_Deep_Clone() + { + var item = _builder.Build(); + + var clone = (Language) item.DeepClone(); + Assert.AreNotSame(clone, item); + Assert.AreEqual(clone, item); + Assert.AreEqual(clone.CreateDate, item.CreateDate); + Assert.AreEqual(clone.CultureName, item.CultureName); + Assert.AreEqual(clone.Id, item.Id); + Assert.AreEqual(clone.IsoCode, item.IsoCode); + Assert.AreEqual(clone.Key, item.Key); + Assert.AreEqual(clone.UpdateDate, item.UpdateDate); + + //This double verifies by reflection + var allProps = clone.GetType().GetProperties(); + foreach (var propertyInfo in allProps) + { + Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); + } + } + + [Test] + public void Can_Serialize_Without_Error() + { + var item = _builder.Build(); + + Assert.DoesNotThrow(() => JsonConvert.SerializeObject(item)); + } + } +} From 728799b036d751d12f0d5828a11d5ec360da8a1d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jan 2020 15:32:19 +0100 Subject: [PATCH 023/159] using extension methods to enforce consistency --- .../Builders/DataEditorBuilder.cs | 28 ++++++++++ .../Builders/DataTypeBuilder.cs | 33 +++++++++++ .../Builders/DictionaryItemBuilder.cs | 40 +++++++------ .../Builders/DictionaryTranslationBuilder.cs | 21 ++++--- .../Builders/Extensions/BuilderExtensions.cs | 28 ++++++++++ .../Builders/LanguageBuilder.cs | 11 ++-- .../Markers/IWithCreateDateBuilder.cs | 9 +++ .../Builders/Markers/IWithIdBuilder.cs | 7 +++ .../Markers/IWithUpdateDateBuilder.cs | 9 +++ .../Models/DataTypeTests.cs | 56 +++++++++++++++++++ 10 files changed, 205 insertions(+), 37 deletions(-) create mode 100644 src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs diff --git a/src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs new file mode 100644 index 0000000000..5a0b18cf9e --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs @@ -0,0 +1,28 @@ +using Moq; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; + +namespace Umbraco.Tests.Shared.Builders +{ + public class DataEditorBuilder : ChildBuilderBase + { + public DataEditorBuilder(TParent parentBuilder) : base(parentBuilder) + { + } + + public override IDataEditor Build() + { + var result = new DataEditor( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of() + ); + + return result; + } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs new file mode 100644 index 0000000000..128990ab4e --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs @@ -0,0 +1,33 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Shared.Builders +{ + public class DataTypeBuilder : BuilderBase, IWithIdBuilder + { + private readonly DataEditorBuilder _dataEditorBuilder; + private int? _id; + + public DataTypeBuilder() + { + _dataEditorBuilder = new DataEditorBuilder(this); + } + + public override DataType Build() + { + var editor = _dataEditorBuilder.Build(); + var id = _id ?? 1; + var result = new DataType(editor) + { + Id = id + }; + + return result; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs index 992c197854..5f88a636f7 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Umbraco.Core.Models; namespace Umbraco.Tests.Shared.Builders { - public class DictionaryItemBuilder + public class DictionaryItemBuilder : IWithIdBuilder, IWithCreateDateBuilder, IWithUpdateDateBuilder { private string _itemkey = null; private readonly List _translationBuilders = new List(); @@ -14,7 +13,6 @@ namespace Umbraco.Tests.Shared.Builders private DateTime? _updateDate; private int? _id = null; - public DictionaryItem Build() { var createDate = _createDate ?? DateTime.Now; @@ -38,24 +36,6 @@ namespace Umbraco.Tests.Shared.Builders return builder; } - public DictionaryItemBuilder WithCreateData(DateTime createDate) - { - _createDate = createDate; - return this; - } - - public DictionaryItemBuilder WithUpdateData(DateTime updateDate) - { - _updateDate = updateDate; - return this; - } - - public DictionaryItemBuilder WithId(int id) - { - _id = id; - return this; - } - public DictionaryItemBuilder WithRandomTranslations(int count) { for (var i = 0; i < count; i++) @@ -64,5 +44,23 @@ namespace Umbraco.Tests.Shared.Builders } return this; } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + DateTime? IWithCreateDateBuilder.CreateDate + { + get => _createDate; + set => _createDate = value; + } + + DateTime? IWithUpdateDateBuilder.UpdateDate + { + get => _updateDate; + set => _updateDate = value; + } } } diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs index fb82133030..3f471777c4 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs @@ -1,10 +1,9 @@ using System; -using System.Globalization; using Umbraco.Core.Models; namespace Umbraco.Tests.Shared.Builders { - public class DictionaryTranslationBuilder : ChildBuilderBase + public class DictionaryTranslationBuilder : ChildBuilderBase, IWithIdBuilder, IWithCreateDateBuilder, IWithUpdateDateBuilder { private string _value = null; private Guid? _uniqueId = null; @@ -50,22 +49,22 @@ namespace Umbraco.Tests.Shared.Builders return this; } - public DictionaryTranslationBuilder WithCreateData(DateTime createDate) + int? IWithIdBuilder.Id { - _createDate = createDate; - return this; + get => _id; + set => _id = value; } - public DictionaryTranslationBuilder WithUpdateData(DateTime updateDate) + DateTime? IWithCreateDateBuilder.CreateDate { - _updateDate = updateDate; - return this; + get => _createDate; + set => _createDate = value; } - public DictionaryTranslationBuilder WithId(int id) + DateTime? IWithUpdateDateBuilder.UpdateDate { - _id = id; - return this; + get => _updateDate; + set => _updateDate = value; } } } diff --git a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs new file mode 100644 index 0000000000..9b6a961c4f --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs @@ -0,0 +1,28 @@ +using System; + +namespace Umbraco.Tests.Shared.Builders.Extensions +{ + public static class BuilderExtensions + { + public static T WithId(this T builder, int id) + where T : IWithIdBuilder + { + builder.Id = id; + return builder; + } + + public static T WithCreateDate(this T builder, DateTime createDate) + where T : IWithCreateDateBuilder + { + builder.CreateDate = createDate; + return builder; + } + + public static T WithUpdateDate(this T builder, DateTime updateDate) + where T : IWithUpdateDateBuilder + { + builder.UpdateDate = updateDate; + return builder; + } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs index 82375434a6..d3855145f3 100644 --- a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs @@ -13,10 +13,11 @@ namespace Umbraco.Tests.Shared.Builders } - public class LanguageBuilder : ChildBuilderBase + public class LanguageBuilder : ChildBuilderBase, IWithIdBuilder { - private int? _id = null; + private string _isoCode = null; + private int? _id; public LanguageBuilder(TParent parentBuilder) : base(parentBuilder) { @@ -34,10 +35,10 @@ namespace Umbraco.Tests.Shared.Builders }; } - public LanguageBuilder WithId(int id) + int? IWithIdBuilder.Id { - _id = id; - return this; + get => _id; + set => _id = value; } } } diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs new file mode 100644 index 0000000000..3ea3b9e7cc --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Tests.Shared.Builders +{ + public interface IWithCreateDateBuilder + { + DateTime? CreateDate { get; set; } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs new file mode 100644 index 0000000000..7575d2429d --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Tests.Shared.Builders +{ + public interface IWithIdBuilder + { + int? Id { get; set; } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs new file mode 100644 index 0000000000..a0d4fe6258 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Tests.Shared.Builders +{ + public interface IWithUpdateDateBuilder + { + DateTime? UpdateDate { get; set; } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs new file mode 100644 index 0000000000..ecd4249bc6 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders; +using Umbraco.Tests.Shared.Builders.Extensions; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models +{ + [TestFixture] + public class DataTypeTests + { + + private readonly DataTypeBuilder _builder = new DataTypeBuilder(); + [Test] + public void Can_Deep_Clone() + { + var dtd = _builder + .WithId(3123) + .Build(); + + var clone = (DataType) dtd.DeepClone(); + + Assert.AreNotSame(clone, dtd); + Assert.AreEqual(clone, dtd); + Assert.AreEqual(clone.CreateDate, dtd.CreateDate); + Assert.AreEqual(clone.CreatorId, dtd.CreatorId); + Assert.AreEqual(clone.DatabaseType, dtd.DatabaseType); + Assert.AreEqual(clone.Id, dtd.Id); + Assert.AreEqual(clone.Key, dtd.Key); + Assert.AreEqual(clone.Level, dtd.Level); + Assert.AreEqual(clone.Name, dtd.Name); + Assert.AreEqual(clone.ParentId, dtd.ParentId); + Assert.AreEqual(clone.Path, dtd.Path); + Assert.AreEqual(clone.SortOrder, dtd.SortOrder); + Assert.AreEqual(clone.Trashed, dtd.Trashed); + Assert.AreEqual(clone.UpdateDate, dtd.UpdateDate); + + //This double verifies by reflection + var allProps = clone.GetType().GetProperties(); + foreach (var propertyInfo in allProps) + { + Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(dtd, null)); + } + } + + [Test] + public void Can_Serialize_Without_Error() + { + var item = _builder + .Build(); + + Assert.DoesNotThrow(() => JsonConvert.SerializeObject(item)); + } + + } +} From b786dd185b63c24ed63ee7ee6173922ad2b1d6bf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jan 2020 15:48:28 +0100 Subject: [PATCH 024/159] Clean up --- .../Builders/Extensions/BuilderExtensions.cs | 2 +- .../Builders/{Markers => Interfaces}/IWithCreateDateBuilder.cs | 0 .../Builders/{Markers => Interfaces}/IWithIdBuilder.cs | 0 .../Builders/{Markers => Interfaces}/IWithUpdateDateBuilder.cs | 0 .../Umbraco.Infrastructure/Models/DataTypeTests.cs | 1 - 5 files changed, 1 insertion(+), 2 deletions(-) rename src/Umbraco.Tests.Shared/Builders/{Markers => Interfaces}/IWithCreateDateBuilder.cs (100%) rename src/Umbraco.Tests.Shared/Builders/{Markers => Interfaces}/IWithIdBuilder.cs (100%) rename src/Umbraco.Tests.Shared/Builders/{Markers => Interfaces}/IWithUpdateDateBuilder.cs (100%) diff --git a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs index 9b6a961c4f..bab15761c1 100644 --- a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Tests.Shared.Builders.Extensions +namespace Umbraco.Tests.Shared.Builders { public static class BuilderExtensions { diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCreateDateBuilder.cs similarity index 100% rename from src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs rename to src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCreateDateBuilder.cs diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithIdBuilder.cs similarity index 100% rename from src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs rename to src/Umbraco.Tests.Shared/Builders/Interfaces/IWithIdBuilder.cs diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithUpdateDateBuilder.cs similarity index 100% rename from src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs rename to src/Umbraco.Tests.Shared/Builders/Interfaces/IWithUpdateDateBuilder.cs diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs index ecd4249bc6..d845e1b4a0 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs @@ -2,7 +2,6 @@ using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Tests.Shared.Builders; -using Umbraco.Tests.Shared.Builders.Extensions; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models { From 76ed4ccc6c9838de3fea106cb6c5ec2c05bfde83 Mon Sep 17 00:00:00 2001 From: elitsa Date: Thu, 9 Jan 2020 16:37:24 +0100 Subject: [PATCH 025/159] Updating namespaces --- .../Builders/DataTypeBuilder.cs | 1 + .../Builders/DictionaryItemBuilder.cs | 1 + .../Builders/DictionaryTranslationBuilder.cs | 1 + .../Builders/Extensions/BuilderExtensions.cs | 15 +++++++++++++++ .../Builders/LanguageBuilder.cs | 1 + .../Builders/Markers/IWithCreateDateBuilder.cs | 2 +- .../Builders/Markers/IWithIdBuilder.cs | 2 +- .../Builders/Markers/IWithUpdateDateBuilder.cs | 2 +- 8 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs index 128990ab4e..b22b313a75 100644 --- a/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs @@ -1,4 +1,5 @@ using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders.Markers; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs index 5f88a636f7..d978980e2a 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders.Markers; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs index 3f471777c4..d74b0b1730 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs @@ -1,5 +1,6 @@ using System; using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders.Markers; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs index 9b6a961c4f..a307e89cb0 100644 --- a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Tests.Shared.Builders.Markers; namespace Umbraco.Tests.Shared.Builders.Extensions { @@ -24,5 +25,19 @@ namespace Umbraco.Tests.Shared.Builders.Extensions builder.UpdateDate = updateDate; return builder; } + + public static T WithAlias(this T builder, string alias) + where T : IWithAliasBuilder + { + builder.Alias = alias; + return builder; + } + + public static T WithName(this T builder, string name) + where T : IWithNameBuilder + { + builder.Name = name; + return builder; + } } } diff --git a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs index d3855145f3..f4ec69541d 100644 --- a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs @@ -2,6 +2,7 @@ using System.Globalization; using Moq; using Umbraco.Core.Configuration; using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders.Markers; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs index 3ea3b9e7cc..6bc3c8c887 100644 --- a/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithCreateDateBuilder.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Tests.Shared.Builders +namespace Umbraco.Tests.Shared.Builders.Markers { public interface IWithCreateDateBuilder { diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs index 7575d2429d..da93d9ac1f 100644 --- a/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithIdBuilder.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Tests.Shared.Builders +namespace Umbraco.Tests.Shared.Builders.Markers { public interface IWithIdBuilder { diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs index a0d4fe6258..5ced57fa4e 100644 --- a/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithUpdateDateBuilder.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Tests.Shared.Builders +namespace Umbraco.Tests.Shared.Builders.Markers { public interface IWithUpdateDateBuilder { From 3dbe0c2847e4b0ea405eb5ce380f9922aa5273cd Mon Sep 17 00:00:00 2001 From: elitsa Date: Thu, 9 Jan 2020 16:38:31 +0100 Subject: [PATCH 026/159] Fixing indentation --- .../Umbraco.Infrastructure/Models/DataTypeTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs index ecd4249bc6..ce984b3a79 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/DataTypeTests.cs @@ -46,8 +46,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [Test] public void Can_Serialize_Without_Error() { - var item = _builder - .Build(); + var item = _builder.Build(); Assert.DoesNotThrow(() => JsonConvert.SerializeObject(item)); } From 0748fede869a3a9b93a8267abc0c0ef8a20f066a Mon Sep 17 00:00:00 2001 From: elitsa Date: Thu, 9 Jan 2020 16:39:28 +0100 Subject: [PATCH 027/159] Adding tests for RelationType --- .../Builders/Markers/IWithAliasBuilder.cs | 7 +++ .../Builders/Markers/IWithNameBuilder.cs | 7 +++ .../Builders/RelationTypeBuilder.cs | 61 +++++++++++++++++++ .../Models/RelationTypeTests.cs | 50 +++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 src/Umbraco.Tests.Shared/Builders/Markers/IWithAliasBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Markers/IWithNameBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithAliasBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithAliasBuilder.cs new file mode 100644 index 0000000000..1a754ccaaf --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithAliasBuilder.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Tests.Shared.Builders.Markers +{ + public interface IWithAliasBuilder + { + string Alias { get; set; } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/Markers/IWithNameBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Markers/IWithNameBuilder.cs new file mode 100644 index 0000000000..1170e7c446 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Markers/IWithNameBuilder.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Tests.Shared.Builders.Markers +{ + public interface IWithNameBuilder + { + string Name { get; set; } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs b/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs new file mode 100644 index 0000000000..c00690650d --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs @@ -0,0 +1,61 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders.Markers; + +namespace Umbraco.Tests.Shared.Builders +{ + + public class RelationTypeBuilder : RelationTypeBuilder + { + public RelationTypeBuilder() : base(null) + { + } + } + + public class RelationTypeBuilder : ChildBuilderBase, IWithIdBuilder, IWithAliasBuilder, IWithNameBuilder + { + private int? _id; + private string _alias; + private string _name; + private readonly Guid? _parentObjectType = null; + private readonly Guid? _childObjectType = null; + + public RelationTypeBuilder(TParent parentBuilder) : base(parentBuilder) + { + } + + + public override IRelationType Build() + { + var alias = _alias ?? Guid.NewGuid().ToString(); + var name = _name ?? Guid.NewGuid().ToString(); + var parentObjectType = _parentObjectType ?? Guid.NewGuid(); + var childObjectType = _childObjectType ?? Guid.NewGuid(); + var id = _id ?? 1; + + return new RelationType(name, alias, false, parentObjectType, + childObjectType) + { + Id = id + }; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + string IWithAliasBuilder.Alias + { + get => _alias; + set => _alias = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs new file mode 100644 index 0000000000..da255926df --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Tests.Shared.Builders; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models +{ + [TestFixture] + public class RelationTypeTests + { + private readonly RelationTypeBuilder _builder = new RelationTypeBuilder(); + + [Test] + public void Can_Deep_Clone() + { + var item = _builder.Build(); + + var clone = (RelationType) item.DeepClone(); + + Assert.AreNotSame(clone, item); + Assert.AreEqual(clone, item); + Assert.AreEqual(clone.Alias, item.Alias); + Assert.AreEqual(clone.ChildObjectType, item.ChildObjectType); + Assert.AreEqual(clone.IsBidirectional, item.IsBidirectional); + Assert.AreEqual(clone.Id, item.Id); + Assert.AreEqual(clone.Key, item.Key); + Assert.AreEqual(clone.Name, item.Name); + Assert.AreNotSame(clone.ParentObjectType, item.ParentObjectType); + Assert.AreEqual(clone.UpdateDate, item.UpdateDate); + + //This double verifies by reflection + var allProps = clone.GetType().GetProperties(); + foreach (var propertyInfo in allProps) + { + Assert.AreEqual(propertyInfo.GetValue(clone, null), propertyInfo.GetValue(item, null)); + } + } + + [Test] + public void Can_Serialize_Without_Error() + { + var item = _builder.Build(); + + Assert.DoesNotThrow(() => JsonConvert.SerializeObject(item)); + } + } +} From 7656072bd576b45128f275ab2f1189405d698a8a Mon Sep 17 00:00:00 2001 From: elitsa Date: Thu, 9 Jan 2020 16:45:46 +0100 Subject: [PATCH 028/159] Fixing namespaces and usings --- src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs | 2 +- src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs | 2 +- .../Builders/DictionaryTranslationBuilder.cs | 2 +- .../Builders/Extensions/BuilderExtensions.cs | 2 +- .../Builders/Interfaces/IWithAliasBuilder.cs | 2 +- .../Builders/Interfaces/IWithCreateDateBuilder.cs | 2 +- .../Builders/Interfaces/IWithIdBuilder.cs | 2 +- .../Builders/Interfaces/IWithNameBuilder.cs | 2 +- .../Builders/Interfaces/IWithUpdateDateBuilder.cs | 2 +- src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs | 2 +- src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs | 2 +- .../Umbraco.Infrastructure/Models/RelationTypeTests.cs | 5 +---- 12 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs index b22b313a75..ed225d53bd 100644 --- a/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs @@ -1,5 +1,5 @@ using Umbraco.Core.Models; -using Umbraco.Tests.Shared.Builders.Markers; +using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs index d978980e2a..97837f6783 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models; -using Umbraco.Tests.Shared.Builders.Markers; +using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs index d74b0b1730..5d44488bab 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs @@ -1,6 +1,6 @@ using System; using Umbraco.Core.Models; -using Umbraco.Tests.Shared.Builders.Markers; +using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs index a307e89cb0..0180e9e54c 100644 --- a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs @@ -1,5 +1,5 @@ using System; -using Umbraco.Tests.Shared.Builders.Markers; +using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders.Extensions { diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithAliasBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithAliasBuilder.cs index 1a754ccaaf..3ab70123f4 100644 --- a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithAliasBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithAliasBuilder.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Tests.Shared.Builders.Markers +namespace Umbraco.Tests.Shared.Builders.Interfaces { public interface IWithAliasBuilder { diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCreateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCreateDateBuilder.cs index 6bc3c8c887..7a7b69fc95 100644 --- a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCreateDateBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCreateDateBuilder.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Tests.Shared.Builders.Markers +namespace Umbraco.Tests.Shared.Builders.Interfaces { public interface IWithCreateDateBuilder { diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithIdBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithIdBuilder.cs index da93d9ac1f..a5a6175d8f 100644 --- a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithIdBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithIdBuilder.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Tests.Shared.Builders.Markers +namespace Umbraco.Tests.Shared.Builders.Interfaces { public interface IWithIdBuilder { diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithNameBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithNameBuilder.cs index 1170e7c446..2ddcf65333 100644 --- a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithNameBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithNameBuilder.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Tests.Shared.Builders.Markers +namespace Umbraco.Tests.Shared.Builders.Interfaces { public interface IWithNameBuilder { diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithUpdateDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithUpdateDateBuilder.cs index 5ced57fa4e..88a23d0275 100644 --- a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithUpdateDateBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithUpdateDateBuilder.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Tests.Shared.Builders.Markers +namespace Umbraco.Tests.Shared.Builders.Interfaces { public interface IWithUpdateDateBuilder { diff --git a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs index f4ec69541d..4b196e169c 100644 --- a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs @@ -2,7 +2,7 @@ using System.Globalization; using Moq; using Umbraco.Core.Configuration; using Umbraco.Core.Models; -using Umbraco.Tests.Shared.Builders.Markers; +using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs b/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs index c00690650d..55ec30fd77 100644 --- a/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs @@ -1,6 +1,6 @@ using System; using Umbraco.Core.Models; -using Umbraco.Tests.Shared.Builders.Markers; +using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs index da255926df..4054f9d992 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Newtonsoft.Json; +using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Tests.Shared.Builders; From 2b0e9e8450d5f39d0a7e9fcc0c4b4e1303eb0859 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 13 Jan 2020 07:29:12 +0100 Subject: [PATCH 029/159] Added more options on the test builders --- .../Builders/ConfigurationEditorBuilder.cs | 33 +++ .../Builders/DataEditorBuilder.cs | 34 ++- .../Builders/DataTypeBuilder.cs | 138 ++++++++++- .../Builders/DataValueEditorBuilder.cs | 66 ++++++ .../Builders/DictionaryItemBuilder.cs | 105 +++++--- .../Builders/DictionaryTranslationBuilder.cs | 103 ++++---- .../Builders/Extensions/BuilderExtensions.cs | 7 + .../Builders/GlobalSettingsBuilder.cs | 224 ++++++++++++++++++ .../Interfaces/IWithCultureInfoBuilder.cs | 9 + .../Interfaces/IWithDeleteDateBuilder.cs | 9 + .../Builders/Interfaces/IWithKeyBuilder.cs | 9 + .../Builders/LanguageBuilder.cs | 101 +++++++- .../Builders/RelationTypeBuilder.cs | 116 ++++++--- .../Umbraco.Tests.Shared.csproj | 1 + .../Models/LanguageTests.cs | 1 + .../Models/RelationTypeTests.cs | 10 +- 16 files changed, 842 insertions(+), 124 deletions(-) create mode 100644 src/Umbraco.Tests.Shared/Builders/ConfigurationEditorBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/DataValueEditorBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/GlobalSettingsBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCultureInfoBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Interfaces/IWithDeleteDateBuilder.cs create mode 100644 src/Umbraco.Tests.Shared/Builders/Interfaces/IWithKeyBuilder.cs diff --git a/src/Umbraco.Tests.Shared/Builders/ConfigurationEditorBuilder.cs b/src/Umbraco.Tests.Shared/Builders/ConfigurationEditorBuilder.cs new file mode 100644 index 0000000000..3abf683bf2 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/ConfigurationEditorBuilder.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Tests.Shared.Builders +{ + public class ConfigurationEditorBuilder : ChildBuilderBase + { + private IDictionary _defaultConfiguration; + + + public ConfigurationEditorBuilder(TParent parentBuilder) : base(parentBuilder) + { + } + + + public ConfigurationEditorBuilder WithDefaultConfiguration(IDictionary defaultConfiguration) + { + _defaultConfiguration = defaultConfiguration; + return this; + } + + public override IConfigurationEditor Build() + { + var defaultConfiguration = _defaultConfiguration ?? new Dictionary(); + + return new ConfigurationEditor() + { + DefaultConfiguration = defaultConfiguration, + }; + } + + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs index 5a0b18cf9e..0a9e94a74e 100644 --- a/src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DataEditorBuilder.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Moq; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; @@ -8,21 +9,46 @@ namespace Umbraco.Tests.Shared.Builders { public class DataEditorBuilder : ChildBuilderBase { + private readonly ConfigurationEditorBuilder> _explicitConfigurationEditorBuilder; + private readonly DataValueEditorBuilder> _explicitValueEditorBuilder; + private IDictionary _defaultConfiguration; + public DataEditorBuilder(TParent parentBuilder) : base(parentBuilder) { + _explicitConfigurationEditorBuilder = new ConfigurationEditorBuilder>(this); + _explicitValueEditorBuilder = new DataValueEditorBuilder>(this); } + public DataEditorBuilder WithDefaultConfiguration(IDictionary defaultConfiguration) + { + _defaultConfiguration = defaultConfiguration; + return this; + } + + public ConfigurationEditorBuilder> AddExplicitConfigurationEditorBuilder() => + _explicitConfigurationEditorBuilder; + + public DataValueEditorBuilder> AddExplicitValueEditorBuilder() => + _explicitValueEditorBuilder; + public override IDataEditor Build() { - var result = new DataEditor( + var defaultConfiguration = _defaultConfiguration ?? new Dictionary(); + var explicitConfigurationEditor = _explicitConfigurationEditorBuilder.Build(); + var explicitValueEditor = _explicitValueEditorBuilder.Build(); + + return new DataEditor( Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of() - ); - - return result; + ) + { + DefaultConfiguration = defaultConfiguration, + ExplicitConfigurationEditor = explicitConfigurationEditor, + ExplicitValueEditor = explicitValueEditor + }; } } } diff --git a/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs index ed225d53bd..62e9e35ee0 100644 --- a/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DataTypeBuilder.cs @@ -1,28 +1,124 @@ +using System; using Umbraco.Core.Models; using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { - public class DataTypeBuilder : BuilderBase, IWithIdBuilder + public class DataTypeBuilder + : BuilderBase, + IWithIdBuilder, + IWithKeyBuilder, + IWithCreateDateBuilder, + IWithUpdateDateBuilder, + IWithDeleteDateBuilder, + IWithNameBuilder { private readonly DataEditorBuilder _dataEditorBuilder; private int? _id; + private int? _parentId; + private Guid? _key; + private DateTime? _createDate; + private DateTime? _updateDate; + private DateTime? _deleteDate; + private string _name; + private bool? _trashed; + // private object _configuration; + private int? _level; + private string _path; + private int? _creatorId; + private ValueStorageType? _databaseType; + private int? _sortOrder; public DataTypeBuilder() { _dataEditorBuilder = new DataEditorBuilder(this); } + public DataTypeBuilder WithParentId(int parentId) + { + _parentId = parentId; + return this; + } + + public DataTypeBuilder WithTrashed(bool trashed) + { + _trashed = trashed; + return this; + } + + // public DataTypeBuilder WithConfiguration(object configuration) + // { + // _configuration = configuration; + // return this; + // } + + public DataTypeBuilder WithLevel(int level) + { + _level = level; + return this; + } + + public DataTypeBuilder WithPath(string path) + { + _path = path; + return this; + } + + public DataTypeBuilder WithCreatorId(int creatorId) + { + _creatorId = creatorId; + return this; + } + + public DataTypeBuilder WithDatabaseType(ValueStorageType databaseType) + { + _databaseType = databaseType; + return this; + } + + public DataTypeBuilder WithSortOrder(int sortOrder) + { + _sortOrder = sortOrder; + return this; + } + + public DataEditorBuilder AddEditor() + { + return _dataEditorBuilder; + } + public override DataType Build() { var editor = _dataEditorBuilder.Build(); + var parentId = _parentId ?? -1; var id = _id ?? 1; - var result = new DataType(editor) - { - Id = id - }; + var key = _key ?? Guid.NewGuid(); + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; + var name = _name ?? Guid.NewGuid().ToString(); + // var configuration = _configuration ?? editor.GetConfigurationEditor().DefaultConfigurationObject; + var level = _level ?? 0; + var path = _path ?? string.Empty; + var creatorId = _creatorId ?? 1; + var databaseType = _databaseType ?? ValueStorageType.Ntext; + var sortOrder = _sortOrder ?? 0; - return result; + return new DataType(editor, parentId) + { + Id = id, + Key = key, + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate, + Name = name, + Trashed = _trashed ?? false, + Level = level, + Path = path, + CreatorId = creatorId, + DatabaseType = databaseType, + SortOrder = sortOrder, + }; } int? IWithIdBuilder.Id @@ -30,5 +126,35 @@ namespace Umbraco.Tests.Shared.Builders get => _id; set => _id = value; } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + DateTime? IWithCreateDateBuilder.CreateDate + { + get => _createDate; + set => _createDate = value; + } + + DateTime? IWithUpdateDateBuilder.UpdateDate + { + get => _updateDate; + set => _updateDate = value; + } + + DateTime? IWithDeleteDateBuilder.DeleteDate + { + get => _deleteDate; + set => _deleteDate = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } } } diff --git a/src/Umbraco.Tests.Shared/Builders/DataValueEditorBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DataValueEditorBuilder.cs new file mode 100644 index 0000000000..be26510934 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/DataValueEditorBuilder.cs @@ -0,0 +1,66 @@ +using System; +using Moq; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; + +namespace Umbraco.Tests.Shared.Builders +{ + public class DataValueEditorBuilder : ChildBuilderBase + { + private string _configuration; + private string _view; + private bool? _hideLabel; + private string _valueType; + + + public DataValueEditorBuilder(TParent parentBuilder) : base(parentBuilder) + { + } + + public DataValueEditorBuilder WithConfiguration(string configuration) + { + _configuration = configuration; + return this; + } + + public DataValueEditorBuilder WithView(string view) + { + _view = view; + return this; + } + + public DataValueEditorBuilder WithHideLabel(bool hideLabel) + { + _hideLabel = hideLabel; + return this; + } + + public DataValueEditorBuilder WithValueType(string valueType) + { + _valueType = valueType; + return this; + } + + public override IDataValueEditor Build() + { + var configuration = _configuration ?? null; + var view = _view ?? null; + var hideLabel = _hideLabel ?? false; + var valueType = _valueType ?? Guid.NewGuid().ToString(); + + return new DataValueEditor( + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of() + ) + { + Configuration = configuration, + View = view, + HideLabel = hideLabel, + ValueType = valueType, + }; + } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs index 97837f6783..6baff80972 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryItemBuilder.cs @@ -6,28 +6,90 @@ using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { - public class DictionaryItemBuilder : IWithIdBuilder, IWithCreateDateBuilder, IWithUpdateDateBuilder + public class DictionaryItemBuilder + : BuilderBase, + IWithIdBuilder, + IWithCreateDateBuilder, + IWithUpdateDateBuilder, + IWithDeleteDateBuilder, + IWithKeyBuilder { - private string _itemkey = null; - private readonly List _translationBuilders = new List(); - private DateTime? _createDate; - private DateTime? _updateDate; - private int? _id = null; + private readonly List _translationBuilders = + new List(); - public DictionaryItem Build() + private DateTime? _createDate; + private DateTime? _deleteDate; + private int? _id; + private string _itemKey; + private Guid? _key; + private Guid? _parentId; + private DateTime? _updateDate; + + DateTime? IWithCreateDateBuilder.CreateDate + { + get => _createDate; + set => _createDate = value; + } + + DateTime? IWithDeleteDateBuilder.DeleteDate + { + get => _deleteDate; + set => _deleteDate = value; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + DateTime? IWithUpdateDateBuilder.UpdateDate + { + get => _updateDate; + set => _updateDate = value; + } + + public override DictionaryItem Build() { var createDate = _createDate ?? DateTime.Now; var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; var id = _id ?? 1; + var key = _key ?? Guid.NewGuid(); + var parentId = _parentId ?? null; + var itemKey = _itemKey ?? Guid.NewGuid().ToString(); - var result = new DictionaryItem(_itemkey ?? Guid.NewGuid().ToString()); - result.Translations = _translationBuilders.Select(x => x.Build()); - result.CreateDate = createDate; - result.UpdateDate = updateDate; - result.Id = id; + var result = new DictionaryItem(itemKey) + { + Translations = _translationBuilders.Select(x => x.Build()), + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate, + Id = id, + ParentId = parentId, + Key = key, + }; return result; } + public DictionaryItemBuilder WithParentId(Guid parentId) + { + _parentId = parentId; + return this; + } + + public DictionaryItemBuilder WithItemKey(string itemKey) + { + _itemKey = itemKey; + return this; + } + public DictionaryTranslationBuilder AddTranslation() { var builder = new DictionaryTranslationBuilder(this); @@ -43,25 +105,8 @@ namespace Umbraco.Tests.Shared.Builders { AddTranslation().Done(); } + return this; } - - int? IWithIdBuilder.Id - { - get => _id; - set => _id = value; - } - - DateTime? IWithCreateDateBuilder.CreateDate - { - get => _createDate; - set => _createDate = value; - } - - DateTime? IWithUpdateDateBuilder.UpdateDate - { - get => _updateDate; - set => _updateDate = value; - } } } diff --git a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs index 5d44488bab..bd596383d1 100644 --- a/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/DictionaryTranslationBuilder.cs @@ -4,56 +4,27 @@ using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { - public class DictionaryTranslationBuilder : ChildBuilderBase, IWithIdBuilder, IWithCreateDateBuilder, IWithUpdateDateBuilder + public class DictionaryTranslationBuilder + : ChildBuilderBase, + IWithIdBuilder, + IWithCreateDateBuilder, + IWithUpdateDateBuilder, + IWithDeleteDateBuilder, + IWithKeyBuilder { - private string _value = null; - private Guid? _uniqueId = null; + private readonly LanguageBuilder _languageBuilder; + private readonly Guid? _uniqueId = null; private DateTime? _createDate; + private DateTime? _deleteDate; + private int? _id; + private Guid? _key; private DateTime? _updateDate; + private string _value; - private LanguageBuilder _languageBuilder; - private int? _id = null; - public DictionaryTranslationBuilder(DictionaryItemBuilder parentBuilder) : base(parentBuilder) { _languageBuilder = new LanguageBuilder(this); - - } - - public override IDictionaryTranslation Build() - { - var createDate = _createDate ?? DateTime.Now; - var updateDate = _updateDate ?? DateTime.Now; - var id = _id ?? 1; - - var result = new DictionaryTranslation( - _languageBuilder.Build(), - _value ?? Guid.NewGuid().ToString(), - _uniqueId ?? Guid.NewGuid()); - - result.CreateDate = createDate; - result.UpdateDate = updateDate; - result.Id = id; - - return result; - } - - public LanguageBuilder WithLanguage() - { - return _languageBuilder; - } - - public DictionaryTranslationBuilder WithValue(string value) - { - _value = value; - return this; - } - - int? IWithIdBuilder.Id - { - get => _id; - set => _id = value; } DateTime? IWithCreateDateBuilder.CreateDate @@ -62,10 +33,58 @@ namespace Umbraco.Tests.Shared.Builders set => _createDate = value; } + DateTime? IWithDeleteDateBuilder.DeleteDate + { + get => _deleteDate; + set => _deleteDate = value; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + DateTime? IWithUpdateDateBuilder.UpdateDate { get => _updateDate; set => _updateDate = value; } + + public override IDictionaryTranslation Build() + { + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; + var id = _id ?? 1; + var key = _key ?? Guid.NewGuid(); + + var result = new DictionaryTranslation( + _languageBuilder.Build(), + _value ?? Guid.NewGuid().ToString(), + _uniqueId ?? key) + { + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate, + Id = id + }; + + return result; + } + + public LanguageBuilder AddLanguage() => _languageBuilder; + + public DictionaryTranslationBuilder WithValue(string value) + { + _value = value; + return this; + } } } diff --git a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs index 0180e9e54c..702bb19bb2 100644 --- a/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Shared/Builders/Extensions/BuilderExtensions.cs @@ -39,5 +39,12 @@ namespace Umbraco.Tests.Shared.Builders.Extensions builder.Name = name; return builder; } + + public static T WithKey(this T builder, Guid key) + where T : IWithKeyBuilder + { + builder.Key = key; + return builder; + } } } diff --git a/src/Umbraco.Tests.Shared/Builders/GlobalSettingsBuilder.cs b/src/Umbraco.Tests.Shared/Builders/GlobalSettingsBuilder.cs new file mode 100644 index 0000000000..b3c8e90fa5 --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/GlobalSettingsBuilder.cs @@ -0,0 +1,224 @@ +using Umbraco.Core.Configuration; + +namespace Umbraco.Tests.Shared.Builders +{ + public class GlobalSettingsBuilder : GlobalSettingsBuilder + { + public GlobalSettingsBuilder() : base(null) + { + } + } + + public class GlobalSettingsBuilder : ChildBuilderBase + + { + private string _configurationStatus; + private string _databaseFactoryServerVersion; + private string _defaultUiLanguage; + private bool? _disableElectionForSingleServer; + private bool? _hideTopLevelNodeFromPath; + private bool? _installEmptyDatabase; + private bool? _installMissingDatabase; + private bool? _isSmtpServerConfigured; + private string _path; + private string _registerType; + private string _reservedPaths; + private string _reservedUrls; + private int? _timeOutInMinutes; + private string _umbracoCssPath; + private string _umbracoMediaPath; + private string _umbracoPath; + private string _umbracoScriptsPath; + private bool? _useHttps; + private int? _versionCheckPeriod; + + + public GlobalSettingsBuilder(TParent parentBuilder) : base(parentBuilder) + { + } + + public GlobalSettingsBuilder WithConfigurationStatus(string configurationStatus) + { + _configurationStatus = configurationStatus; + return this; + } + + public GlobalSettingsBuilder WithDatabaseFactoryServerVersion(string databaseFactoryServerVersion) + { + _databaseFactoryServerVersion = databaseFactoryServerVersion; + return this; + } + + public GlobalSettingsBuilder WithDefaultUiLanguage(string defaultUiLanguage) + { + _defaultUiLanguage = defaultUiLanguage; + return this; + } + + public GlobalSettingsBuilder WithDisableElectionForSingleServer(bool disableElectionForSingleServer) + { + _disableElectionForSingleServer = disableElectionForSingleServer; + return this; + } + + public GlobalSettingsBuilder WithHideTopLevelNodeFromPath(bool hideTopLevelNodeFromPath) + { + _hideTopLevelNodeFromPath = hideTopLevelNodeFromPath; + return this; + } + + public GlobalSettingsBuilder WithInstallEmptyDatabase(bool installEmptyDatabase) + { + _installEmptyDatabase = installEmptyDatabase; + return this; + } + + public GlobalSettingsBuilder WithInstallMissingDatabase(bool installMissingDatabase) + { + _installMissingDatabase = installMissingDatabase; + return this; + } + + public GlobalSettingsBuilder WithIsSmtpServerConfigured(bool isSmtpServerConfigured) + { + _isSmtpServerConfigured = isSmtpServerConfigured; + return this; + } + + public GlobalSettingsBuilder WithPath(string path) + { + _path = path; + return this; + } + + public GlobalSettingsBuilder WithRegisterType(string registerType) + { + _registerType = registerType; + return this; + } + + public GlobalSettingsBuilder WithReservedPaths(string reservedPaths) + { + _reservedPaths = reservedPaths; + return this; + } + + public GlobalSettingsBuilder WithReservedUrls(string reservedUrls) + { + _reservedUrls = reservedUrls; + return this; + } + + public GlobalSettingsBuilder WithUmbracoPath(string umbracoPath) + { + _umbracoPath = umbracoPath; + return this; + } + + public GlobalSettingsBuilder WithUseHttps(bool useHttps) + { + _useHttps = useHttps; + return this; + } + + public GlobalSettingsBuilder WithUmbracoCssPath(string umbracoCssPath) + { + _umbracoCssPath = umbracoCssPath; + return this; + } + + public GlobalSettingsBuilder WithUmbracoMediaPath(string umbracoMediaPath) + { + _umbracoMediaPath = umbracoMediaPath; + return this; + } + + public GlobalSettingsBuilder WithUmbracoScriptsPath(string umbracoScriptsPath) + { + _umbracoScriptsPath = umbracoScriptsPath; + return this; + } + + public GlobalSettingsBuilder WithVersionCheckPeriod(int versionCheckPeriod) + { + _versionCheckPeriod = versionCheckPeriod; + return this; + } + + public GlobalSettingsBuilder WithTimeOutInMinutes(int timeOutInMinutes) + { + _timeOutInMinutes = timeOutInMinutes; + return this; + } + + public override IGlobalSettings Build() + { + var configurationStatus = _configurationStatus ?? "9.0.0"; + var databaseFactoryServerVersion = _databaseFactoryServerVersion ?? null; + var defaultUiLanguage = _defaultUiLanguage ?? "en"; + var disableElectionForSingleServer = _disableElectionForSingleServer ?? false; + var hideTopLevelNodeFromPath = _hideTopLevelNodeFromPath ?? false; + var installEmptyDatabase = _installEmptyDatabase ?? false; + var installMissingDatabase = _installMissingDatabase ?? false; + var isSmtpServerConfigured = _isSmtpServerConfigured ?? false; + var path = _path ?? "/umbraco"; + var registerType = _registerType ?? null; + var reservedPaths = _reservedPaths ?? "~/app_plugins/,~/install/,~/mini-profiler-resources/,"; + var reservedUrls = _reservedUrls ?? "~/config/splashes/noNodes.aspx,~/.well-known,"; + var umbracoPath = _umbracoPath ?? "~/umbraco"; + var useHttps = _useHttps ?? false; + var umbracoCssPath = _umbracoCssPath ?? "~/css"; + var umbracoMediaPath = _umbracoMediaPath ?? "~/media"; + var umbracoScriptsPath = _umbracoScriptsPath ?? "~/scripts"; + var versionCheckPeriod = _versionCheckPeriod ?? 0; + var timeOutInMinutes = _timeOutInMinutes ?? 20; + + + return new TestGlobalSettings + { + ConfigurationStatus = configurationStatus, + DatabaseFactoryServerVersion = databaseFactoryServerVersion, + DefaultUILanguage = defaultUiLanguage, + DisableElectionForSingleServer = disableElectionForSingleServer, + HideTopLevelNodeFromPath = hideTopLevelNodeFromPath, + InstallEmptyDatabase = installEmptyDatabase, + InstallMissingDatabase = installMissingDatabase, + IsSmtpServerConfigured = isSmtpServerConfigured, + Path = path, + RegisterType = registerType, + ReservedPaths = reservedPaths, + ReservedUrls = reservedUrls, + UmbracoPath = umbracoPath, + UseHttps = useHttps, + UmbracoCssPath = umbracoCssPath, + UmbracoMediaPath = umbracoMediaPath, + UmbracoScriptsPath = umbracoScriptsPath, + VersionCheckPeriod = versionCheckPeriod, + TimeOutInMinutes = timeOutInMinutes + }; + } + + private class TestGlobalSettings : IGlobalSettings + { + public string ReservedUrls { get; set; } + public string ReservedPaths { get; set; } + public string Path { get; set; } + public string ConfigurationStatus { get; set; } + public int TimeOutInMinutes { get; set; } + public string DefaultUILanguage { get; set; } + public bool HideTopLevelNodeFromPath { get; set; } + public bool UseHttps { get; set; } + public int VersionCheckPeriod { get; set; } + public string UmbracoPath { get; set; } + public string UmbracoCssPath { get; set; } + public string UmbracoScriptsPath { get; set; } + public string UmbracoMediaPath { get; set; } + public bool IsSmtpServerConfigured { get; set; } + public bool InstallMissingDatabase { get; set; } + public bool InstallEmptyDatabase { get; set; } + public bool DisableElectionForSingleServer { get; set; } + public string RegisterType { get; set; } + public string DatabaseFactoryServerVersion { get; set; } + } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCultureInfoBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCultureInfoBuilder.cs new file mode 100644 index 0000000000..ee33d4549d --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithCultureInfoBuilder.cs @@ -0,0 +1,9 @@ +using System.Globalization; + +namespace Umbraco.Tests.Shared.Builders.Interfaces +{ + public interface IWithCultureInfoBuilder + { + CultureInfo CultureInfo { get; set; } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithDeleteDateBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithDeleteDateBuilder.cs new file mode 100644 index 0000000000..3ae8ddfeed --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithDeleteDateBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Tests.Shared.Builders.Interfaces +{ + public interface IWithDeleteDateBuilder + { + DateTime? DeleteDate { get; set; } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithKeyBuilder.cs b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithKeyBuilder.cs new file mode 100644 index 0000000000..78da4dbc0b --- /dev/null +++ b/src/Umbraco.Tests.Shared/Builders/Interfaces/IWithKeyBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Tests.Shared.Builders.Interfaces +{ + public interface IWithKeyBuilder + { + Guid? Key { get; set; } + } +} diff --git a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs index 4b196e169c..59e859dd3a 100644 --- a/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/LanguageBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Globalization; using Moq; using Umbraco.Core.Configuration; @@ -13,27 +14,45 @@ namespace Umbraco.Tests.Shared.Builders } } - - public class LanguageBuilder : ChildBuilderBase, IWithIdBuilder + public class LanguageBuilder + : ChildBuilderBase, + IWithIdBuilder, + IWithKeyBuilder, + IWithCreateDateBuilder, + IWithUpdateDateBuilder, + IWithDeleteDateBuilder, + IWithCultureInfoBuilder { - - private string _isoCode = null; + private DateTime? _createDate; + private CultureInfo _cultureInfo; + private DateTime? _deleteDate; + private int? _fallbackLanguageId; private int? _id; + private bool? _isDefault; + private bool? _isMandatory; + private Guid? _key; + private DateTime? _updateDate; public LanguageBuilder(TParent parentBuilder) : base(parentBuilder) { } - public override ILanguage Build() + DateTime? IWithCreateDateBuilder.CreateDate { - var culture = CultureInfo.GetCultureInfo("en-US"); - var isoCode = _isoCode ?? culture.Name; - return new Language(Mock.Of(), isoCode) - { - Id = _id ?? 1, - CultureName = culture.TwoLetterISOLanguageName, - IsoCode = new RegionInfo(culture.LCID).Name, - }; + get => _createDate; + set => _createDate = value; + } + + CultureInfo IWithCultureInfoBuilder.CultureInfo + { + get => _cultureInfo; + set => _cultureInfo = value; + } + + DateTime? IWithDeleteDateBuilder.DeleteDate + { + get => _deleteDate; + set => _deleteDate = value; } int? IWithIdBuilder.Id @@ -41,5 +60,61 @@ namespace Umbraco.Tests.Shared.Builders get => _id; set => _id = value; } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + DateTime? IWithUpdateDateBuilder.UpdateDate + { + get => _updateDate; + set => _updateDate = value; + } + + public override ILanguage Build() + { + var cultureInfo = _cultureInfo ?? CultureInfo.GetCultureInfo("en-US"); + var key = _key ?? Guid.NewGuid(); + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; + var fallbackLanguageId = _fallbackLanguageId ?? null; + var isDefault = _isDefault ?? false; + var isMandatory = _isMandatory ?? false; + + return new Language(Mock.Of(), cultureInfo.Name) + { + Id = _id ?? 1, + CultureName = cultureInfo.TwoLetterISOLanguageName, + IsoCode = new RegionInfo(cultureInfo.LCID).Name, + Key = key, + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate, + IsDefault = isDefault, + IsMandatory = isMandatory, + FallbackLanguageId = fallbackLanguageId + }; + } + + public LanguageBuilder WithIsDefault(bool isDefault) + { + _isDefault = isDefault; + return this; + } + + public LanguageBuilder WithIsMandatory(bool isMandatory) + { + _isMandatory = isMandatory; + return this; + } + + public LanguageBuilder WithFallbackLanguageId(int fallbackLanguageId) + { + _fallbackLanguageId = fallbackLanguageId; + return this; + } } } diff --git a/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs b/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs index 55ec30fd77..2cb81688b7 100644 --- a/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs +++ b/src/Umbraco.Tests.Shared/Builders/RelationTypeBuilder.cs @@ -4,7 +4,6 @@ using Umbraco.Tests.Shared.Builders.Interfaces; namespace Umbraco.Tests.Shared.Builders { - public class RelationTypeBuilder : RelationTypeBuilder { public RelationTypeBuilder() : base(null) @@ -12,39 +11,30 @@ namespace Umbraco.Tests.Shared.Builders } } - public class RelationTypeBuilder : ChildBuilderBase, IWithIdBuilder, IWithAliasBuilder, IWithNameBuilder + public class RelationTypeBuilder + : ChildBuilderBase, + IWithIdBuilder, + IWithAliasBuilder, + IWithNameBuilder, + IWithKeyBuilder, + IWithCreateDateBuilder, + IWithUpdateDateBuilder, + IWithDeleteDateBuilder { - private int? _id; private string _alias; + private Guid? _childObjectType; + private DateTime? _createDate; + private DateTime? _deleteDate; + private int? _id; + private bool? _isBidirectional; + private Guid? _key; private string _name; - private readonly Guid? _parentObjectType = null; - private readonly Guid? _childObjectType = null; + private Guid? _parentObjectType; + private DateTime? _updateDate; public RelationTypeBuilder(TParent parentBuilder) : base(parentBuilder) { } - - - public override IRelationType Build() - { - var alias = _alias ?? Guid.NewGuid().ToString(); - var name = _name ?? Guid.NewGuid().ToString(); - var parentObjectType = _parentObjectType ?? Guid.NewGuid(); - var childObjectType = _childObjectType ?? Guid.NewGuid(); - var id = _id ?? 1; - - return new RelationType(name, alias, false, parentObjectType, - childObjectType) - { - Id = id - }; - } - - int? IWithIdBuilder.Id - { - get => _id; - set => _id = value; - } string IWithAliasBuilder.Alias { @@ -52,10 +42,82 @@ namespace Umbraco.Tests.Shared.Builders set => _alias = value; } + DateTime? IWithCreateDateBuilder.CreateDate + { + get => _createDate; + set => _createDate = value; + } + + DateTime? IWithDeleteDateBuilder.DeleteDate + { + get => _deleteDate; + set => _deleteDate = value; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + string IWithNameBuilder.Name { get => _name; set => _name = value; } + + DateTime? IWithUpdateDateBuilder.UpdateDate + { + get => _updateDate; + set => _updateDate = value; + } + + public override IRelationType Build() + { + var alias = _alias ?? Guid.NewGuid().ToString(); + var name = _name ?? Guid.NewGuid().ToString(); + var parentObjectType = _parentObjectType ?? null; + var childObjectType = _childObjectType ?? null; + var id = _id ?? 1; + var key = _key ?? Guid.NewGuid(); + var isBidirectional = _isBidirectional ?? false; + var createDate = _createDate ?? DateTime.Now; + var updateDate = _updateDate ?? DateTime.Now; + var deleteDate = _deleteDate ?? null; + + return new RelationType(name, alias, isBidirectional, parentObjectType, + childObjectType) + { + Id = id, + Key = key, + CreateDate = createDate, + UpdateDate = updateDate, + DeleteDate = deleteDate + }; + } + + public RelationTypeBuilder WithIsBidirectional(bool isBidirectional) + { + _isBidirectional = isBidirectional; + return this; + } + + public RelationTypeBuilder WithChildObjectType(Guid childObjectType) + { + _childObjectType = childObjectType; + return this; + } + + public RelationTypeBuilder WithParentObjectType(Guid parentObjectType) + { + _parentObjectType = parentObjectType; + return this; + } } } diff --git a/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj b/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj index 93afb6f531..d18779a4fd 100644 --- a/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj +++ b/src/Umbraco.Tests.Shared/Umbraco.Tests.Shared.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs index 57ed20310a..6d80eac16f 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/LanguageTests.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Tests.Shared.Builders; +using Umbraco.Tests.Shared.Builders.Extensions; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs index 4054f9d992..140b974b61 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Models/RelationTypeTests.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Tests.Shared.Builders; @@ -13,7 +14,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models [Test] public void Can_Deep_Clone() { - var item = _builder.Build(); + var item = _builder + .WithParentObjectType(Guid.NewGuid()) + .WithChildObjectType(Guid.NewGuid()) + .Build(); var clone = (RelationType) item.DeepClone(); @@ -21,11 +25,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Models Assert.AreEqual(clone, item); Assert.AreEqual(clone.Alias, item.Alias); Assert.AreEqual(clone.ChildObjectType, item.ChildObjectType); + Assert.AreEqual(clone.ParentObjectType, item.ParentObjectType); Assert.AreEqual(clone.IsBidirectional, item.IsBidirectional); Assert.AreEqual(clone.Id, item.Id); Assert.AreEqual(clone.Key, item.Key); Assert.AreEqual(clone.Name, item.Name); Assert.AreNotSame(clone.ParentObjectType, item.ParentObjectType); + Assert.AreNotSame(clone.ChildObjectType, item.ChildObjectType); Assert.AreEqual(clone.UpdateDate, item.UpdateDate); //This double verifies by reflection From d4e6eb2b6b052b79d34d095f2405b21f15618ede Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Jan 2020 17:04:38 +0100 Subject: [PATCH 030/159] 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 889f0fc08553c5e6ef47c70b81181bfb3517bf88 Mon Sep 17 00:00:00 2001 From: elitsa Date: Mon, 3 Feb 2020 11:39:11 +0100 Subject: [PATCH 031/159] 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 3bfc2514301b14f03a6a9e6ac3c1cf515b838b84 Mon Sep 17 00:00:00 2001 From: elitsa Date: Mon, 3 Feb 2020 11:39:11 +0100 Subject: [PATCH 032/159] 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 9c428b7f0194197ab235c5799bb237d35b8b3a10 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 12 Feb 2020 11:55:51 +0000 Subject: [PATCH 033/159] 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 034/159] 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 721d41da62846374510ec9dfdc18c78637c8a175 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth <322383+benjaminhowarth1@users.noreply.github.com> Date: Wed, 12 Feb 2020 17:01:59 +0000 Subject: [PATCH 035/159] Update PropertyType.cs Please, *please* stop making things internal when it breaks legacy behaviour (such as obtaining properties by tab group, which is precisely what this allows for). --- src/Umbraco.Core/Models/PropertyType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index fd23756acb..bcafd549f0 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -178,7 +178,7 @@ namespace Umbraco.Core.Models /// /// For generic properties, the value is null. [DataMember] - internal Lazy PropertyGroupId + public Lazy PropertyGroupId { get => _propertyGroupId; set => SetPropertyValueAndDetectChanges(value, ref _propertyGroupId, nameof(PropertyGroupId)); From 7d0733f40d4c698742af6bd03e3891922cc074ce Mon Sep 17 00:00:00 2001 From: Anders Bjerner Date: Mon, 24 Feb 2020 20:36:38 +0100 Subject: [PATCH 036/159] Making DataEditor.GetValueEditor method virtual --- src/Umbraco.Core/PropertyEditors/DataEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 7dc260e4c7..c749c61300 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -105,7 +105,7 @@ namespace Umbraco.Core.PropertyEditors /// Technically, it could be cached by datatype but let's keep things /// simple enough for now. /// - public IDataValueEditor GetValueEditor(object configuration) + public virtual IDataValueEditor GetValueEditor(object configuration) { // if an explicit value editor has been set (by the manifest parser) // then return it, and ignore the configuration, which is going to be From 4dc5b02b89fdfa019b30eaaa6ff41f65010611ab Mon Sep 17 00:00:00 2001 From: Matthew-Wise <6782865+Matthew-Wise@users.noreply.github.com> Date: Wed, 26 Feb 2020 13:44:57 +0000 Subject: [PATCH 037/159] Changed showPage to Save and publish updated button style to link (#7649) * Changed showPage to Save and publish updated button style to link * Change language key from showPage to saveAndPreview --- .../src/common/mocks/services/localization.mocks.js | 2 +- .../src/views/components/content/edit.html | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/de.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/es.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/he.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/it.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml | 2 +- 22 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index 3874ff9bf6..ea7f3a6d4c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -84,7 +84,7 @@ angular.module('umbraco.mocks'). "buttons_save": "Save", "buttons_saveAndPublish": "Save and publish", "buttons_saveToPublish": "Save and send for approval", - "buttons_showPage": "Preview", + "buttons_saveAndPreview": "Save and preview", "buttons_showPageDisabled": "Preview is disabled because there's no template assigned", "buttons_styleChoose": "Choose style", "buttons_styleShow": "Show styles", 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..e1eb5e454a 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 @@ -45,9 +45,9 @@ alias="preview" ng-if="!page.isNew && content.allowPreview && page.showPreviewButton" type="button" - button-style="info" + button-style="link" action="preview(content)" - label-key="buttons_showPage"> + label-key="buttons_saveAndPreview"> Uložit Uložit a publikovat Uložit a odeslat ke schválení - Náhled + Náhled Náhled je deaktivován, protože není přiřazena žádná šablona Vybrat styl Zobrazit styly diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index ae8925e911..85eaa34319 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -139,7 +139,7 @@ Gem og send til udgivelse Gem listevisning Planlæg - Forhåndsvisning + Forhåndsvisning Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt Vælg formattering Vis koder diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml index 9c02879db9..529074f24d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml @@ -140,7 +140,7 @@ Speichern und zur Abnahme übergeben Listenansicht sichern Veröffentlichung planen - Vorschau + Vorschau Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist Stil auswählen Stil anzeigen diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index a85df5714b..0a372dbcb5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -141,7 +141,7 @@ Save and send for approval Save list view Schedule - Preview + Save and preview Preview is disabled because there's no template assigned Choose style Show styles 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 d14fb03727..482973f5e7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -139,7 +139,7 @@ Send for approval Save list view Schedule - Preview + Save and preview Preview is disabled because there's no template assigned Choose style Show styles diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml index 371e6de7a2..c35c84ebdc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml @@ -124,7 +124,7 @@ Guardar y publicar Guardar y enviar para aprobación Guardar vista de lista - Previsualizar + Previsualizar La previsualización está deshabilitada porque no hay ninguna plantilla asignada Elegir estilo Mostrar estilos diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml index e3cfa32d62..b5d1c8feb2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml @@ -124,7 +124,7 @@ Sauver et planifier Sauver et envoyer pour approbation Sauver la mise en page de la liste - Prévisualiser + Prévisualiser La prévisualisation est désactivée car aucun modèle n'a été assigné. Choisir un style Afficher les styles diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml index 9b816b4682..e100cb4301 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml @@ -68,7 +68,7 @@ שמור שמור ופרסם שמור ושלח לאישור - תצוגה מקדימה + תצוגה מקדימה בחר עיצוב הצג עיצוב הוספת טבלה diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml index d2a1d75ee7..4866fff843 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml @@ -69,7 +69,7 @@ Salva Salva e pubblica Salva e invia per approvazione - Anteprima + Anteprima Scegli lo stile Mostra gli stili diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml index dd68ed45e5..21559f915a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml @@ -85,7 +85,7 @@ 保存及び公開 保存して承認に送る リスト ビューの保存 - プレビュー + プレビュー テンプレートが指定されていないのでプレビューは無効になっています スタイルの選択 スタイルの表示 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml index 12f7c9ed50..a87f6f1410 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml @@ -67,7 +67,7 @@ 저장 저장 후 발행 저장 후 승인을 위해 전송 - 미리보기 + 미리보기 스타일 선택 스타일 보기 테이블 삽입 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml index 78b0ebfb5a..731aea4a20 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml @@ -83,7 +83,7 @@ Lagre og publiser Lagre og planlegge Lagre og send til publisering - Forhåndsvis + Forhåndsvis Forhåndsvisning er deaktivert siden det ikke er angitt noen mal Velg formattering Vis stiler diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml index 90f06fe7a6..601626d896 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml @@ -90,7 +90,7 @@ Opslaan en publiceren Opslaan en verzenden voor goedkeuring Sla list view op - voorbeeld bekijken + voorbeeld bekijken Voorbeeld bekijken is uitgeschakeld omdat er geen template is geselecteerd Stijl kiezen Stijlen tonen diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml index de3e988118..fd806041c5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml @@ -121,7 +121,7 @@ Zapisz i publikuj Zapisz i wyślij do zaakceptowania Zapisz widok listy - Podgląd + Podgląd Podgląd jest wyłączony, ponieważ żaden szablon nie został przydzielony Wybierz styl Pokaż style diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml index e7afd04acd..9fd4696c28 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml @@ -67,7 +67,7 @@ Salvar Salvar e publicar Salvar e mandar para aprovação - Prévia + Prévia Escolha estilo Mostrar estilos Inserir tabela diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml index c52e17e829..7a3e099262 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml @@ -155,7 +155,7 @@ Направить на публикацию Сохранить список Выбрать - Предварительный просмотр + Предварительный просмотр Предварительный просмотр запрещен, так как документу не сопоставлен шаблон Другие действия Выбрать стиль diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml index 152a40b965..e7e7abe2cd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml @@ -134,7 +134,7 @@ Spara och skicka för godkännande Schemaläggning Välj - Förhandsgranska + Förhandsgranska Förhandsgranskning är avstängt på grund av att det inte finns någon mall tilldelad Ångra Välj stil diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml index 02069a53dc..f998080b06 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml @@ -85,7 +85,7 @@ Kaydet Kaydet ve Yayınla Kaydet ve Onay için gönder - Önizle + Önizle Önizleme kapalı, Atanmış şablon yok Stili seçin Stilleri Göster diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml index a8dd8a8bef..5210b46bcc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml @@ -90,7 +90,7 @@ 保存并发布 保存并提交审核 保存列表视图 - 预览 + 预览 因未设置模板无法预览 选择样式 显示样式 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml index bac817ad20..320c3f63d8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml @@ -87,7 +87,7 @@ 保存並發佈 保存並提交審核 保存清單檢視 - 預覽 + 預覽 因未設置範本無法預覽 選擇樣式 顯示樣式 From 384746cd26a0e96caa63b4d080a6181c12775d27 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 26 Feb 2020 16:16:26 +0100 Subject: [PATCH 038/159] V8: Use current client culture when searching content pickers (#7123) * Use current client culture when searching content pickers * Use string interpolation --- src/Umbraco.Web/Editors/EntityController.cs | 3 ++- .../Models/Mapping/EntityMapDefinition.cs | 6 +++++ src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 25 +++++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index a894b681f0..3938ae5ab8 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -768,7 +768,8 @@ namespace Umbraco.Web.Editors /// private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null, bool ignoreUserStartNodes = false) { - return _treeSearcher.ExamineSearch(query, entityType, 200, 0, out _, searchFrom, ignoreUserStartNodes); + var culture = ClientCulture(); + return _treeSearcher.ExamineSearch(query, entityType, 200, 0, culture, out _, searchFrom, ignoreUserStartNodes); } private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 34b8f664f3..ddff093aab 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -177,6 +177,12 @@ namespace Umbraco.Web.Models.Mapping target.Name = source.Values.ContainsKey("nodeName") ? source.Values["nodeName"] : "[no name]"; + var culture = context.GetCulture(); + if(culture.IsNullOrWhiteSpace() == false) + { + target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name; + } + if (source.Values.TryGetValue(UmbracoExamineIndex.UmbracoFileFieldName, out var umbracoFile)) { if (umbracoFile != null) diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 146177f86f..dcc156e356 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; using Umbraco.Web.Trees; namespace Umbraco.Web.Search @@ -65,6 +66,16 @@ namespace Umbraco.Web.Search UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) + { + return ExamineSearch(query, entityType, pageSize, pageIndex, culture: null, out totalFound, searchFrom, ignoreUserStartNodes); + } + + public IEnumerable ExamineSearch( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, string culture, + out long totalFound, string searchFrom = null, bool ignoreUserStartNodes = false) { var sb = new StringBuilder(); @@ -140,7 +151,7 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: return MediaFromSearchResults(pagedResult); case UmbracoEntityTypes.Document: - return ContentFromSearchResults(pagedResult); + return ContentFromSearchResults(pagedResult, culture); default: throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType); } @@ -443,13 +454,17 @@ namespace Umbraco.Web.Search /// /// /// - private IEnumerable ContentFromSearchResults(IEnumerable results) + private IEnumerable ContentFromSearchResults(IEnumerable results, string culture = null) { var defaultLang = _languageService.GetDefaultLanguageIsoCode(); - foreach (var result in results) { - var entity = _mapper.Map(result); + var entity = _mapper.Map(result, context => { + if(culture != null) { + context.SetCulture(culture); + } + } + ); var intId = entity.Id.TryConvertTo(); if (intId.Success) @@ -457,7 +472,7 @@ namespace Umbraco.Web.Search //if it varies by culture, return the default language URL if (result.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var varies) && varies == "y") { - entity.AdditionalData["Url"] = _umbracoContext.Url(intId.Result, defaultLang); + entity.AdditionalData["Url"] = _umbracoContext.Url(intId.Result, culture ?? defaultLang); } else { From af7919ce65dd3c743cdded13d68be07c7f81929f Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 27 Feb 2020 09:07:45 +1000 Subject: [PATCH 039/159] make events public --- src/Umbraco.Core/Events/UserGroupWithUsers.cs | 2 +- src/Umbraco.Core/Services/Implement/UserService.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Events/UserGroupWithUsers.cs b/src/Umbraco.Core/Events/UserGroupWithUsers.cs index b69650d33f..7d456a22ea 100644 --- a/src/Umbraco.Core/Events/UserGroupWithUsers.cs +++ b/src/Umbraco.Core/Events/UserGroupWithUsers.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Events { - internal class UserGroupWithUsers + public class UserGroupWithUsers { public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers) { diff --git a/src/Umbraco.Core/Services/Implement/UserService.cs b/src/Umbraco.Core/Services/Implement/UserService.cs index 363bc72bc3..95ab5376ff 100644 --- a/src/Umbraco.Core/Services/Implement/UserService.cs +++ b/src/Umbraco.Core/Services/Implement/UserService.cs @@ -1197,12 +1197,12 @@ namespace Umbraco.Core.Services.Implement /// /// Occurs before Save /// - internal static event TypedEventHandler> SavingUserGroup; + public static event TypedEventHandler> SavingUserGroup; /// /// Occurs after Save /// - internal static event TypedEventHandler> SavedUserGroup; + public static event TypedEventHandler> SavedUserGroup; /// /// Occurs before Delete From 0f540bc74db2123bf6254932bdf613d8c14dee7c Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Tue, 25 Feb 2020 22:50:41 +0000 Subject: [PATCH 040/159] RTE config tool bar options are messed up --- .../src/less/components/umb-form-check.less | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 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 1c5c275642..a52f81b92a 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 @@ -1,5 +1,8 @@ -@checkboxWidth: 16px; -@checkboxHeight: 16px; +@checkboxWidth: 18px; +@checkboxHeight: 18px; +label.umb-form-check--checkbox{ + margin:3px 0; +} .umb-form-check { display: flex; @@ -10,11 +13,11 @@ cursor: pointer !important; .umb-form-check__symbol { - margin-top: 1px; + margin-top: 4px; margin-right: 10px; } .umb-form-check__info { - + margin-left:20px; } @@ -95,6 +98,10 @@ &__state { display: flex; height: 18px; + position: absolute; + margin-top: 2px; + top: 0; + left: -1px; } &__check { From e6a55a215a5c51fe6aa048eece32807f7c176408 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 29 Feb 2020 14:26:35 +0100 Subject: [PATCH 041/159] Replaced nonodes.aspx with MVC controller and view, with configurable view location. --- src/Umbraco.Configuration/GlobalSettings.cs | 17 +++++ .../Configuration/IGlobalSettings.cs | 5 ++ src/Umbraco.Core/Constants-AppSettings.cs | 5 ++ src/Umbraco.Core/Constants-Web.cs | 5 ++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Web/Mvc/RenderNoContentControllerTests.cs | 54 ++++++++++++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 10 +-- .../config/splashes/NoNodes.aspx.cs | 22 ------- .../config/splashes/NoNodes.aspx.designer.cs | 15 ----- .../config/splashes/NoNodes.cshtml | 49 +++++++++++++++ .../config/splashes/noNodes.aspx | 63 ------------------- src/Umbraco.Web/Models/NoNodesViewModel.cs | 7 +++ .../Mvc/RenderNoContentController.cs | 39 ++++++++++++ .../Runtime/WebInitialComponent.cs | 13 +++- src/Umbraco.Web/Umbraco.Web.csproj | 2 + src/Umbraco.Web/UmbracoInjectedModule.cs | 11 ++-- 16 files changed, 200 insertions(+), 118 deletions(-) create mode 100644 src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs delete mode 100644 src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs delete mode 100644 src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.designer.cs create mode 100644 src/Umbraco.Web.UI/config/splashes/NoNodes.cshtml delete mode 100644 src/Umbraco.Web.UI/config/splashes/noNodes.aspx create mode 100644 src/Umbraco.Web/Models/NoNodesViewModel.cs create mode 100644 src/Umbraco.Web/Mvc/RenderNoContentController.cs diff --git a/src/Umbraco.Configuration/GlobalSettings.cs b/src/Umbraco.Configuration/GlobalSettings.cs index a44f7ae636..6ed786f473 100644 --- a/src/Umbraco.Configuration/GlobalSettings.cs +++ b/src/Umbraco.Configuration/GlobalSettings.cs @@ -407,5 +407,22 @@ namespace Umbraco.Core.Configuration return backingField; } + + /// + /// Gets the path to the razor file used when no published content is available. + /// + public string NoNodesViewPath + { + get + { + var configuredValue = ConfigurationManager.AppSettings[Constants.AppSettings.NoNodesViewPath]; + if (!string.IsNullOrWhiteSpace(configuredValue)) + { + return configuredValue; + } + + return "~/config/splashes/NoNodes.cshtml"; + } + } } } diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs index 1b1f328142..d63b1ddc6a 100644 --- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs @@ -95,5 +95,10 @@ bool DisableElectionForSingleServer { get; } string RegisterType { get; } string DatabaseFactoryServerVersion { get; } + + /// + /// Gets the path to the razor file used when no published content is available. + /// + string NoNodesViewPath { get; } } } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 4c47f12ba0..c55b4b0314 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -115,6 +115,11 @@ namespace Umbraco.Core /// public const string DisableElectionForSingleServer = "Umbraco.Core.DisableElectionForSingleServer"; + /// + /// Gets the path to the razor file used when no published content is available. + /// + public const string NoNodesViewPath = "Umbraco.Core.NoNodesViewPath"; + /// /// Debug specific web.config AppSetting keys for Umbraco /// diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index a1e138116d..17fddd98ea 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -34,6 +34,11 @@ /// The header name that angular uses to pass in the token to validate the cookie /// public const string AngularHeadername = "X-UMB-XSRF-TOKEN"; + + /// + /// The route for rendering a page when no content is published. + /// + public const string NoContentRoute = "/UmbNoContent"; } } } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 9815c94728..e0bd475a46 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -286,6 +286,7 @@ + diff --git a/src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs new file mode 100644 index 0000000000..e728d75dc5 --- /dev/null +++ b/src/Umbraco.Tests/Web/Mvc/RenderNoContentControllerTests.cs @@ -0,0 +1,54 @@ +using System.Web.Mvc; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Web; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.Tests.Web.Mvc +{ + [TestFixture] + public class RenderNoContentControllerTests + { + [Test] + public void Redirects_To_Root_When_Content_Published() + { + var mockUmbracoContext = new Mock(); + mockUmbracoContext.Setup(x => x.Content.HasContent()).Returns(true); + var mockIOHelper = new Mock(); + var mockGlobalSettings = new Mock(); + var controller = new RenderNoContentController(mockUmbracoContext.Object, mockIOHelper.Object, mockGlobalSettings.Object); + + var result = controller.Index() as RedirectResult; + + Assert.IsNotNull(result); + Assert.AreEqual("~/", result.Url); + } + + [Test] + public void Renders_View_When_No_Content_Published() + { + const string UmbracoPathSetting = "~/umbraco"; + const string UmbracoPath = "/umbraco"; + const string ViewPath = "~/config/splashes/NoNodes.cshtml"; + var mockUmbracoContext = new Mock(); + mockUmbracoContext.Setup(x => x.Content.HasContent()).Returns(false); + var mockIOHelper = new Mock(); + mockIOHelper.Setup(x => x.ResolveUrl(It.Is(y => y == UmbracoPathSetting))).Returns(UmbracoPath); + var mockGlobalSettings = new Mock(); + mockGlobalSettings.SetupGet(x => x.UmbracoPath).Returns(UmbracoPathSetting); + mockGlobalSettings.SetupGet(x => x.NoNodesViewPath).Returns(ViewPath); + var controller = new RenderNoContentController(mockUmbracoContext.Object, mockIOHelper.Object, mockGlobalSettings.Object); + + var result = controller.Index() as ViewResult; + Assert.IsNotNull(result); + Assert.AreEqual(ViewPath, result.ViewName); + + var model = result.Model as NoNodesViewModel; + Assert.IsNotNull(model); + Assert.AreEqual(UmbracoPath, model.UmbracoPath); + } + } +} diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 7a7b1bada5..6e955140d9 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -141,13 +141,6 @@ Properties\SolutionInfo.cs - - noNodes.aspx - ASPXCodeBehind - - - noNodes.aspx - True @@ -173,11 +166,10 @@ - + - ClientDependency.config diff --git a/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs b/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs deleted file mode 100644 index 51653d54ca..0000000000 --- a/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.UI.Config.Splashes -{ - public partial class NoNodes : System.Web.UI.Page - { - - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - var store = Current.UmbracoContext.Content; - if (store.HasContent()) - { - //if there is actually content, go to the root - Response.Redirect("~/"); - } - } - - } -} diff --git a/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.designer.cs b/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.designer.cs deleted file mode 100644 index 350b610bf0..0000000000 --- a/src/Umbraco.Web.UI/config/splashes/NoNodes.aspx.designer.cs +++ /dev/null @@ -1,15 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Umbraco.Web.UI.Config.Splashes { - - - public partial class NoNodes { - } -} diff --git a/src/Umbraco.Web.UI/config/splashes/NoNodes.cshtml b/src/Umbraco.Web.UI/config/splashes/NoNodes.cshtml new file mode 100644 index 0000000000..4193e58873 --- /dev/null +++ b/src/Umbraco.Web.UI/config/splashes/NoNodes.cshtml @@ -0,0 +1,49 @@ +@inherits System.Web.Mvc.WebViewPage + + + + + + + + Umbraco: No Published Content + + + + + +
+
+
+ + +

Welcome to your Umbraco installation

+

You're seeing this wonderful page because your website doesn't contain any published content yet.

+ + + +
+
+

Easy start with Umbraco.tv

+

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

+ + Umbraco.tv → +
+ +
+

Be a part of the community

+

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

+ + our.Umbraco → +
+
+ +
+
+ +
+ + + diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx deleted file mode 100644 index 961f2e006d..0000000000 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ /dev/null @@ -1,63 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="True" Inherits="Umbraco.Web.UI.Config.Splashes.NoNodes" CodeBehind="NoNodes.aspx.cs" %> -<%@ Import Namespace="Umbraco.Core" %> -<%@ Import Namespace="Umbraco.Web.Composing" %> -<%@ Import Namespace="Umbraco.Core.Configuration" %> -<%@ Import Namespace="Umbraco.Core.IO" %> - - - - - - - - - - - - - - - - - - - - -
-
-
- - -

Welcome to your Umbraco installation

-

You're seeing this wonderful page because your website doesn't contain any published content yet.

- - - - -
-
-

Easy start with Umbraco.tv

-

We have created a bunch of 'how-to' videos, to get you easily started with Umbraco. Learn how to build projects in just a couple of minutes. Easiest CMS in the world.

- - Umbraco.tv → -
- -
-

Be a part of the community

-

The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

- - our.Umbraco → -
-
- -
-
- -
- - - - - diff --git a/src/Umbraco.Web/Models/NoNodesViewModel.cs b/src/Umbraco.Web/Models/NoNodesViewModel.cs new file mode 100644 index 0000000000..ca75fd3c02 --- /dev/null +++ b/src/Umbraco.Web/Models/NoNodesViewModel.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Web.Models +{ + public class NoNodesViewModel + { + public string UmbracoPath { get; set; } + } +} diff --git a/src/Umbraco.Web/Mvc/RenderNoContentController.cs b/src/Umbraco.Web/Mvc/RenderNoContentController.cs new file mode 100644 index 0000000000..2ffd323440 --- /dev/null +++ b/src/Umbraco.Web/Mvc/RenderNoContentController.cs @@ -0,0 +1,39 @@ +using System; +using System.Web.Mvc; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Web.Models; + +namespace Umbraco.Web.Mvc +{ + public class RenderNoContentController : Controller + { + private readonly IUmbracoContext _umbracoContext; + private readonly IIOHelper _ioHelper; + private readonly IGlobalSettings _globalSettings; + + public RenderNoContentController(IUmbracoContext umbracoContext, IIOHelper ioHelper, IGlobalSettings globalSettings) + { + _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); + _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); + _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); + } + + public ActionResult Index() + { + var store = _umbracoContext.Content; + if (store.HasContent()) + { + // If there is actually content, go to the root. + return Redirect("~/"); + } + + var model = new NoNodesViewModel + { + UmbracoPath = _ioHelper.ResolveUrl(_globalSettings.UmbracoPath), + }; + + return View(_globalSettings.NoNodesViewPath, model); + } + } +} diff --git a/src/Umbraco.Web/Runtime/WebInitialComponent.cs b/src/Umbraco.Web/Runtime/WebInitialComponent.cs index ea7b5b94e7..933cababed 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComponent.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComponent.cs @@ -17,7 +17,6 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.Strings; using Umbraco.Core.IO; -using Umbraco.Core.Strings; using Umbraco.Web.Install; using Umbraco.Web.JavaScript; using Umbraco.Web.Mvc; @@ -181,6 +180,9 @@ namespace Umbraco.Web.Runtime ); defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory(), shortStringHelper); + // register no content route + RouteNoContentController(umbracoPath); + // register install routes RouteTable.Routes.RegisterArea(); @@ -191,6 +193,14 @@ namespace Umbraco.Web.Runtime RoutePluginControllers(globalSettings, surfaceControllerTypes, apiControllerTypes, ioHelper); } + private static void RouteNoContentController(string umbracoPath) + { + RouteTable.Routes.MapRoute( + "umbraco-no-content", + umbracoPath + Core.Constants.Web.NoContentRoute, + new { controller = "RenderNoContent", action = "Index" }); + } + private static void RoutePluginControllers( IGlobalSettings globalSettings, SurfaceControllerTypeCollection surfaceControllerTypes, @@ -252,6 +262,5 @@ namespace Umbraco.Web.Runtime // make it use our custom/special SurfaceMvcHandler route.RouteHandler = new SurfaceRouteHandler(); } - } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 27f372753a..43ae4d5e57 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -143,6 +143,8 @@ + + diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index a311d76f73..d3c72090db 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -1,17 +1,14 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Web; using System.Web.Routing; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Web.Routing; using Umbraco.Core.Exceptions; -using Umbraco.Core.Security; +using Umbraco.Core.Logging; using Umbraco.Web.Composing; +using Umbraco.Web.Routing; using Umbraco.Web.Security; namespace Umbraco.Web @@ -244,8 +241,8 @@ namespace Umbraco.Web _logger.Warn("Umbraco has no content"); - const string noContentUrl = "~/config/splashes/noNodes.aspx"; - httpContext.RewritePath(_uriUtility.ToAbsolute(noContentUrl)); + var rewriteTo = _uriUtility.ToAbsolute(_globalSettings.UmbracoPath + Constants.Web.NoContentRoute); + httpContext.RewritePath(_uriUtility.ToAbsolute(rewriteTo)); return false; } From 2853448963bfafdb57e76e9b58b8bf9ce321a36e Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Sat, 29 Feb 2020 20:50:50 +0000 Subject: [PATCH 042/159] Improve accessibility of grid prevalues screen (#6949) * WIP Improve accessibility of grid prevalues screen * Clean up of the view, improve accessibility and add localization fallbacks * forgot to commit the stylesheet * formatting fixes --- .../src/less/gridview.less | 22 +++++ .../propertyeditors/grid/grid.prevalues.html | 91 +++++++++++-------- 2 files changed, 73 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index 238feead90..11ba7b2795 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -101,6 +101,9 @@ position:relative; } + .usky-grid .grid-layout { + max-width: 600px; +} // ROW // ------------------------- @@ -517,6 +520,25 @@ position:relative; } + .usky-grid .uSky-templates .layout { + margin-top: 5px; + margin-bottom: 20px; + float: left; +} + + +.usky-grid .uSky-templates .columns { + margin-top: 5px; + margin-bottom: 25px; + float: left; +} + + +.usky-grid .uSky-templates .columns .preview-cell p { + font-size: 6px; + line-height: 8px; + text-align: center; +} /**************************************************************************************************/ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html index 92d1a9ef26..986f0cbc7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html @@ -1,9 +1,15 @@
-
+
-

-

+

+ + Grid Layouts + +

+

+ Layouts are the overall work area for the grid editor, usually you only need one or two different layouts +

    + class="preview-rows layout">
    {{template.name}}
    - - +
-
-
+
-

-

+

+ Row Configurations +

+

+ Rows are predefined cells arranged horizontally +

    + class="preview-rows columns">
    -

    {{area.maxItems}}

    +

    {{area.maxItems}}

    @@ -74,17 +85,18 @@
    {{layout.label || layout.name}}
    - - - - +
@@ -93,7 +105,7 @@
-
+
@@ -108,22 +120,21 @@ ng-model="model.value.config">
  • - + - - +
    • -
    • - - - - - +
    • +
    @@ -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 043/159] 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 @@