From 8a60c95a36242ee6cbae36232599bed38e902ec9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 12 Sep 2019 17:04:29 +1000 Subject: [PATCH 01/25] 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 02/25] #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 03/25] #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 04/25] 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 05/25] 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 06/25] 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 07/25] 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 08/25] 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 09/25] 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 10/25] 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 11/25] 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 12/25] 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 13/25] 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 6f67105645e616a8b36dcddb1db7cabd8c9a3c2d Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 11 Dec 2019 09:25:30 +0100 Subject: [PATCH 14/25] 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 15/25] 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 16/25] 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 d4e6eb2b6b052b79d34d095f2405b21f15618ede Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Jan 2020 17:04:38 +0100 Subject: [PATCH 17/25] 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 18/25] 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 19/25] 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 4cbaec68f0d5c7f6b5624346c584d0d9b3f3d831 Mon Sep 17 00:00:00 2001 From: elitsa Date: Tue, 3 Mar 2020 12:07:56 +0100 Subject: [PATCH 20/25] Obsoleting UmbracoHelper props which are going to be removed in the future --- src/Umbraco.Web/UmbracoHelper.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 1bc8df5b24..b72cd8069c 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -70,24 +70,31 @@ namespace Umbraco.Web #endregion // ensures that we can return the specified value + [Obsolete("This method is only used in Obsolete properties")] T Ensure(T o) where T : class => o ?? throw new InvalidOperationException("This UmbracoHelper instance has not been initialized."); - + + [Obsolete("Inject and use an instance of " + nameof(IUmbracoComponentRenderer) + " in the constructor for using it in classes or get it from Current in views")] private IUmbracoComponentRenderer ComponentRenderer => Ensure(_componentRenderer); + + [Obsolete("Inject and use an instance of " + nameof(ICultureDictionaryFactory) + " in the constructor instead")] private ICultureDictionaryFactory CultureDictionaryFactory => Ensure(_cultureDictionaryFactory); /// /// Gets the tag context. /// + [Obsolete("Inject and use an instance of " + nameof(ITagQuery) + " in the constructor instead")] public ITagQuery TagQuery => Ensure(_tagQuery); /// /// Gets the query context. /// + [Obsolete("Inject and use an instance of " + nameof(IPublishedContentQuery) + " in the constructor for using it in classes or get it from Current in views")] public IPublishedContentQuery ContentQuery => Ensure(_publishedContentQuery); /// /// Gets the membership helper. /// + [Obsolete("Inject and use an instance of " + nameof(Security.MembershipHelper) + " in the constructor instead")] public MembershipHelper MembershipHelper => Ensure(_membershipHelper); /// From 8bc077fa1871cb050caa390dfee22890d8b8048c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 4 Mar 2020 13:37:05 +0100 Subject: [PATCH 21/25] Introduce methods on current, that can be necessary if not using the obsolete properties. --- src/Umbraco.Web/Composing/Current.cs | 6 ++++++ src/Umbraco.Web/Models/RegisterModel.cs | 3 +-- src/Umbraco.Web/UmbracoHelper.cs | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index ec65046e84..2419eaa6d4 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -88,6 +88,12 @@ namespace Umbraco.Web.Composing public static UmbracoHelper UmbracoHelper => Factory.GetInstance(); + public static IUmbracoComponentRenderer UmbracoComponentRenderer + => Factory.GetInstance(); + public static ITagQuery TagQuery + => Factory.GetInstance(); + public static IPublishedContentQuery PublishedContentQuery + => Factory.GetInstance(); public static DistributedCache DistributedCache => Factory.GetInstance(); diff --git a/src/Umbraco.Web/Models/RegisterModel.cs b/src/Umbraco.Web/Models/RegisterModel.cs index 86a5459a74..fd6cadc04e 100644 --- a/src/Umbraco.Web/Models/RegisterModel.cs +++ b/src/Umbraco.Web/Models/RegisterModel.cs @@ -5,7 +5,6 @@ using System.ComponentModel.DataAnnotations; using System.Web; using System.Web.Mvc; using Umbraco.Core; -using Umbraco.Core.Composing; using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; @@ -65,7 +64,7 @@ namespace Umbraco.Web.Models /// [Required] public string Password { get; set; } - + /// /// The username of the model, if UsernameIsEmail is true then this is ignored. /// diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index b72cd8069c..7224955922 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -72,23 +72,23 @@ namespace Umbraco.Web // ensures that we can return the specified value [Obsolete("This method is only used in Obsolete properties")] T Ensure(T o) where T : class => o ?? throw new InvalidOperationException("This UmbracoHelper instance has not been initialized."); - - [Obsolete("Inject and use an instance of " + nameof(IUmbracoComponentRenderer) + " in the constructor for using it in classes or get it from Current in views")] + + [Obsolete("Inject and use an instance of " + nameof(IUmbracoComponentRenderer) + " in the constructor for using it in classes or get it from Current.UmbracoComponentRenderer in views.")] private IUmbracoComponentRenderer ComponentRenderer => Ensure(_componentRenderer); - [Obsolete("Inject and use an instance of " + nameof(ICultureDictionaryFactory) + " in the constructor instead")] + [Obsolete("Inject and use an instance of " + nameof(ICultureDictionaryFactory) + " in the constructor for using it in classes or get it from Current.CultureDictionaryFactory in views.")] private ICultureDictionaryFactory CultureDictionaryFactory => Ensure(_cultureDictionaryFactory); /// /// Gets the tag context. /// - [Obsolete("Inject and use an instance of " + nameof(ITagQuery) + " in the constructor instead")] + [Obsolete("Inject and use an instance of " + nameof(ITagQuery) + " in the constructor for using it in classes or get it from Current.TagQuery in views.")] public ITagQuery TagQuery => Ensure(_tagQuery); /// /// Gets the query context. /// - [Obsolete("Inject and use an instance of " + nameof(IPublishedContentQuery) + " in the constructor for using it in classes or get it from Current in views")] + [Obsolete("Inject and use an instance of " + nameof(IPublishedContentQuery) + " in the constructor for using it in classes or get it from Current.PublishedContentQuery in views")] public IPublishedContentQuery ContentQuery => Ensure(_publishedContentQuery); /// From 6241bee1b22afd68a54d4f6233de8b381e64f1f1 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 4 Mar 2020 13:37:05 +0100 Subject: [PATCH 22/25] Introduce methods on current, that can be necessary if not using the obsolete properties. --- src/Umbraco.Web/Composing/Current.cs | 6 ++++++ src/Umbraco.Web/Models/RegisterModel.cs | 3 +-- src/Umbraco.Web/UmbracoHelper.cs | 10 +++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index ec65046e84..2419eaa6d4 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -88,6 +88,12 @@ namespace Umbraco.Web.Composing public static UmbracoHelper UmbracoHelper => Factory.GetInstance(); + public static IUmbracoComponentRenderer UmbracoComponentRenderer + => Factory.GetInstance(); + public static ITagQuery TagQuery + => Factory.GetInstance(); + public static IPublishedContentQuery PublishedContentQuery + => Factory.GetInstance(); public static DistributedCache DistributedCache => Factory.GetInstance(); diff --git a/src/Umbraco.Web/Models/RegisterModel.cs b/src/Umbraco.Web/Models/RegisterModel.cs index 86a5459a74..fd6cadc04e 100644 --- a/src/Umbraco.Web/Models/RegisterModel.cs +++ b/src/Umbraco.Web/Models/RegisterModel.cs @@ -5,7 +5,6 @@ using System.ComponentModel.DataAnnotations; using System.Web; using System.Web.Mvc; using Umbraco.Core; -using Umbraco.Core.Composing; using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; @@ -65,7 +64,7 @@ namespace Umbraco.Web.Models /// [Required] public string Password { get; set; } - + /// /// The username of the model, if UsernameIsEmail is true then this is ignored. /// diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index b72cd8069c..7224955922 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -72,23 +72,23 @@ namespace Umbraco.Web // ensures that we can return the specified value [Obsolete("This method is only used in Obsolete properties")] T Ensure(T o) where T : class => o ?? throw new InvalidOperationException("This UmbracoHelper instance has not been initialized."); - - [Obsolete("Inject and use an instance of " + nameof(IUmbracoComponentRenderer) + " in the constructor for using it in classes or get it from Current in views")] + + [Obsolete("Inject and use an instance of " + nameof(IUmbracoComponentRenderer) + " in the constructor for using it in classes or get it from Current.UmbracoComponentRenderer in views.")] private IUmbracoComponentRenderer ComponentRenderer => Ensure(_componentRenderer); - [Obsolete("Inject and use an instance of " + nameof(ICultureDictionaryFactory) + " in the constructor instead")] + [Obsolete("Inject and use an instance of " + nameof(ICultureDictionaryFactory) + " in the constructor for using it in classes or get it from Current.CultureDictionaryFactory in views.")] private ICultureDictionaryFactory CultureDictionaryFactory => Ensure(_cultureDictionaryFactory); /// /// Gets the tag context. /// - [Obsolete("Inject and use an instance of " + nameof(ITagQuery) + " in the constructor instead")] + [Obsolete("Inject and use an instance of " + nameof(ITagQuery) + " in the constructor for using it in classes or get it from Current.TagQuery in views.")] public ITagQuery TagQuery => Ensure(_tagQuery); /// /// Gets the query context. /// - [Obsolete("Inject and use an instance of " + nameof(IPublishedContentQuery) + " in the constructor for using it in classes or get it from Current in views")] + [Obsolete("Inject and use an instance of " + nameof(IPublishedContentQuery) + " in the constructor for using it in classes or get it from Current.PublishedContentQuery in views")] public IPublishedContentQuery ContentQuery => Ensure(_publishedContentQuery); /// From 9be30f3e91028ac40a83ef2b440f7a67e350f4cb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 10 Mar 2020 10:36:34 +0100 Subject: [PATCH 23/25] Added info to obsolete message --- src/Umbraco.Web/UmbracoHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 7224955922..6a2fba1152 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -94,7 +94,7 @@ namespace Umbraco.Web /// /// Gets the membership helper. /// - [Obsolete("Inject and use an instance of " + nameof(Security.MembershipHelper) + " in the constructor instead")] + [Obsolete("Inject and use an instance of " + nameof(Security.MembershipHelper) + " in the constructor instead. In views you can use @Members.")] public MembershipHelper MembershipHelper => Ensure(_membershipHelper); /// From 7ca6781d09b8cc398db3d4a08f1f9d6f1ec02e29 Mon Sep 17 00:00:00 2001 From: Marc Goodson Date: Tue, 10 Mar 2020 20:49:59 +0000 Subject: [PATCH 24/25] v7: Fix false matches for AltTemplate convention Urls (#7150) --- .../ContentFinderByNiceUrlAndTemplate.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs index f19886c4f2..f97e81791e 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Routing /// /// Tries to find and assign an Umbraco document to a PublishedContentRequest. /// - /// The PublishedContentRequest. + /// The PublishedContentRequest. /// A value indicating whether an Umbraco document was found and assigned. /// If successful, also assigns the template. public override bool TryFindContent(PublishedContentRequest docRequest) @@ -43,16 +43,24 @@ namespace Umbraco.Web.Routing var route = docRequest.HasDomain ? (docRequest.Domain.RootNodeId.ToString() + path) : path; node = FindContent(docRequest, route); - - if (node.IsAllowedTemplate(template.Id)) + //not guaranteed to find a node - just because last portion of url contains a template alias, doesn't mean remaining part of the url is a published node + if (node != null) { - docRequest.TemplateModel = template; + if (node.IsAllowedTemplate(template.Id)) + { + docRequest.TemplateModel = template; + } + else + { + LogHelper.Warn("Configuration settings prevent template \"{0}\" from showing for node \"{1}\"", () => templateAlias, () => node.Id); + docRequest.PublishedContent = null; + node = null; + } } else { - LogHelper.Warn("Configuration settings prevent template \"{0}\" from showing for node \"{1}\"", () => templateAlias, () => node.Id); + LogHelper.Debug("Attempt to find content by alternative template alias: \"{0}\" triggered because end portion of url matched template alias, but no node exists for the url without the alt template alias at the route: \"{1}\"", () => templateAlias, () => route); docRequest.PublishedContent = null; - node = null; } } else @@ -68,4 +76,4 @@ namespace Umbraco.Web.Routing return node != null; } } -} \ No newline at end of file +} From d11a1946f573f055c88a219bee3f7f1d6b058aa7 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Tue, 10 Mar 2020 20:52:53 +0000 Subject: [PATCH 25/25] V7: Don't call ClearPreviewXmlContent from ClearDocumentXmlCache (#6631) --- src/Umbraco.Web/umbraco.presentation/content.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 484014656c..ca847f628b 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -478,8 +478,6 @@ namespace umbraco safeXml.AcceptChanges(); } } - - ClearPreviewXmlContent(id); } ///